Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb90ace65 | ||
|
|
01aed205fe | ||
|
|
14991ae2aa | ||
|
|
1de233b197 | ||
|
|
32a17fb255 | ||
|
|
4f17092305 | ||
|
|
58868c9c18 | ||
|
|
7054beebeb | ||
|
|
8a5da5e906 | ||
|
|
2083516b8c | ||
| f11e5ec210 | |||
|
|
27b15773e0 | ||
| 8575de6ea5 | |||
|
|
5440aa7d6d | ||
| 30ac26160b | |||
| b884cc97a0 | |||
| f1b9e35b09 | |||
|
|
5945e8c761 | ||
| cd96935c6b | |||
|
|
a807bdb74d | ||
| a95278368d | |||
|
|
791f98bf61 | ||
| e024150c5a | |||
| 89d09777a0 | |||
|
|
aa61e8a931 | ||
| 8c0580d140 | |||
|
|
8f1b51ef4f | ||
| 16d5d95c4e | |||
|
|
5945e1b0e7 | ||
| bd363d0e33 | |||
|
|
3d155fb097 | ||
| 1f1deb5f7f | |||
|
|
787c52a8dc | ||
| 7a1ad47439 | |||
|
|
9effc859ac | ||
| 8d31834112 | |||
| 9f0c81ea82 | |||
|
|
df7f38bbd8 | ||
|
|
782c56f9b6 | ||
|
|
38267640c3 | ||
| 1b08ff46b7 | |||
|
|
099c3e2db2 | ||
| ff6203d387 | |||
| 286d7916e9 | |||
|
|
a7864a05ee | ||
| cf72bc6827 | |||
| 03457a7f9e | |||
| 0c92d8e4c2 | |||
| 2f1f409710 | |||
| 9b21287bd0 | |||
|
|
f7606f09cf | ||
| 449ebf83cb | |||
| fd6dbfc8da | |||
|
|
ceb5d83d75 | ||
| 8eb03ddedc | |||
|
|
a5c2a44512 | ||
|
|
3853b8faf8 | ||
|
|
67f83e94dc | ||
| fe2b29da9c | |||
|
|
3dd3ceeb3e | ||
| c32d326c69 | |||
|
|
90bf763bde | ||
| 1438e39ffd | |||
|
|
e2ab13d599 | ||
| 737737a6c1 | |||
|
|
e15c1db49b | ||
| 340670742c | |||
| 10cefc3c11 | |||
|
|
bdd3e125a0 | ||
| 9a4e2bba79 | |||
|
|
92d69d9166 | ||
|
|
7312d3f91a | ||
|
|
3d20c47eb9 | ||
| 177eddc695 | |||
|
|
5e9e87bbba | ||
| 3c57b99064 | |||
| 9d9677972b | |||
| a2fa008cb6 | |||
| 303c33b44d | |||
|
|
7d9297616f | ||
|
|
f651e0e3f0 | ||
|
|
461d743ea6 | ||
| 5896b422f7 | |||
| aabd450ea3 | |||
| 0abac8518c | |||
| 5247de38df | |||
| 40846dd7f4 | |||
| 29fdf85c07 | |||
| f6c55d4185 | |||
|
|
7ef6b36ff5 | ||
|
|
d8154025ea | ||
|
|
5c73bbf8bc | ||
|
|
bcb76c0a99 | ||
|
|
99446eca74 | ||
|
|
1472646624 | ||
|
|
b685e64e71 | ||
|
|
ee7c76ef68 | ||
|
|
afcd360603 | ||
|
|
3352a59ff9 | ||
|
|
468a092fd9 | ||
|
|
7c0667cb39 | ||
|
|
2d05024e82 | ||
|
|
2dddcc9804 | ||
|
|
d879626b56 | ||
|
|
8ecec1edbe | ||
|
|
95534f5304 | ||
| 160b3f38ed | |||
|
|
2dd384e8e4 | ||
| c6ae22c7e1 | |||
| a6a9e5861e | |||
|
|
a584f2a61a | ||
| b80e205680 | |||
|
|
9a2ea3f2a3 | ||
| 80e9bba722 | |||
| 66dd6051e5 | |||
| 3ab1c82a37 | |||
| d72fe1a781 | |||
| b7d9d73de5 | |||
| ca65024cd2 | |||
|
|
2ae051f07d | ||
| 31ca5d0819 | |||
|
|
9ff009712a | ||
| 0d9ab498b1 | |||
| 12c1b1dfb8 | |||
|
|
fa2b0486d0 | ||
|
|
8c150054a0 | ||
|
|
a9cb0c6b4f | ||
|
|
95acd6e76a | ||
|
|
aab2a6767e | ||
|
|
00391fba67 | ||
|
|
f9cc46f879 | ||
|
|
a0c97dc4c2 | ||
|
|
e60c99d81e | ||
|
|
734dd8d1fe | ||
|
|
1b196ac4ca | ||
|
|
0bdac1bff8 | ||
|
|
82e31e0ef0 | ||
|
|
707ace7ba1 | ||
|
|
bc419e2d22 | ||
|
|
dddf9a7a74 | ||
|
|
d47b9536ee | ||
|
|
533b1d2b01 | ||
|
|
1868d26472 | ||
|
|
1dcaf847a6 | ||
|
|
8eb17baf35 | ||
|
|
e67392e2c0 | ||
| 6ad58b81b0 | |||
| 42633da853 | |||
|
|
b4e7d957e4 | ||
| c819bdaa1c | |||
| bb5ef9ccbf | |||
| 982e896916 | |||
| a21e2693fa | |||
|
|
e7a7bd2644 | ||
|
|
b64f15a483 |
419
.gitea/README.md
Normal file
419
.gitea/README.md
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# 🚀 GUI客户端自动化版本管理系统
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
**与后端完全一致的版本管理系统** - 直接连接PostgreSQL数据库
|
||||||
|
|
||||||
|
- ✅ **直接数据库操作** - 与后端使用相同方式(PostgreSQL)
|
||||||
|
- ✅ **完全一致** - 相同的表、相同的字段、相同的逻辑
|
||||||
|
- ✅ **极简设计** - 无需API,配置一次,永久生效
|
||||||
|
- ✅ **全自动** - 提交代码即可,自动版本递增
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 核心特性
|
||||||
|
|
||||||
|
### 存储方式(与后端完全一致)
|
||||||
|
|
||||||
|
```
|
||||||
|
后端Django GUI客户端
|
||||||
|
↓ ↓
|
||||||
|
Django ORM psycopg2
|
||||||
|
↓ ↓
|
||||||
|
└──────────┬──────────────────┘
|
||||||
|
↓
|
||||||
|
PostgreSQL
|
||||||
|
web_versionhistory表
|
||||||
|
```
|
||||||
|
|
||||||
|
| 特性 | 后端 | GUI |
|
||||||
|
|------|------|-----|
|
||||||
|
| **操作方式** | Django ORM | psycopg2(SQL) |
|
||||||
|
| **数据库** | PostgreSQL (8.155.9.53) | **相同** |
|
||||||
|
| **表名** | web_versionhistory | **相同** |
|
||||||
|
| **版本类型** | "Web端" | "水滴智能通讯插件" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始(2步)
|
||||||
|
|
||||||
|
### 1️⃣ 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install psycopg2-binary
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 提交代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 新增功能(MINOR版本)
|
||||||
|
git commit -m "[minor] 新增拼多多平台支持"
|
||||||
|
|
||||||
|
# 修复Bug(PATCH版本)
|
||||||
|
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数据库操作,与后端完全一致
|
||||||
505
.gitea/scripts/gui_version_creator.py
Normal file
505
.gitea/scripts/gui_version_creator.py
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
#!/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})")
|
||||||
|
logger.info(f" 📦 版本: {version_record['version']}")
|
||||||
|
logger.info(f" 🔗 下载地址: {version_record.get('download_url', '')}")
|
||||||
|
logger.info(f" 📝 内容: {version_record['content'][:50]}...")
|
||||||
|
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"""
|
||||||
|
logger.info(f"正在更新config.py到版本: {new_version}")
|
||||||
|
logger.info(f"配置文件路径: {self.config_file}")
|
||||||
|
logger.info(f"配置文件绝对路径: {self.config_file.absolute()}")
|
||||||
|
logger.info(f"配置文件是否存在: {self.config_file.exists()}")
|
||||||
|
|
||||||
|
if not self.config_file.exists():
|
||||||
|
logger.error(f"❌ 配置文件不存在: {self.config_file}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 读取文件
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
logger.info(f"已读取config.py,文件大小: {len(content)} 字节")
|
||||||
|
|
||||||
|
# 查找当前版本
|
||||||
|
import re
|
||||||
|
pattern = r'APP_VERSION\s*=\s*["\'][\d.]+["\']'
|
||||||
|
match = re.search(pattern, content)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
old_version_line = match.group(0)
|
||||||
|
logger.info(f"找到当前版本行: {old_version_line}")
|
||||||
|
|
||||||
|
replacement = f'APP_VERSION = "{new_version}"'
|
||||||
|
new_content = re.sub(pattern, replacement, content)
|
||||||
|
|
||||||
|
logger.info(f"准备写入新版本: {replacement}")
|
||||||
|
|
||||||
|
# 写入文件
|
||||||
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
# 验证写入
|
||||||
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||||
|
verify_content = f.read()
|
||||||
|
verify_match = re.search(pattern, verify_content)
|
||||||
|
if verify_match:
|
||||||
|
logger.info(f"✅ 写入验证成功: {verify_match.group(0)}")
|
||||||
|
|
||||||
|
logger.info(f"✅ 已更新 config.py: APP_VERSION = \"{new_version}\"")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"❌ 未找到APP_VERSION配置项,pattern: {pattern}")
|
||||||
|
logger.info(f"文件内容预览(前200字符): {content[:200]}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ 更新config.py失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
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. 创建版本记录
|
||||||
|
# 生成下载地址(KS3对象存储 - 广州节点,使用三级域名格式)
|
||||||
|
installer_filename = f"ShuiDi_AI_Assistant_Setup_v{next_version}.exe"
|
||||||
|
download_url = f"https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/{installer_filename}"
|
||||||
|
# 完整示例: https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.0.exe
|
||||||
|
|
||||||
|
version_record = {
|
||||||
|
'version': 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'),
|
||||||
|
'download_url': download_url, # 新增:下载地址
|
||||||
|
'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"🔗 下载地址: {download_url}")
|
||||||
|
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()
|
||||||
182
.gitea/scripts/upload_installer_to_ks3.py
Normal file
182
.gitea/scripts/upload_installer_to_ks3.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Upload GUI installer to KS3 object storage
|
||||||
|
- Target directory: installers/
|
||||||
|
- Set proper HTTP headers for browser download
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 🔥 强制禁用代理,避免代理连接问题
|
||||||
|
# 必须在所有网络操作之前设置
|
||||||
|
os.environ['NO_PROXY'] = '*'
|
||||||
|
os.environ['HTTP_PROXY'] = ''
|
||||||
|
os.environ['HTTPS_PROXY'] = ''
|
||||||
|
os.environ['http_proxy'] = ''
|
||||||
|
os.environ['https_proxy'] = ''
|
||||||
|
os.environ['ALL_PROXY'] = ''
|
||||||
|
os.environ['all_proxy'] = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ks3.connection import Connection
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: ks3sdk not installed. Please run: pip install ks3sdk")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# KS3 Configuration (using actual project config)
|
||||||
|
KS3_ACCESS_KEY = 'AKLT0Ey7Nq7ZSXykki0X0RGG'
|
||||||
|
KS3_SECRET_KEY = 'OONxkt9wwJa1FIco21vCbirs1HB6AGzDWdRyV0k2'
|
||||||
|
KS3_ENDPOINT = 'ks3-cn-guangzhou.ksyuncs.com' # Guangzhou region
|
||||||
|
KS3_BUCKET = 'shuidrop-chat-server'
|
||||||
|
KS3_IS_SECURE = True # Use HTTPS
|
||||||
|
KS3_PREFIX = 'installers/'
|
||||||
|
|
||||||
|
|
||||||
|
def get_ks3_connection():
|
||||||
|
"""Get KS3 connection"""
|
||||||
|
try:
|
||||||
|
connection = Connection(
|
||||||
|
KS3_ACCESS_KEY,
|
||||||
|
KS3_SECRET_KEY,
|
||||||
|
host=KS3_ENDPOINT,
|
||||||
|
is_secure=KS3_IS_SECURE,
|
||||||
|
timeout=1800, # Increase timeout to 30 minutes for large files
|
||||||
|
)
|
||||||
|
logger.info(f"KS3 connection established: {KS3_ENDPOINT}")
|
||||||
|
return connection
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"KS3 connection failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_latest_installer():
|
||||||
|
"""Find the latest generated installer"""
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
installer_dir = project_root / 'installer' / 'output'
|
||||||
|
|
||||||
|
if not installer_dir.exists():
|
||||||
|
logger.error(f"Installer directory not found: {installer_dir}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
installers = list(installer_dir.glob('*.exe'))
|
||||||
|
if not installers:
|
||||||
|
logger.error(f"No installer files found in: {installer_dir}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
latest_installer = max(installers, key=lambda p: p.stat().st_mtime)
|
||||||
|
file_size_mb = latest_installer.stat().st_size / 1024 / 1024
|
||||||
|
|
||||||
|
logger.info(f"Found installer: {latest_installer.name}")
|
||||||
|
logger.info(f"File size: {file_size_mb:.2f} MB")
|
||||||
|
|
||||||
|
return latest_installer
|
||||||
|
|
||||||
|
|
||||||
|
def progress_callback(uploaded_bytes, total_bytes):
|
||||||
|
"""Upload progress callback"""
|
||||||
|
if total_bytes > 0:
|
||||||
|
percentage = (uploaded_bytes / total_bytes) * 100
|
||||||
|
uploaded_mb = uploaded_bytes / 1024 / 1024
|
||||||
|
total_mb = total_bytes / 1024 / 1024
|
||||||
|
logger.info(f"Upload progress: {percentage:.1f}% ({uploaded_mb:.1f}/{total_mb:.1f} MB)")
|
||||||
|
|
||||||
|
|
||||||
|
def upload_installer(connection, installer_path):
|
||||||
|
"""Upload installer to KS3 with progress tracking"""
|
||||||
|
try:
|
||||||
|
bucket = connection.get_bucket(KS3_BUCKET)
|
||||||
|
ks3_key = f"{KS3_PREFIX}{installer_path.name}"
|
||||||
|
|
||||||
|
file_size = installer_path.stat().st_size
|
||||||
|
file_size_mb = file_size / 1024 / 1024
|
||||||
|
|
||||||
|
logger.info(f"Starting upload to KS3...")
|
||||||
|
logger.info(f"Target path: {ks3_key}")
|
||||||
|
logger.info(f"File size: {file_size_mb:.2f} MB")
|
||||||
|
logger.info(f"Estimated time: ~{int(file_size_mb / 2)} seconds (assuming 2MB/s)")
|
||||||
|
logger.info("Please wait, this may take several minutes...")
|
||||||
|
|
||||||
|
key = bucket.new_key(ks3_key)
|
||||||
|
|
||||||
|
# Upload file with public read permission and progress tracking
|
||||||
|
# Use cb parameter for progress callback (called every 5% or every 10MB)
|
||||||
|
key.set_contents_from_filename(
|
||||||
|
str(installer_path),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Disposition': f'attachment; filename="{installer_path.name}"',
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
'x-kss-storage-class': 'STANDARD',
|
||||||
|
'x-kss-acl': 'public-read' # Set public read permission in headers
|
||||||
|
},
|
||||||
|
policy='public-read', # Set ACL policy
|
||||||
|
cb=progress_callback, # Progress callback function
|
||||||
|
num_cb=10 # Call callback 10 times during upload (every 10%)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate download URL (using KS3 third-level domain format)
|
||||||
|
# Format: https://{bucket}.{endpoint}/{key}
|
||||||
|
protocol = 'https' if KS3_IS_SECURE else 'http'
|
||||||
|
download_url = f"{protocol}://{KS3_BUCKET}.{KS3_ENDPOINT}/{ks3_key}"
|
||||||
|
|
||||||
|
logger.info("")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("Upload successful!")
|
||||||
|
logger.info(f"Download URL: {download_url}")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
return download_url
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("")
|
||||||
|
logger.error("=" * 70)
|
||||||
|
logger.error(f"Upload failed: {e}")
|
||||||
|
logger.error("=" * 70)
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("Starting GUI installer upload to KS3")
|
||||||
|
logger.info(f"KS3 Endpoint: {KS3_ENDPOINT}")
|
||||||
|
logger.info(f"Bucket: {KS3_BUCKET}")
|
||||||
|
logger.info(f"Target directory: {KS3_PREFIX}")
|
||||||
|
logger.info("Proxy disabled: Direct connection to KS3")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
installer_path = find_latest_installer()
|
||||||
|
if not installer_path:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
connection = get_ks3_connection()
|
||||||
|
if not connection:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
download_url = upload_installer(connection, installer_path)
|
||||||
|
if not download_url:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
logger.info("=" * 70)
|
||||||
|
logger.info("Task completed successfully!")
|
||||||
|
logger.info(f"Installer: {installer_path.name}")
|
||||||
|
logger.info(f"Download URL: {download_url}")
|
||||||
|
logger.info("=" * 70)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
165
.gitea/scripts/view_version_history.py
Normal file
165
.gitea/scripts/view_version_history.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
版本历史查看工具
|
||||||
|
用于查看和管理GUI客户端的版本历史记录
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
683
.gitea/workflows/gui-version-release.yml
Normal file
683
.gitea/workflows/gui-version-release.yml
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
name: GUI Version Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, develop ] # Trigger on master and develop branches
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gui-version-release:
|
||||||
|
runs-on: windows
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: E:\shuidrop_gui
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: Pull latest code
|
||||||
|
- name: Pull latest code
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Pulling latest code";
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Working directory: $(Get-Location)";
|
||||||
|
Write-Host "Branch: ${{ github.ref_name }}";
|
||||||
|
Write-Host "Commit: ${{ github.sha }}";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
git config core.autocrlf false;
|
||||||
|
git config core.longpaths true;
|
||||||
|
|
||||||
|
Write-Host "Fetching from origin...";
|
||||||
|
git fetch origin;
|
||||||
|
|
||||||
|
Write-Host "Checking out branch: ${{ github.ref_name }}";
|
||||||
|
git checkout ${{ github.ref_name }};
|
||||||
|
|
||||||
|
Write-Host "Pulling latest changes...";
|
||||||
|
git reset --hard origin/${{ github.ref_name }};
|
||||||
|
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Current status:";
|
||||||
|
git log -1 --oneline;
|
||||||
|
Write-Host "==========================================";
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
pip install py-mini-racer
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "WARNING: Failed to install py-mini-racer (DY platform may not work)"
|
||||||
|
} else {
|
||||||
|
Write-Host "OK: py-mini-racer installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Dependencies installed successfully"
|
||||||
|
|
||||||
|
# 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 4.5: Build production executable
|
||||||
|
- name: Build production executable
|
||||||
|
if: success()
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
PYTHONIOENCODING: utf-8
|
||||||
|
run: |
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Step 4.5: Build production executable";
|
||||||
|
Write-Host "==========================================";
|
||||||
|
|
||||||
|
# Check and install dependencies only if needed
|
||||||
|
Write-Host "Checking dependencies...";
|
||||||
|
|
||||||
|
# Check PyInstaller
|
||||||
|
$pyinstallerInstalled = python -c "try: import PyInstaller; print('installed')`nexcept: print('not-installed')" 2>$null;
|
||||||
|
if ($pyinstallerInstalled -ne "installed") {
|
||||||
|
Write-Host "Installing PyInstaller...";
|
||||||
|
python -m pip install pyinstaller --quiet;
|
||||||
|
} else {
|
||||||
|
Write-Host "PyInstaller: already installed";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Pillow
|
||||||
|
$pillowInstalled = python -c "try: from PIL import Image; print('installed')`nexcept: print('not-installed')" 2>$null;
|
||||||
|
if ($pillowInstalled -ne "installed") {
|
||||||
|
Write-Host "Installing Pillow...";
|
||||||
|
python -m pip install Pillow --quiet;
|
||||||
|
} else {
|
||||||
|
Write-Host "Pillow: already installed";
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Environment ready:";
|
||||||
|
Write-Host " Python:" (python --version);
|
||||||
|
Write-Host " PyInstaller:" (python -m PyInstaller --version);
|
||||||
|
Write-Host " Pillow:" (python -c "from PIL import Image; print(Image.__version__)");
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
python build_production.py;
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Build failed";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Production build completed successfully";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Step 4.6: Check or Install NSIS
|
||||||
|
- name: Check or Install NSIS
|
||||||
|
if: success()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Step 4.6: Check or Install NSIS";
|
||||||
|
Write-Host "==========================================";
|
||||||
|
|
||||||
|
# Step 1: Check if NSIS is already installed
|
||||||
|
Write-Host "Checking if NSIS is already installed...";
|
||||||
|
|
||||||
|
$nsisPaths = @(
|
||||||
|
"C:\Program Files (x86)\NSIS",
|
||||||
|
"C:\Program Files\NSIS",
|
||||||
|
"C:\Tools\NSIS",
|
||||||
|
"$env:ProgramFiles\NSIS",
|
||||||
|
"$env:ProgramFiles(x86)\NSIS"
|
||||||
|
);
|
||||||
|
|
||||||
|
$nsisFound = $false;
|
||||||
|
$nsisPath = "";
|
||||||
|
|
||||||
|
foreach ($path in $nsisPaths) {
|
||||||
|
if (Test-Path $path) {
|
||||||
|
$makensisPath = Join-Path $path "makensis.exe";
|
||||||
|
if (Test-Path $makensisPath) {
|
||||||
|
$nsisPath = $path;
|
||||||
|
$nsisFound = $true;
|
||||||
|
Write-Host "Found NSIS at: $nsisPath";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Also check if makensis is in PATH
|
||||||
|
if (-not $nsisFound) {
|
||||||
|
$makensisInPath = Get-Command makensis -ErrorAction SilentlyContinue;
|
||||||
|
if ($makensisInPath) {
|
||||||
|
$nsisPath = Split-Path $makensisInPath.Source;
|
||||||
|
$nsisFound = $true;
|
||||||
|
Write-Host "Found makensis in PATH: $nsisPath";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nsisFound) {
|
||||||
|
$env:Path = "$nsisPath;$env:Path";
|
||||||
|
Write-Host "Using existing NSIS installation";
|
||||||
|
& makensis /VERSION;
|
||||||
|
Write-Host "";
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "NSIS not found, attempting installation...";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Step 2: Try Chocolatey
|
||||||
|
Write-Host "Method 1: Trying Chocolatey...";
|
||||||
|
$chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue;
|
||||||
|
|
||||||
|
if ($chocoInstalled) {
|
||||||
|
try {
|
||||||
|
choco install nsis -y --no-progress --limit-output;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Start-Sleep -Seconds 3;
|
||||||
|
|
||||||
|
# Refresh PATH
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User");
|
||||||
|
|
||||||
|
$nsisPath = "C:\Program Files (x86)\NSIS";
|
||||||
|
if (-not (Test-Path $nsisPath)) {
|
||||||
|
$nsisPath = "C:\Program Files\NSIS";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path (Join-Path $nsisPath "makensis.exe")) {
|
||||||
|
$env:Path = "$nsisPath;$env:Path";
|
||||||
|
& makensis /VERSION;
|
||||||
|
Write-Host "NSIS installed via Chocolatey";
|
||||||
|
Write-Host "";
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "Chocolatey installation failed: $_";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "Chocolatey not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Try winget (Windows Package Manager)
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Method 2: Trying winget...";
|
||||||
|
$wingetInstalled = Get-Command winget -ErrorAction SilentlyContinue;
|
||||||
|
|
||||||
|
if ($wingetInstalled) {
|
||||||
|
try {
|
||||||
|
winget install --id=NSIS.NSIS -e --silent --accept-package-agreements --accept-source-agreements;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Start-Sleep -Seconds 3;
|
||||||
|
|
||||||
|
$nsisPath = "C:\Program Files (x86)\NSIS";
|
||||||
|
if (-not (Test-Path $nsisPath)) {
|
||||||
|
$nsisPath = "C:\Program Files\NSIS";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path (Join-Path $nsisPath "makensis.exe")) {
|
||||||
|
$env:Path = "$nsisPath;$env:Path";
|
||||||
|
& makensis /VERSION;
|
||||||
|
Write-Host "NSIS installed via winget";
|
||||||
|
Write-Host "";
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "winget installation failed: $_";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "winget not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 4: Manual installation is not possible due to network restrictions
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "========================================";
|
||||||
|
Write-Host "ERROR: NSIS is not installed and automatic installation failed";
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Please install NSIS manually on the CI/CD runner:";
|
||||||
|
Write-Host "1. Download from: https://nsis.sourceforge.io/Download";
|
||||||
|
Write-Host "2. Install to: C:\Program Files (x86)\NSIS";
|
||||||
|
Write-Host "3. Or use: choco install nsis";
|
||||||
|
Write-Host "4. Or use: winget install NSIS.NSIS";
|
||||||
|
Write-Host "========================================";
|
||||||
|
exit 1;
|
||||||
|
|
||||||
|
# Step 4.7: Build NSIS installer
|
||||||
|
- name: Build NSIS installer
|
||||||
|
if: success()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Step 4.7: Build NSIS installer";
|
||||||
|
Write-Host "==========================================";
|
||||||
|
|
||||||
|
# Ensure NSIS is in PATH
|
||||||
|
$nsisPath = "C:\Program Files (x86)\NSIS";
|
||||||
|
if (-not (Test-Path $nsisPath)) {
|
||||||
|
$nsisPath = "C:\Program Files\NSIS";
|
||||||
|
}
|
||||||
|
$env:Path = "$nsisPath;$env:Path";
|
||||||
|
Write-Host "Using NSIS from: $nsisPath";
|
||||||
|
|
||||||
|
cd installer;
|
||||||
|
python build_installer.py;
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Installer build failed";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$installers = Get-ChildItem -Path "output" -Filter "*.exe" -ErrorAction SilentlyContinue;
|
||||||
|
if (-not $installers -or $installers.Count -eq 0) {
|
||||||
|
Write-Host "No installer file found";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$installerName = $installers[0].Name;
|
||||||
|
$installerSize = [math]::Round($installers[0].Length / 1MB, 2);
|
||||||
|
|
||||||
|
Write-Host "Installer built successfully";
|
||||||
|
Write-Host " Filename: $installerName";
|
||||||
|
Write-Host " Size: $installerSize MB";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Step 4.8: Upload installer to KS3
|
||||||
|
- name: Upload installer to KS3
|
||||||
|
if: success()
|
||||||
|
shell: powershell
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: |
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "Step 4.8: Upload installer to KS3";
|
||||||
|
Write-Host "==========================================";
|
||||||
|
Write-Host "NOTE: Large file upload may take 5-10 minutes";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Install KS3 SDK
|
||||||
|
Write-Host "Installing KS3 SDK...";
|
||||||
|
python -m pip install ks3sdk --quiet;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "ERROR: Failed to install ks3sdk";
|
||||||
|
Write-Host "Skipping KS3 upload (version release continues)";
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Starting upload process...";
|
||||||
|
Write-Host "This may take several minutes, please be patient...";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Run upload script
|
||||||
|
python .gitea/scripts/upload_installer_to_ks3.py;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "OK: Installer uploaded to KS3 successfully";
|
||||||
|
} else {
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "WARNING: KS3 upload failed (exit code: $LASTEXITCODE)";
|
||||||
|
Write-Host "NOTICE: Version release continues";
|
||||||
|
Write-Host "NOTICE: You can manually upload the installer later";
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Manual upload steps:";
|
||||||
|
Write-Host " 1. Find installer in: installer/output/";
|
||||||
|
Write-Host " 2. Upload to KS3 bucket: shuidrop-chat-server";
|
||||||
|
Write-Host " 3. Target path: installers/";
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Step 5: Commit version changes
|
||||||
|
- name: Commit version changes
|
||||||
|
if: success()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
Write-Host "========================================";
|
||||||
|
Write-Host "Step 5: Commit version changes";
|
||||||
|
Write-Host "========================================";
|
||||||
|
|
||||||
|
# Read new version number
|
||||||
|
$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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure Git
|
||||||
|
git config user.name "Gitea Actions Bot";
|
||||||
|
git config user.email "bot@gitea.local";
|
||||||
|
|
||||||
|
# Ensure we are on the correct branch (not detached HEAD)
|
||||||
|
$BRANCH = "${{ github.ref_name }}";
|
||||||
|
$currentBranch = git rev-parse --abbrev-ref HEAD;
|
||||||
|
Write-Host "Current branch: $currentBranch";
|
||||||
|
Write-Host "Target branch: $BRANCH";
|
||||||
|
|
||||||
|
if ($currentBranch -ne $BRANCH) {
|
||||||
|
Write-Host "WARNING: Not on target branch, checking out...";
|
||||||
|
git checkout $BRANCH;
|
||||||
|
Write-Host "OK: Checked out to $BRANCH";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up any existing rebase state before starting
|
||||||
|
$rebaseExists = (Test-Path ".git/rebase-merge") -or (Test-Path ".git/rebase-apply");
|
||||||
|
if ($rebaseExists) {
|
||||||
|
Write-Host "WARNING: Found existing rebase state, cleaning up...";
|
||||||
|
git rebase --abort 2>$null;
|
||||||
|
Remove-Item -Path ".git/rebase-merge" -Recurse -Force -ErrorAction SilentlyContinue;
|
||||||
|
Remove-Item -Path ".git/rebase-apply" -Recurse -Force -ErrorAction SilentlyContinue;
|
||||||
|
Write-Host "OK: Rebase state cleaned";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for changes
|
||||||
|
git add config.py version_history.json;
|
||||||
|
$hasChanges = git diff --staged --quiet;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Detected changes in version files";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# KEY CHANGE: Pull first, then commit
|
||||||
|
Write-Host "Step 5.1: Pulling latest changes first...";
|
||||||
|
git fetch origin $BRANCH;
|
||||||
|
|
||||||
|
# Check if remote has updates
|
||||||
|
$LOCAL = git rev-parse HEAD;
|
||||||
|
$REMOTE = git rev-parse origin/$BRANCH;
|
||||||
|
|
||||||
|
if ($LOCAL -ne $REMOTE) {
|
||||||
|
Write-Host "Remote has new commits, need to merge...";
|
||||||
|
Write-Host "Local: $LOCAL";
|
||||||
|
Write-Host "Remote: $REMOTE";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# FIX: Stash local changes before pull to avoid conflicts
|
||||||
|
Write-Host "Stashing local version changes...";
|
||||||
|
git stash push -m "CI-CD-temp-stash" config.py version_history.json;
|
||||||
|
|
||||||
|
Write-Host "Pulling remote changes...";
|
||||||
|
# Pull remote changes (use merge strategy to avoid rebase conflicts)
|
||||||
|
git pull origin $BRANCH --no-rebase;
|
||||||
|
|
||||||
|
# Pop stashed changes back
|
||||||
|
Write-Host "Restoring version changes...";
|
||||||
|
$stashPopResult = git stash pop 2>&1;
|
||||||
|
$stashPopExitCode = $LASTEXITCODE;
|
||||||
|
|
||||||
|
if ($stashPopExitCode -ne 0) {
|
||||||
|
Write-Host "WARNING: Stash pop encountered conflicts, resolving...";
|
||||||
|
Write-Host "$stashPopResult";
|
||||||
|
|
||||||
|
# Check conflict files
|
||||||
|
$conflicts = git diff --name-only --diff-filter=U;
|
||||||
|
Write-Host "Conflict files: $conflicts";
|
||||||
|
|
||||||
|
# FIX: Smart conflict resolution to preserve user code
|
||||||
|
# For config.py: use remote version (user code), then re-apply version update
|
||||||
|
if ($conflicts -match "config.py") {
|
||||||
|
Write-Host "Resolving config.py conflict...";
|
||||||
|
# Use remote version (preserve user code)
|
||||||
|
git checkout --theirs config.py;
|
||||||
|
# Re-apply version update only
|
||||||
|
python .gitea/scripts/gui_version_creator.py;
|
||||||
|
git add config.py;
|
||||||
|
Write-Host "OK: Resolved config.py (preserved user code + updated version)";
|
||||||
|
}
|
||||||
|
|
||||||
|
# For version_history.json: use CI/CD version (append record)
|
||||||
|
if ($conflicts -match "version_history.json") {
|
||||||
|
Write-Host "Resolving version_history.json conflict...";
|
||||||
|
git checkout --ours version_history.json;
|
||||||
|
git add version_history.json;
|
||||||
|
Write-Host "OK: Resolved version_history.json (using CI/CD version)";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drop the stash after resolving
|
||||||
|
git stash drop 2>$null;
|
||||||
|
} else {
|
||||||
|
Write-Host "OK: Stash pop successful";
|
||||||
|
|
||||||
|
# FIX: After stash pop, files are automatically merged
|
||||||
|
# config.py now contains:
|
||||||
|
# 1. User's code from remote (from pull)
|
||||||
|
# 2. Version update from stash (from gui_version_creator.py)
|
||||||
|
|
||||||
|
Write-Host "Checking if version update is preserved...";
|
||||||
|
|
||||||
|
# Verify version number is correct
|
||||||
|
$configContent = Get-Content "config.py" -Raw;
|
||||||
|
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
|
||||||
|
$currentVersion = $matches[1];
|
||||||
|
Write-Host "Current APP_VERSION in config.py: $currentVersion";
|
||||||
|
Write-Host "Expected version: $VERSION";
|
||||||
|
|
||||||
|
if ($currentVersion -ne $VERSION) {
|
||||||
|
Write-Host "WARNING: Version mismatch, re-applying version update...";
|
||||||
|
# Re-execute version update (only modify APP_VERSION line)
|
||||||
|
python .gitea/scripts/gui_version_creator.py;
|
||||||
|
} else {
|
||||||
|
Write-Host "OK: Version is correct";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add current files (includes user code + version update)
|
||||||
|
git add config.py version_history.json;
|
||||||
|
|
||||||
|
Write-Host "Files staged successfully";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "No remote changes, proceeding with commit...";
|
||||||
|
|
||||||
|
# FIX: Ensure files are staged even if no remote changes
|
||||||
|
git add config.py version_history.json;
|
||||||
|
Write-Host "Files staged for commit";
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Step 5.2: Committing version changes...";
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
$hasUncommitted = git diff --quiet; $diffExitCode1 = $LASTEXITCODE;
|
||||||
|
$hasStagedChanges = git diff --staged --quiet; $diffExitCode2 = $LASTEXITCODE;
|
||||||
|
|
||||||
|
if (($diffExitCode1 -ne 0) -or ($diffExitCode2 -ne 0)) {
|
||||||
|
Write-Host "Detected uncommitted changes, creating commit...";
|
||||||
|
git commit -m "[skip ci] Update version to v$VERSION" --no-verify;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "OK: Commit successful";
|
||||||
|
} else {
|
||||||
|
Write-Host "ERROR: Commit failed";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "No changes to commit (already committed in merge)";
|
||||||
|
Write-Host "Skipping commit step";
|
||||||
|
$LASTEXITCODE = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "";
|
||||||
|
Write-Host "Step 5.3: Pushing to remote...";
|
||||||
|
|
||||||
|
# Push to remote (retry up to 3 times with smart strategy)
|
||||||
|
$pushSuccess = $false;
|
||||||
|
for ($i = 1; $i -le 3; $i++) {
|
||||||
|
Write-Host "Push attempt $i/3...";
|
||||||
|
|
||||||
|
# Before each attempt, ensure clean state
|
||||||
|
if ($i -gt 1) {
|
||||||
|
Write-Host "Preparing for retry $i...";
|
||||||
|
|
||||||
|
# Step 1: Clean up any rebase state
|
||||||
|
$rebaseCheck = (Test-Path ".git/rebase-merge") -or (Test-Path ".git/rebase-apply");
|
||||||
|
if ($rebaseCheck) {
|
||||||
|
Write-Host "Cleaning up rebase state...";
|
||||||
|
git rebase --abort 2>$null;
|
||||||
|
Remove-Item -Path ".git/rebase-merge" -Recurse -Force -ErrorAction SilentlyContinue;
|
||||||
|
Remove-Item -Path ".git/rebase-apply" -Recurse -Force -ErrorAction SilentlyContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 2: Ensure on correct branch
|
||||||
|
$currentBranchCheck = git rev-parse --abbrev-ref HEAD;
|
||||||
|
if ($currentBranchCheck -ne $BRANCH) {
|
||||||
|
Write-Host "Checking out to branch $BRANCH...";
|
||||||
|
git checkout $BRANCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 3: Fetch latest remote state
|
||||||
|
Write-Host "Fetching latest remote state...";
|
||||||
|
git fetch origin $BRANCH;
|
||||||
|
|
||||||
|
# Step 4: Record current version number
|
||||||
|
$currentVersionMatch = (Get-Content "config.py" -Raw) -match 'APP_VERSION\s*=\s*"([\d.]+)"';
|
||||||
|
$targetVersion = $matches[1];
|
||||||
|
Write-Host "Target version to apply: $targetVersion";
|
||||||
|
|
||||||
|
# Step 5: Reset to remote state
|
||||||
|
Write-Host "Resetting to remote state...";
|
||||||
|
git reset --hard origin/$BRANCH;
|
||||||
|
|
||||||
|
# Step 6: FIX - Re-apply version update only, do not overwrite entire file
|
||||||
|
Write-Host "Re-applying version update only...";
|
||||||
|
python .gitea/scripts/gui_version_creator.py;
|
||||||
|
|
||||||
|
# Step 7: Stage and commit again
|
||||||
|
git add config.py version_history.json;
|
||||||
|
git commit -m "[skip ci] Update version to v$VERSION" --no-verify;
|
||||||
|
|
||||||
|
Write-Host "Retry preparation complete";
|
||||||
|
Start-Sleep -Seconds 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Attempt to push
|
||||||
|
git push origin $BRANCH;
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "OK: Push successful on attempt $i";
|
||||||
|
$pushSuccess = $true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
Write-Host "WARNING: Push attempt $i failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $pushSuccess) {
|
||||||
|
Write-Host "ERROR: Push failed after 3 attempts";
|
||||||
|
Write-Host "NOTICE: This is not critical - version is already in database and KS3";
|
||||||
|
Write-Host "NOTICE: Git repository sync can be done manually later";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "ERROR: Commit failed";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host "No changes to commit";
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "========================================";
|
||||||
|
Write-Host "";
|
||||||
|
|
||||||
|
# Step 6: Display summary
|
||||||
|
- name: Display summary
|
||||||
|
if: always()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$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 "==========================================";
|
||||||
|
|
||||||
|
|
||||||
1250
Utils/Dy/DyUtils.py
1250
Utils/Dy/DyUtils.py
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import uuid
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
# 发送消息
|
# 发送文本消息
|
||||||
def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str):
|
def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str):
|
||||||
"""
|
"""
|
||||||
构造发送消息消息体
|
构造发送消息消息体
|
||||||
@@ -78,7 +78,7 @@ def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, t
|
|||||||
'15': [
|
'15': [
|
||||||
{'1': b'pigeon_source', '2': b'web'},
|
{'1': b'pigeon_source', '2': b'web'},
|
||||||
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
|
{'1': b'pigeon_sign', '2': pigeon_sign.encode()}, # 🔥 修复:使用动态参数,不是硬编码
|
||||||
{'1': b'session_aid', '2': b'1383'},
|
{'1': b'session_aid', '2': b'1383'},
|
||||||
{'1': b'session_did', '2': session_did.encode()},
|
{'1': b'session_did', '2': session_did.encode()},
|
||||||
{'1': b'app_name', '2': b'im'},
|
{'1': b'app_name', '2': b'im'},
|
||||||
@@ -373,3 +373,229 @@ def heartbeat_message(pigeon_sign: str, token: str, session_did: str):
|
|||||||
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
|
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
|
||||||
return value, message_type
|
return value, message_type
|
||||||
|
|
||||||
|
|
||||||
|
# 🔥 新增:发送图片消息
|
||||||
|
def send_img(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, img: str, image_width: str = "2000", image_height: str = "1125", image_format: str = "png", image_size: str = "3157512"):
|
||||||
|
"""
|
||||||
|
构造发送图片消息体
|
||||||
|
:param image_width: 图片宽度
|
||||||
|
:param image_size: 图片大小
|
||||||
|
:param image_height: 图片高度
|
||||||
|
:param image_format: 图片格式
|
||||||
|
:param pigeon_sign: 接口返回
|
||||||
|
:param token: 接口返回
|
||||||
|
:param receiver_id: wss消息返回 对方用户id
|
||||||
|
:param shop_id: cookie自带
|
||||||
|
:param talk_id: wss消息返回 激活窗口id
|
||||||
|
:param session_did: cookie自带
|
||||||
|
:param p_id: wss消息返回
|
||||||
|
:param user_code: 用户token
|
||||||
|
:param img: 图片URI或URL
|
||||||
|
:return: (value, message_type)
|
||||||
|
"""
|
||||||
|
value = {
|
||||||
|
'1': 11778,
|
||||||
|
'2': int(time.time() * 1000),
|
||||||
|
'3': 10001,
|
||||||
|
'4': 1,
|
||||||
|
'5': [
|
||||||
|
{'1': b'pigeon_source', '2': b'web'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
|
||||||
|
],
|
||||||
|
'7': {'14': 98},
|
||||||
|
'8': {
|
||||||
|
'1': 100,
|
||||||
|
'2': 11778,
|
||||||
|
'3': b'1.0.4-beta.2',
|
||||||
|
'4': token.encode(),
|
||||||
|
'5': 3,
|
||||||
|
'6': 3,
|
||||||
|
'7': b'2d97ea6:feat/add_init_callback',
|
||||||
|
'8': {
|
||||||
|
'100': {
|
||||||
|
'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(),
|
||||||
|
'2': 11,
|
||||||
|
'3': p_id,
|
||||||
|
'4': "[图片]".encode(),
|
||||||
|
'5': [
|
||||||
|
{'1': b'type', '2': b'file_image'},
|
||||||
|
{'1': b'shop_id', '2': shop_id.encode()},
|
||||||
|
{'1': b'sender_role', '2': b'2'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'src', '2': b'pc'},
|
||||||
|
{'1': b'srcType', '2': b'1'},
|
||||||
|
{'1': b'source', '2': b'pc-web'},
|
||||||
|
{'1': b'receiver_id', '2': str(receiver_id).encode()},
|
||||||
|
{'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'},
|
||||||
|
{'1': b'tag_valid', '2': b'1'},
|
||||||
|
{'1': b'imageUrl', '2': img.encode()},
|
||||||
|
{'1': b'imageWidth', '2': image_width.encode()},
|
||||||
|
{'1': b'imageHeight', '2': image_height.encode()},
|
||||||
|
{'1': b'imageFormat', '2': image_format.encode()},
|
||||||
|
{'1': b'imageSize', '2': image_size.encode()},
|
||||||
|
{'1': b'uuid', '2': str(uuid.uuid4()).encode()},
|
||||||
|
{'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()},
|
||||||
|
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'sender_id', '2': b''},
|
||||||
|
{'1': b'biz_ext', '2': b'{}'},
|
||||||
|
{'1': b'p:from_source', '2': b'web'},
|
||||||
|
{'1': b's:mentioned_users', '2': b''},
|
||||||
|
{'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()}
|
||||||
|
],
|
||||||
|
'6': 1000,
|
||||||
|
'7': user_code.encode(),
|
||||||
|
'8': str(uuid.uuid4()).encode(),
|
||||||
|
'14': talk_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'9': session_did.encode(),
|
||||||
|
'11': b'web',
|
||||||
|
'15': [
|
||||||
|
{'1': b'pigeon_source', '2': b'web'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
|
||||||
|
{'1': b'session_aid', '2': b'1383'},
|
||||||
|
{'1': b'session_did', '2': session_did.encode()},
|
||||||
|
{'1': b'app_name', '2': b'im'},
|
||||||
|
{'1': b'priority_region', '2': b'cn'},
|
||||||
|
{'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'cookie_enabled', '2': b'true'},
|
||||||
|
{'1': b'browser_language', '2': b'zh-CN'},
|
||||||
|
{'1': b'browser_platform', '2': b'Win32'},
|
||||||
|
{'1': b'browser_name', '2': b'Mozilla'},
|
||||||
|
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'browser_online', '2': b'true'},
|
||||||
|
{'1': b'screen_width', '2': b'1707'},
|
||||||
|
{'1': b'screen_height', '2': b'1067'},
|
||||||
|
{'1': b'referer', '2': b''},
|
||||||
|
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
|
||||||
|
],
|
||||||
|
'18': 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
|
||||||
|
return value, message_type
|
||||||
|
|
||||||
|
|
||||||
|
# 🔥 新增:发送视频消息
|
||||||
|
def send_video(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, vid: str, cover_url: str, height: str, width: str, duration: str):
|
||||||
|
"""
|
||||||
|
构造发送视频消息体
|
||||||
|
:param duration: 视频时长
|
||||||
|
:param width: 视频宽度
|
||||||
|
:param height: 视频高度
|
||||||
|
:param cover_url: 封面URL
|
||||||
|
:param pigeon_sign: 接口返回
|
||||||
|
:param token: 接口返回
|
||||||
|
:param receiver_id: wss消息返回 对方用户id
|
||||||
|
:param shop_id: cookie自带
|
||||||
|
:param talk_id: wss消息返回 激活窗口id
|
||||||
|
:param session_did: cookie自带
|
||||||
|
:param p_id: wss消息返回
|
||||||
|
:param user_code: 用户token
|
||||||
|
:param vid: 视频id
|
||||||
|
:return: (value, message_type)
|
||||||
|
"""
|
||||||
|
# 🔥 修复:确保数值类型正确(height/width为int,duration为float)
|
||||||
|
try:
|
||||||
|
height_int = int(height) if isinstance(height, str) else height
|
||||||
|
width_int = int(width) if isinstance(width, str) else width
|
||||||
|
duration_float = float(duration) if isinstance(duration, str) else duration
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# 如果转换失败,使用默认值
|
||||||
|
height_int = 1080
|
||||||
|
width_int = 1920
|
||||||
|
duration_float = 0.0
|
||||||
|
|
||||||
|
msg_render_model = json.dumps({
|
||||||
|
"msg_render_type": "video",
|
||||||
|
"render_body": {
|
||||||
|
"vid": vid,
|
||||||
|
"coverURL": cover_url,
|
||||||
|
"height": height_int,
|
||||||
|
"width": width_int,
|
||||||
|
"duration": duration_float
|
||||||
|
}
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
value = {
|
||||||
|
'1': 10015,
|
||||||
|
'2': int(time.time() * 1000),
|
||||||
|
'3': 10001,
|
||||||
|
'4': 1,
|
||||||
|
'5': [
|
||||||
|
{'1': b'pigeon_source', '2': b'web'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
|
||||||
|
],
|
||||||
|
'7': {'14': 98},
|
||||||
|
'8': {
|
||||||
|
'1': 100,
|
||||||
|
'2': 10015,
|
||||||
|
'3': b'1.0.4-beta.2',
|
||||||
|
'4': token.encode(),
|
||||||
|
'5': 3,
|
||||||
|
'6': 3,
|
||||||
|
'7': b'2d97ea6:feat/add_init_callback',
|
||||||
|
'8': {
|
||||||
|
'100': {
|
||||||
|
'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(),
|
||||||
|
'2': 11,
|
||||||
|
'3': p_id,
|
||||||
|
'4': "[视频]".encode(),
|
||||||
|
'5': [
|
||||||
|
{'1': b'type', '2': b'video'},
|
||||||
|
{'1': b'shop_id', '2': shop_id.encode()},
|
||||||
|
{'1': b'sender_role', '2': b'2'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'src', '2': b'pc'},
|
||||||
|
{'1': b'srcType', '2': b'1'},
|
||||||
|
{'1': b'source', '2': b'pc-web'},
|
||||||
|
{'1': b'receiver_id', '2': str(receiver_id).encode()},
|
||||||
|
{'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'},
|
||||||
|
{'1': b'msg_render_model', '2': msg_render_model},
|
||||||
|
{'1': b'uuid', '2': str(uuid.uuid4()).encode()},
|
||||||
|
{'1': b'start_scene', '2': b'1'},
|
||||||
|
{'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()},
|
||||||
|
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'sender_id', '2': b''},
|
||||||
|
{'1': b'biz_ext', '2': b'{}'},
|
||||||
|
{'1': b'p:from_source', '2': b'web'},
|
||||||
|
{'1': b's:mentioned_users', '2': b''},
|
||||||
|
{'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()}
|
||||||
|
],
|
||||||
|
'6': 1000,
|
||||||
|
'7': user_code.encode(),
|
||||||
|
'8': str(uuid.uuid4()).encode(),
|
||||||
|
'14': talk_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'9': session_did.encode(),
|
||||||
|
'11': b'web',
|
||||||
|
'15': [
|
||||||
|
{'1': b'pigeon_source', '2': b'web'},
|
||||||
|
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
|
||||||
|
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
|
||||||
|
{'1': b'session_aid', '2': b'1383'},
|
||||||
|
{'1': b'session_did', '2': session_did.encode()},
|
||||||
|
{'1': b'app_name', '2': b'im'},
|
||||||
|
{'1': b'priority_region', '2': b'cn'},
|
||||||
|
{'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'cookie_enabled', '2': b'true'},
|
||||||
|
{'1': b'browser_language', '2': b'zh-CN'},
|
||||||
|
{'1': b'browser_platform', '2': b'Win32'},
|
||||||
|
{'1': b'browser_name', '2': b'Mozilla'},
|
||||||
|
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
|
||||||
|
{'1': b'browser_online', '2': b'true'},
|
||||||
|
{'1': b'screen_width', '2': b'1707'},
|
||||||
|
{'1': b'screen_height', '2': b'1067'},
|
||||||
|
{'1': b'referer', '2': b''},
|
||||||
|
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
|
||||||
|
],
|
||||||
|
'18': 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
|
||||||
|
return value, message_type
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import requests
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
# 定义持久化数据类
|
# 定义持久化数据类
|
||||||
@@ -154,6 +156,14 @@ class FixJdCookie:
|
|||||||
self.base_reconnect_delay = 1.0 # 基础重连延迟
|
self.base_reconnect_delay = 1.0 # 基础重连延迟
|
||||||
self.max_reconnect_delay = 60.0 # 最大重连延迟
|
self.max_reconnect_delay = 60.0 # 最大重连延迟
|
||||||
self.reconnect_backoff = 1.5 # 退避系数
|
self.reconnect_backoff = 1.5 # 退避系数
|
||||||
|
|
||||||
|
# 🔥 存储认证信息,用于文件上传
|
||||||
|
self.cookies_str = None
|
||||||
|
self.current_aid = None
|
||||||
|
self.current_pin_zj = None
|
||||||
|
|
||||||
|
# 🔥 启动时清理过期临时文件
|
||||||
|
self._cleanup_old_temp_files(max_age_hours=24)
|
||||||
|
|
||||||
def _log(self, message, log_type="INFO"):
|
def _log(self, message, log_type="INFO"):
|
||||||
"""内部日志方法"""
|
"""内部日志方法"""
|
||||||
@@ -177,6 +187,11 @@ class FixJdCookie:
|
|||||||
"""初始化 socket"""
|
"""初始化 socket"""
|
||||||
await self.send_heartbeat(ws, aid, pin_zj)
|
await self.send_heartbeat(ws, aid, pin_zj)
|
||||||
print("开始监听初始化")
|
print("开始监听初始化")
|
||||||
|
|
||||||
|
# 🔧 修复:生成唯一设备ID,避免多端互踢
|
||||||
|
import uuid
|
||||||
|
unique_device_id = f"shuidrop_gui_{uuid.uuid4().hex[:16]}"
|
||||||
|
|
||||||
auth = {
|
auth = {
|
||||||
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
||||||
"aid": aid,
|
"aid": aid,
|
||||||
@@ -185,8 +200,9 @@ class FixJdCookie:
|
|||||||
"type": "auth",
|
"type": "auth",
|
||||||
"body": {"presence": 1, "clientVersion": "2.6.3"},
|
"body": {"presence": 1, "clientVersion": "2.6.3"},
|
||||||
"to": {"app": "im.waiter"},
|
"to": {"app": "im.waiter"},
|
||||||
"from": {"app": "im.waiter", "pin": pin_zj, "clientType": "comet", "dvc": "device1234"}
|
"from": {"app": "im.waiter", "pin": pin_zj, "clientType": "comet", "dvc": unique_device_id}
|
||||||
}
|
}
|
||||||
|
print(f"[DEBUG] 使用唯一设备ID: {unique_device_id}")
|
||||||
await ws.send(json.dumps(auth))
|
await ws.send(json.dumps(auth))
|
||||||
|
|
||||||
|
|
||||||
@@ -210,6 +226,80 @@ class FixJdCookie:
|
|||||||
}
|
}
|
||||||
await ws.send(json.dumps(message))
|
await ws.send(json.dumps(message))
|
||||||
|
|
||||||
|
async def get_all_customer(self, ws, aid, pin_zj):
|
||||||
|
"""异步获取商家组织架构(客服列表)"""
|
||||||
|
id_str = hashlib.md5(str(int(time.time() * 1000) + 300).encode()).hexdigest()
|
||||||
|
message = {
|
||||||
|
"id": id_str,
|
||||||
|
"aid": aid,
|
||||||
|
"lang": "zh_CN",
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"from": {
|
||||||
|
"app": "im.waiter", "pin": pin_zj, "art": "customerGroupMsg", "clientType": "comet"
|
||||||
|
},
|
||||||
|
"type": "org_new",
|
||||||
|
"body": {"param": pin_zj, "paramtype": "ByPin"}
|
||||||
|
}
|
||||||
|
await ws.send(json.dumps(message))
|
||||||
|
while True:
|
||||||
|
response = await ws.recv()
|
||||||
|
data = json.loads(response)
|
||||||
|
if data.get("id") == id_str and data.get("body", {}).get("groupname") == "商家组织架构":
|
||||||
|
group = data.get("body", {}).get("group")
|
||||||
|
if group:
|
||||||
|
try:
|
||||||
|
# 与旧实现保持一致的数据路径
|
||||||
|
self.customer_list = group[0].get("group")[0].get("users")
|
||||||
|
except Exception:
|
||||||
|
self.customer_list = []
|
||||||
|
return
|
||||||
|
|
||||||
|
async def send_staff_list_to_backend(self, store_id):
|
||||||
|
"""发送客服列表到后端(通过统一后端连接)"""
|
||||||
|
try:
|
||||||
|
if not hasattr(self, 'customer_list') or not self.customer_list:
|
||||||
|
self._log("⚠️ 客服列表为空", "WARNING")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 转换客服数据格式
|
||||||
|
staff_infos = []
|
||||||
|
for staff in self.customer_list:
|
||||||
|
staff_info = {
|
||||||
|
"staff_id": staff.get("pin", ""),
|
||||||
|
"name": staff.get("nickname", ""),
|
||||||
|
"status": staff.get("status", 0),
|
||||||
|
"department": staff.get("department", ""),
|
||||||
|
"online": staff.get("online", True)
|
||||||
|
}
|
||||||
|
staff_infos.append(staff_info)
|
||||||
|
|
||||||
|
# 通过后端统一连接发送
|
||||||
|
try:
|
||||||
|
from WebSocket.backend_singleton import get_backend_client
|
||||||
|
backend_client = get_backend_client()
|
||||||
|
if not backend_client or not getattr(backend_client, 'is_connected', False):
|
||||||
|
self._log("❌ 统一后端连接不可用", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"type": "staff_list",
|
||||||
|
"content": "客服列表更新",
|
||||||
|
"data": {
|
||||||
|
"staff_list": staff_infos,
|
||||||
|
"total_count": len(staff_infos)
|
||||||
|
},
|
||||||
|
"store_id": str(store_id)
|
||||||
|
}
|
||||||
|
backend_client.send_message(message)
|
||||||
|
self._log(f"✅ 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 发送客服列表到后端失败: {e}", "ERROR")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 发送客服列表到后端异常: {e}", "ERROR")
|
||||||
|
return False
|
||||||
|
|
||||||
async def transfer_customer(self, ws, aid, pin, pin_zj, chat_name):
|
async def transfer_customer(self, ws, aid, pin, pin_zj, chat_name):
|
||||||
"""异步客服转接 在发送的消息为客服转接的关键词的时候"""
|
"""异步客服转接 在发送的消息为客服转接的关键词的时候"""
|
||||||
message = {
|
message = {
|
||||||
@@ -233,10 +323,476 @@ class FixJdCookie:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content):
|
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content, msg_type="text"):
|
||||||
"""异步发送单条消息"""
|
"""异步发送消息 - 支持文本/图片/视频
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
content: 消息内容(文本内容或URL)
|
||||||
|
msg_type: 消息类型 (text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
print('本地发送消息')
|
# 根据消息类型调用不同的发送方法
|
||||||
|
if msg_type == "image":
|
||||||
|
return await self.send_image_message(ws, pin, aid, pin_zj, vender_id, content)
|
||||||
|
elif msg_type == "video":
|
||||||
|
return await self.send_video_message(ws, pin, aid, pin_zj, vender_id, content)
|
||||||
|
else:
|
||||||
|
# 文本消息(默认)
|
||||||
|
print('本地发送文本消息')
|
||||||
|
message = {
|
||||||
|
"ver": "4.3",
|
||||||
|
"type": "chat_message",
|
||||||
|
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
||||||
|
"to": {"app": "im.customer", "pin": pin},
|
||||||
|
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
||||||
|
"lang": "zh_CN",
|
||||||
|
"aid": aid,
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"readFlag": 0,
|
||||||
|
"body": {
|
||||||
|
"content": content,
|
||||||
|
"translated": False,
|
||||||
|
"param": {"cusVenderId": vender_id},
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ws.send(json.dumps(message))
|
||||||
|
logger.info(f"消息已经发送到客户端[info] {pin}: {content[:20]} ...")
|
||||||
|
except websockets.ConnectionClosed:
|
||||||
|
logger.error('本地发送消息失败 连接关闭')
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# 同时这里也要及时进行raise抛出 这样比较好让系统可以看出 异常了可以抛出信息不至于后续被认为
|
||||||
|
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _get_temp_directory(self) -> str:
|
||||||
|
"""智能选择临时文件目录(优先级降级策略)
|
||||||
|
|
||||||
|
优先级:
|
||||||
|
1. 环境变量 SHUIDROP_TEMP_DIR(用户自定义)
|
||||||
|
2. 应用程序所在目录/temp(如果有写权限且不在Program Files)
|
||||||
|
3. 系统临时目录(兜底方案)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 临时目录路径
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 优先级1:用户自定义环境变量
|
||||||
|
custom_temp = os.getenv('SHUIDROP_TEMP_DIR')
|
||||||
|
if custom_temp:
|
||||||
|
custom_temp = os.path.join(custom_temp, "shuidrop_jd_temp_uploads")
|
||||||
|
try:
|
||||||
|
os.makedirs(custom_temp, exist_ok=True)
|
||||||
|
# 测试写权限
|
||||||
|
test_file = os.path.join(custom_temp, ".write_test")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
self._log(f"✅ [JD路径] 使用自定义临时目录: {custom_temp}", "INFO")
|
||||||
|
return custom_temp
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD路径] 自定义目录不可用: {e}", "WARNING")
|
||||||
|
|
||||||
|
# 优先级2:应用程序所在目录(避免占用C盘)
|
||||||
|
try:
|
||||||
|
# 获取可执行文件路径
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# 打包后
|
||||||
|
app_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
# 开发环境
|
||||||
|
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# 检查是否在 Program Files(需要管理员权限)
|
||||||
|
if 'Program Files' not in app_dir and 'Program Files (x86)' not in app_dir:
|
||||||
|
app_temp = os.path.join(app_dir, "temp_uploads", "jd")
|
||||||
|
os.makedirs(app_temp, exist_ok=True)
|
||||||
|
|
||||||
|
# 测试写权限
|
||||||
|
test_file = os.path.join(app_temp, ".write_test")
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
|
||||||
|
self._log(f"✅ [JD路径] 使用应用程序目录: {app_temp}", "INFO")
|
||||||
|
return app_temp
|
||||||
|
else:
|
||||||
|
self._log(f"ℹ️ [JD路径] 应用在Program Files,跳过应用目录", "DEBUG")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD路径] 应用目录不可用: {e}", "DEBUG")
|
||||||
|
|
||||||
|
# 优先级3:系统临时目录(兜底方案)
|
||||||
|
system_temp = os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
|
||||||
|
os.makedirs(system_temp, exist_ok=True)
|
||||||
|
self._log(f"✅ [JD路径] 使用系统临时目录: {system_temp}", "INFO")
|
||||||
|
return system_temp
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 最终兜底
|
||||||
|
self._log(f"❌ [JD路径] 所有路径策略失败,使用默认: {e}", "ERROR")
|
||||||
|
import tempfile
|
||||||
|
return os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
|
||||||
|
|
||||||
|
def _get_file_extension(self, url: str, default_ext: str) -> str:
|
||||||
|
"""智能提取文件扩展名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 文件URL
|
||||||
|
default_ext: 默认扩展名(jpg/mp4)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 文件扩展名
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 移除查询参数
|
||||||
|
url_without_params = url.split('?')[0]
|
||||||
|
|
||||||
|
# 检查是否有有效的文件扩展名
|
||||||
|
if '.' in url_without_params:
|
||||||
|
parts = url_without_params.split('.')
|
||||||
|
ext = parts[-1].lower()
|
||||||
|
|
||||||
|
# 验证扩展名是否合法(只包含字母数字)
|
||||||
|
if ext and len(ext) <= 5 and ext.isalnum():
|
||||||
|
# 常见图片/视频扩展名
|
||||||
|
valid_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv']
|
||||||
|
if ext in valid_exts:
|
||||||
|
return ext
|
||||||
|
|
||||||
|
# 如果无法提取有效扩展名,使用默认值
|
||||||
|
return default_ext
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return default_ext
|
||||||
|
|
||||||
|
def _cleanup_old_temp_files(self, max_age_hours=24):
|
||||||
|
"""清理过期的临时文件(24小时以上)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_age_hours: 文件最大保留时间(小时)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用智能路径选择
|
||||||
|
temp_dir = self._get_temp_directory()
|
||||||
|
if not os.path.exists(temp_dir):
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
max_age_seconds = max_age_hours * 3600
|
||||||
|
cleaned_count = 0
|
||||||
|
|
||||||
|
# 遍历临时目录中的文件
|
||||||
|
for filename in os.listdir(temp_dir):
|
||||||
|
file_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
# 只处理文件,跳过目录
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查文件年龄
|
||||||
|
file_mtime = os.path.getmtime(file_path)
|
||||||
|
file_age = now - file_mtime
|
||||||
|
|
||||||
|
if file_age > max_age_seconds:
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
os.remove(file_path)
|
||||||
|
cleaned_count += 1
|
||||||
|
self._log(f"🗑️ [JD清理] 删除过期文件: {filename} ({file_size} bytes, {file_age/3600:.1f}小时前)", "DEBUG")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD清理] 删除文件失败 {filename}: {e}", "DEBUG")
|
||||||
|
|
||||||
|
if cleaned_count > 0:
|
||||||
|
self._log(f"✅ [JD清理] 已清理 {cleaned_count} 个过期临时文件", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ [JD清理] 临时文件清理失败: {e}", "DEBUG")
|
||||||
|
|
||||||
|
async def download_file(self, url: str, save_dir: str = None, max_retries: int = 3) -> str:
|
||||||
|
"""下载外部文件到本地(带重试机制 + 智能路径选择)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 文件URL
|
||||||
|
save_dir: 保存目录(None时自动选择最佳路径)
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 本地文件路径
|
||||||
|
"""
|
||||||
|
# 使用智能路径选择策略
|
||||||
|
if save_dir is None:
|
||||||
|
save_dir = self._get_temp_directory()
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 智能提取文件扩展名
|
||||||
|
default_ext = 'jpg'
|
||||||
|
ext = self._get_file_extension(url, default_ext)
|
||||||
|
|
||||||
|
# 生成唯一文件名
|
||||||
|
file_name = f"jd_download_{uuid.uuid4().hex[:12]}.{ext}"
|
||||||
|
save_path = os.path.join(save_dir, file_name)
|
||||||
|
|
||||||
|
# 重试机制
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
if attempt > 0:
|
||||||
|
self._log(f"🔄 [JD下载] 第{attempt + 1}次重试下载...", "INFO")
|
||||||
|
else:
|
||||||
|
self._log(f"📥 [JD下载] 开始下载: {url[:100]}...", "INFO")
|
||||||
|
|
||||||
|
# 使用线程池下载
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.get(url, timeout=60, headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查HTTP状态
|
||||||
|
if response.status_code != 200:
|
||||||
|
self._log(f"❌ [JD下载] HTTP状态码: {response.status_code}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
continue
|
||||||
|
raise Exception(f"下载失败,HTTP状态码: {response.status_code}")
|
||||||
|
|
||||||
|
# 获取文件数据
|
||||||
|
file_data = response.content
|
||||||
|
file_size_kb = len(file_data) // 1024
|
||||||
|
|
||||||
|
# 检查文件大小(限制50MB)
|
||||||
|
if file_size_kb > 51200:
|
||||||
|
self._log(f"❌ [JD下载] 文件过大: {file_size_kb}KB,超过50MB限制", "ERROR")
|
||||||
|
raise Exception(f"文件过大: {file_size_kb}KB")
|
||||||
|
|
||||||
|
# 检查文件是否为空
|
||||||
|
if file_size_kb == 0:
|
||||||
|
self._log(f"❌ [JD下载] 下载的文件为空", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
raise Exception("下载的文件为空")
|
||||||
|
|
||||||
|
# 写入临时文件
|
||||||
|
with open(save_path, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
|
||||||
|
self._log(f"✅ [JD下载] 下载成功,大小: {file_size_kb}KB,文件: {file_name}", "SUCCESS")
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self._log(f"❌ [JD下载] 网络请求失败: {e}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ [JD下载] 下载失败: {e}", "ERROR")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 所有重试都失败
|
||||||
|
raise Exception(f"下载失败:已重试{max_retries}次")
|
||||||
|
|
||||||
|
async def upload_file_to_jd(self, file_path: str, file_type: str) -> dict:
|
||||||
|
"""上传文件到京东服务器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 本地文件路径
|
||||||
|
file_type: 文件类型 (image/video)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {path: 京东URL, width: 宽度, height: 高度}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.cookies_str or not self.current_aid or not self.current_pin_zj:
|
||||||
|
raise Exception("缺少必要的认证信息(cookies/aid/pin)")
|
||||||
|
|
||||||
|
self._log(f"📤 开始上传文件到京东: {file_path}", "INFO")
|
||||||
|
|
||||||
|
# 读取文件内容
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
file_content = f.read()
|
||||||
|
|
||||||
|
# 根据文件类型选择API和MIME类型
|
||||||
|
if file_type == "image" or any(ext in file_path.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif']):
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
elif file_type == "video" or '.mp4' in file_path.lower():
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadFile.action"
|
||||||
|
mime_type = 'video/mp4'
|
||||||
|
else:
|
||||||
|
raise Exception(f"不支持的文件类型: {file_path}")
|
||||||
|
|
||||||
|
# 准备请求
|
||||||
|
headers = {
|
||||||
|
"authority": "imio.jd.com",
|
||||||
|
"accept": "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"origin": "https://dongdong.jd.com",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"referer": "https://dongdong.jd.com/",
|
||||||
|
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
|
"sec-fetch-dest": "empty",
|
||||||
|
"sec-fetch-mode": "cors",
|
||||||
|
"sec-fetch-site": "same-site",
|
||||||
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||||
|
"cookie": self.cookies_str
|
||||||
|
}
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'upload': (os.path.basename(file_path), file_content, mime_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'httpsEnable': 'true',
|
||||||
|
'clientType': 'comet',
|
||||||
|
'appId': 'im.waiter',
|
||||||
|
'pin': self.current_pin_zj,
|
||||||
|
'aid': self.current_aid
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步请求(在异步上下文中使用 run_in_executor)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('path'):
|
||||||
|
self._log(f"✅ 文件上传成功: {result.get('path')}", "SUCCESS")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise Exception(f"上传失败: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 文件上传失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_video_thumbnail(self, video_path: str) -> dict:
|
||||||
|
"""提取视频封面并上传
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_path: 视频文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {path: 封面URL, width: 宽度, height: 高度}
|
||||||
|
"""
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._log(f"🎬 开始提取视频封面: {video_path}", "INFO")
|
||||||
|
|
||||||
|
# 提取视频第一帧
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
frame_position = int(0 * fps) # 第0秒
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_position)
|
||||||
|
|
||||||
|
ret, frame = cap.read()
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
raise Exception("无法读取视频帧")
|
||||||
|
|
||||||
|
# 编码为JPG
|
||||||
|
success, encoded_image = cv2.imencode('.jpg', frame)
|
||||||
|
if not success:
|
||||||
|
raise Exception("无法编码图像")
|
||||||
|
|
||||||
|
thumbnail_content = encoded_image.tobytes()
|
||||||
|
|
||||||
|
# 上传封面(使用图片上传接口)
|
||||||
|
headers = {
|
||||||
|
"authority": "imio.jd.com",
|
||||||
|
"accept": "*/*",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.9",
|
||||||
|
"cache-control": "no-cache",
|
||||||
|
"origin": "https://dongdong.jd.com",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"referer": "https://dongdong.jd.com/",
|
||||||
|
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
|
"sec-fetch-dest": "empty",
|
||||||
|
"sec-fetch-mode": "cors",
|
||||||
|
"sec-fetch-site": "same-site",
|
||||||
|
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||||
|
"cookie": self.cookies_str
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
|
||||||
|
files = {
|
||||||
|
'upload': ("thumbnail.jpg", thumbnail_content, 'image/jpeg')
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'httpsEnable': 'true',
|
||||||
|
'clientType': 'comet',
|
||||||
|
'appId': 'im.waiter',
|
||||||
|
'pin': self.current_pin_zj,
|
||||||
|
'aid': self.current_aid
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步请求
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
response = await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if result.get('path'):
|
||||||
|
self._log(f"✅ 视频封面上传成功: {result.get('path')}", "SUCCESS")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise Exception(f"封面上传失败: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 视频封面提取失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def send_image_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, image_url: str):
|
||||||
|
"""发送图片消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
image_url: 图片URL(外部URL,需要下载后上传)
|
||||||
|
"""
|
||||||
|
temp_file = None
|
||||||
|
try:
|
||||||
|
self._log(f"📷 开始发送图片消息: {image_url}", "INFO")
|
||||||
|
|
||||||
|
# 1. 下载图片
|
||||||
|
temp_file = await self.download_file(image_url)
|
||||||
|
|
||||||
|
# 2. 上传到京东服务器
|
||||||
|
upload_result = await self.upload_file_to_jd(temp_file, "image")
|
||||||
|
|
||||||
|
# 3. 发送图片消息
|
||||||
message = {
|
message = {
|
||||||
"ver": "4.3",
|
"ver": "4.3",
|
||||||
"type": "chat_message",
|
"type": "chat_message",
|
||||||
@@ -248,21 +804,94 @@ class FixJdCookie:
|
|||||||
"timestamp": int(time.time() * 1000),
|
"timestamp": int(time.time() * 1000),
|
||||||
"readFlag": 0,
|
"readFlag": 0,
|
||||||
"body": {
|
"body": {
|
||||||
"content": content,
|
"height": upload_result.get("height", 0),
|
||||||
"translated": False,
|
"width": upload_result.get("width", 0),
|
||||||
|
"url": upload_result.get("path"),
|
||||||
|
"translated": "",
|
||||||
"param": {"cusVenderId": vender_id},
|
"param": {"cusVenderId": vender_id},
|
||||||
"type": "text"
|
"type": "image"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.send(json.dumps(message))
|
await ws.send(json.dumps(message))
|
||||||
logger.info(f"消息已经发送到客户端[info] {pin}: {content[:20]} ...")
|
self._log(f"✅ 图片消息发送成功: {pin}", "SUCCESS")
|
||||||
except websockets.ConnectionClosed:
|
|
||||||
logger.error('本地发送消息失败 连接关闭')
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 同时这里也要及时进行raise抛出 这样比较好让系统可以看出 异常了可以抛出信息不至于后续被认为
|
self._log(f"❌ 图片消息发送失败: {e}", "ERROR")
|
||||||
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
|
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
try:
|
||||||
|
os.remove(temp_file)
|
||||||
|
self._log(f"🗑️ 已清理临时文件: {temp_file}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send_video_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, video_url: str):
|
||||||
|
"""发送视频消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ws: WebSocket连接
|
||||||
|
pin: 客户pin
|
||||||
|
aid: 账号aid
|
||||||
|
pin_zj: 客服pin
|
||||||
|
vender_id: 商家ID
|
||||||
|
video_url: 视频URL(外部URL,需要下载后上传)
|
||||||
|
"""
|
||||||
|
temp_video = None
|
||||||
|
try:
|
||||||
|
self._log(f"🎥 开始发送视频消息: {video_url}", "INFO")
|
||||||
|
|
||||||
|
# 1. 下载视频
|
||||||
|
temp_video = await self.download_file(video_url)
|
||||||
|
|
||||||
|
# 2. 提取并上传封面
|
||||||
|
thumbnail_result = await self.get_video_thumbnail(temp_video)
|
||||||
|
|
||||||
|
# 3. 上传视频
|
||||||
|
await asyncio.sleep(2) # 等待2秒,避免请求过快
|
||||||
|
video_result = await self.upload_file_to_jd(temp_video, "video")
|
||||||
|
|
||||||
|
# 4. 发送视频消息
|
||||||
|
message = {
|
||||||
|
"ver": "4.3",
|
||||||
|
"type": "chat_message",
|
||||||
|
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
||||||
|
"to": {"app": "im.customer", "pin": pin},
|
||||||
|
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
|
||||||
|
"lang": "zh_CN",
|
||||||
|
"aid": aid,
|
||||||
|
"timestamp": int(time.time() * 1000),
|
||||||
|
"readFlag": 0,
|
||||||
|
"body": {
|
||||||
|
"desc": "",
|
||||||
|
"duration": 6,
|
||||||
|
"param": {},
|
||||||
|
"reupload": "false",
|
||||||
|
"size": os.path.getsize(temp_video),
|
||||||
|
"thumbHeight": thumbnail_result.get("height", 0),
|
||||||
|
"thumbWidth": thumbnail_result.get("width", 0),
|
||||||
|
"thumbnail": thumbnail_result.get("path"),
|
||||||
|
"url": video_result.get("path"),
|
||||||
|
"type": "video"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.send(json.dumps(message))
|
||||||
|
self._log(f"✅ 视频消息发送成功: {pin}", "SUCCESS")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 视频消息发送失败: {e}", "ERROR")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_video and os.path.exists(temp_video):
|
||||||
|
try:
|
||||||
|
os.remove(temp_video)
|
||||||
|
self._log(f"🗑️ 已清理临时视频: {temp_video}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def get_userinfo(self, response_text):
|
def get_userinfo(self, response_text):
|
||||||
"""获取用户 pin 并存入 pins 列表"""
|
"""获取用户 pin 并存入 pins 列表"""
|
||||||
@@ -475,6 +1104,11 @@ class FixJdCookie:
|
|||||||
print("✅ DEBUG 进入message_monitoring方法")
|
print("✅ DEBUG 进入message_monitoring方法")
|
||||||
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
|
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
|
||||||
|
|
||||||
|
# 🔥 保存认证信息,用于文件上传
|
||||||
|
self.cookies_str = cookies_str
|
||||||
|
self.current_aid = aid
|
||||||
|
self.current_pin_zj = pin_zj
|
||||||
|
|
||||||
# 连接后端AI服务 - 使用店铺ID或venderId
|
# 连接后端AI服务 - 使用店铺ID或venderId
|
||||||
store_id = str(store.get('id', '')) or str(vender_id)
|
store_id = str(store.get('id', '')) or str(vender_id)
|
||||||
self._log(f"🔗 尝试连接后端服务,店铺ID: {store_id}", "DEBUG")
|
self._log(f"🔗 尝试连接后端服务,店铺ID: {store_id}", "DEBUG")
|
||||||
@@ -506,6 +1140,13 @@ class FixJdCookie:
|
|||||||
# print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}")
|
# print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}")
|
||||||
print(f"🖥️ 服务端地址: {ws.remote_address}")
|
print(f"🖥️ 服务端地址: {ws.remote_address}")
|
||||||
|
|
||||||
|
# 连接成功后,获取并上报一次客服列表(最小改动恢复)
|
||||||
|
try:
|
||||||
|
await self.get_all_customer(ws, aid, pin_zj)
|
||||||
|
await self.send_staff_list_to_backend(store.get('id', ''))
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR")
|
||||||
|
|
||||||
# --- 注册连接信息到全局管理
|
# --- 注册连接信息到全局管理
|
||||||
shop_key = f"京东:{store['id']}"
|
shop_key = f"京东:{store['id']}"
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -516,7 +1157,8 @@ class FixJdCookie:
|
|||||||
aid=aid,
|
aid=aid,
|
||||||
pin_zj=pin_zj,
|
pin_zj=pin_zj,
|
||||||
platform="京东",
|
platform="京东",
|
||||||
loop=loop
|
loop=loop,
|
||||||
|
cookies_str=cookies_str # 🔥 传递cookies用于文件上传
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
|
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
|
||||||
@@ -530,12 +1172,44 @@ class FixJdCookie:
|
|||||||
print(f"等待监听消息-{datetime.now()}")
|
print(f"等待监听消息-{datetime.now()}")
|
||||||
response = await asyncio.wait_for(ws.recv(), timeout=1)
|
response = await asyncio.wait_for(ws.recv(), timeout=1)
|
||||||
print(f"原始消息类型:{type(response)}, 消息体为: {response}")
|
print(f"原始消息类型:{type(response)}, 消息体为: {response}")
|
||||||
|
|
||||||
|
# 🔧 修复:检测被踢下线消息
|
||||||
|
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
|
||||||
|
|
||||||
|
# 检查是否为server_msg类型且code=5(被踢下线)
|
||||||
|
if json_resp.get("type") == "server_msg":
|
||||||
|
body = json_resp.get("body", {})
|
||||||
|
if body.get("code") == 5:
|
||||||
|
msgtext = body.get("msgtext", "账号在其他设备登录")
|
||||||
|
self._log(f"⚠️ 收到被踢下线消息: {msgtext}", "WARNING")
|
||||||
|
self._log("⚠️ 检测到多端登录冲突,请确保:", "WARNING")
|
||||||
|
self._log(" 1. 关闭网页版京东咚咚", "WARNING")
|
||||||
|
self._log(" 2. 关闭其他客户端", "WARNING")
|
||||||
|
self._log(" 3. 确认只有本客户端连接", "WARNING")
|
||||||
|
|
||||||
|
# 通知GUI显示弹窗
|
||||||
|
try:
|
||||||
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
|
ws_manager = get_websocket_manager()
|
||||||
|
if ws_manager:
|
||||||
|
# 从platform_listeners获取店铺名称
|
||||||
|
store_name = "京东店铺"
|
||||||
|
for key, info in ws_manager.platform_listeners.items():
|
||||||
|
if info.get('store_id') == store:
|
||||||
|
store_name = info.get('store_name', '') or "京东店铺"
|
||||||
|
break
|
||||||
|
# 传递 store_id 参数,用于通知后端
|
||||||
|
ws_manager.notify_platform_kicked("京东", store_name, msgtext, store_id=store)
|
||||||
|
except Exception as notify_error:
|
||||||
|
self._log(f"通知GUI失败: {notify_error}", "ERROR")
|
||||||
|
|
||||||
|
# 不再自动重连,等待用户处理
|
||||||
|
stop_event.set()
|
||||||
|
break
|
||||||
|
|
||||||
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
|
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
|
||||||
|
|
||||||
# 安全解析消息
|
# print(json_resp)
|
||||||
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
|
|
||||||
|
|
||||||
print(json_resp)
|
|
||||||
|
|
||||||
ver = json_resp.get("ver")
|
ver = json_resp.get("ver")
|
||||||
print(f"版本{ver}")
|
print(f"版本{ver}")
|
||||||
@@ -579,9 +1253,8 @@ class FixJdCookie:
|
|||||||
if not await self.handle_reconnect(e):
|
if not await self.handle_reconnect(e):
|
||||||
break
|
break
|
||||||
|
|
||||||
# 关闭后端服务连接
|
# 关闭后端服务连接(JDBackendService没有close方法,跳过)
|
||||||
if self.backend_connected:
|
if self.backend_connected:
|
||||||
await self.backend_service.close()
|
|
||||||
self.backend_connected = False
|
self.backend_connected = False
|
||||||
self._log("🛑 消息监听已停止", "INFO")
|
self._log("🛑 消息监听已停止", "INFO")
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
# WebSocket/BackendClient.py
|
# WebSocket/BackendClient.py
|
||||||
|
# hsc1
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -31,6 +32,9 @@ class BackendClient:
|
|||||||
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
|
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
|
||||||
self.token_error_callback: Optional[Callable] = None # 新增:token错误回调
|
self.token_error_callback: Optional[Callable] = None # 新增:token错误回调
|
||||||
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
|
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
|
||||||
|
self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调
|
||||||
|
self.balance_insufficient_callback: Optional[Callable] = None # 新增:余额不足回调
|
||||||
|
self.log_callback: Optional[Callable] = None # 新增:日志回调
|
||||||
|
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|
||||||
@@ -46,6 +50,18 @@ class BackendClient:
|
|||||||
self.loop = None
|
self.loop = None
|
||||||
self.thread = None
|
self.thread = None
|
||||||
|
|
||||||
|
def _log(self, message: str, level: str = "INFO"):
|
||||||
|
"""Unified logging method that works in both dev and packaged environments"""
|
||||||
|
# Always print to console (visible in dev mode)
|
||||||
|
print(f"[{level}] {message}")
|
||||||
|
|
||||||
|
# Also call the log callback if available (for file logging)
|
||||||
|
if self.log_callback:
|
||||||
|
try:
|
||||||
|
self.log_callback(message, level)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""连接到WebSocket服务器"""
|
"""连接到WebSocket服务器"""
|
||||||
if self.is_connected:
|
if self.is_connected:
|
||||||
@@ -89,7 +105,7 @@ class BackendClient:
|
|||||||
"""连接并监听消息 - 带重连机制"""
|
"""连接并监听消息 - 带重连机制"""
|
||||||
while not self.should_stop:
|
while not self.should_stop:
|
||||||
try:
|
try:
|
||||||
print(f"正在连接后端WebSocket: {self.url}")
|
self._log(f"正在连接后端WebSocket: {self.url}")
|
||||||
|
|
||||||
# 建立连接(可配置的ping设置)
|
# 建立连接(可配置的ping设置)
|
||||||
from config import WS_PING_INTERVAL, WS_PING_TIMEOUT, WS_ENABLE_PING
|
from config import WS_PING_INTERVAL, WS_PING_TIMEOUT, WS_ENABLE_PING
|
||||||
@@ -105,7 +121,7 @@ class BackendClient:
|
|||||||
max_queue=32, # 最大队列大小
|
max_queue=32, # 最大队列大小
|
||||||
compression=None # 禁用压缩以提高性能
|
compression=None # 禁用压缩以提高性能
|
||||||
)
|
)
|
||||||
print(f"[连接] 已启用心跳:ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s")
|
self._log(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,
|
||||||
@@ -113,30 +129,44 @@ class BackendClient:
|
|||||||
max_queue=32,
|
max_queue=32,
|
||||||
compression=None
|
compression=None
|
||||||
)
|
)
|
||||||
print("[连接] 已禁用心跳机制")
|
self._log("已禁用心跳机制", "WARNING")
|
||||||
|
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
|
|
||||||
|
# 🔥 在重置之前记录是否是重连(用于后续上报平台状态)
|
||||||
|
was_reconnecting = self.reconnect_attempts > 0
|
||||||
|
|
||||||
self.reconnect_attempts = 0 # 重置重连计数
|
self.reconnect_attempts = 0 # 重置重连计数
|
||||||
self.is_reconnecting = False
|
self.is_reconnecting = False
|
||||||
print("后端WebSocket连接成功")
|
self._log("后端WebSocket连接成功", "SUCCESS")
|
||||||
|
|
||||||
# 等待连接稳定后再发送状态通知
|
# 等待连接稳定后再发送状态通知
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# 发送连接状态通知给后端
|
# 发送连接状态通知给后端
|
||||||
self._notify_connection_status(True)
|
self._notify_connection_status(True, is_reconnect=was_reconnecting)
|
||||||
|
|
||||||
self.on_connected()
|
self.on_connected(was_reconnecting)
|
||||||
|
|
||||||
# 消息循环
|
# 消息循环
|
||||||
async for message in self.websocket:
|
async for message in self.websocket:
|
||||||
try:
|
try:
|
||||||
# 打印原始文本帧与长度
|
# 🔍 添加心跳检测日志
|
||||||
try:
|
try:
|
||||||
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
|
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
|
||||||
print(f"后端发送消息体内容:{message}")
|
# 解析消息类型
|
||||||
|
data_preview = json.loads(message)
|
||||||
|
msg_type = data_preview.get('type', 'unknown')
|
||||||
|
|
||||||
|
# 心跳相关消息用DEBUG级别,其他消息用INFO级别
|
||||||
|
if msg_type in ['pong', 'connection_status_ack']:
|
||||||
|
self._log(f"💓 [心跳] 收到后端响应: {msg_type}", "DEBUG")
|
||||||
|
else:
|
||||||
|
self._log(f"📨 [后端] 收到消息: type={msg_type}, 长度={raw_len}字节", "DEBUG")
|
||||||
|
print(f"后端发送消息体内容:{message}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
data = json.loads(message)
|
data = json.loads(message)
|
||||||
self.on_message_received(data)
|
self.on_message_received(data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -148,13 +178,13 @@ class BackendClient:
|
|||||||
|
|
||||||
# 详细分析断开原因
|
# 详细分析断开原因
|
||||||
if e.code == 1006:
|
if e.code == 1006:
|
||||||
print(f"[重连] WebSocket异常关闭 (1006): 可能是心跳超时或网络问题")
|
self._log("WebSocket异常关闭 (1006): 可能是心跳超时或网络问题", "WARNING")
|
||||||
elif e.code == 1000:
|
elif e.code == 1000:
|
||||||
print(f"[重连] WebSocket正常关闭 (1000): 服务端主动断开")
|
self._log("WebSocket正常关闭 (1000): 服务端主动断开", "INFO")
|
||||||
elif e.code == 1001:
|
elif e.code == 1001:
|
||||||
print(f"[重连] WebSocket关闭 (1001): 端点离开")
|
self._log("WebSocket关闭 (1001): 端点离开", "INFO")
|
||||||
else:
|
else:
|
||||||
print(f"[重连] WebSocket关闭 ({e.code}): {e.reason}")
|
self._log(f"WebSocket关闭 ({e.code}): {e.reason}", "WARNING")
|
||||||
|
|
||||||
self._handle_connection_closed(e)
|
self._handle_connection_closed(e)
|
||||||
if not await self._should_reconnect():
|
if not await self._should_reconnect():
|
||||||
@@ -184,14 +214,14 @@ class BackendClient:
|
|||||||
def _handle_connection_closed(self, error):
|
def _handle_connection_closed(self, error):
|
||||||
"""处理连接关闭"""
|
"""处理连接关闭"""
|
||||||
error_msg = f"WebSocket连接已关闭: {error.code} {error.reason if hasattr(error, 'reason') else ''}"
|
error_msg = f"WebSocket连接已关闭: {error.code} {error.reason if hasattr(error, 'reason') else ''}"
|
||||||
print(f"[重连] {error_msg}")
|
self._log(error_msg, "WARNING")
|
||||||
|
|
||||||
# 特殊处理ping超时等情况
|
# 特殊处理ping超时等情况
|
||||||
if hasattr(error, 'code'):
|
if hasattr(error, 'code'):
|
||||||
if error.code == 1011: # Internal error (ping timeout)
|
if error.code == 1011: # Internal error (ping timeout)
|
||||||
print("[重连] 检测到ping超时,这是常见的网络问题")
|
self._log("检测到ping超时,这是常见的网络问题", "WARNING")
|
||||||
elif error.code == 1006: # Abnormal closure
|
elif error.code == 1006: # Abnormal closure
|
||||||
print("[重连] 检测到异常关闭,可能是网络中断")
|
self._log("检测到异常关闭,可能是网络中断或心跳超时", "WARNING")
|
||||||
|
|
||||||
if not self.is_reconnecting:
|
if not self.is_reconnecting:
|
||||||
self.on_error(error_msg)
|
self.on_error(error_msg)
|
||||||
@@ -199,25 +229,25 @@ class BackendClient:
|
|||||||
def _handle_network_error(self, error):
|
def _handle_network_error(self, error):
|
||||||
"""处理网络错误"""
|
"""处理网络错误"""
|
||||||
error_msg = f"网络连接错误: {type(error).__name__} - {str(error)}"
|
error_msg = f"网络连接错误: {type(error).__name__} - {str(error)}"
|
||||||
print(f"[重连] {error_msg}")
|
self._log(error_msg, "ERROR")
|
||||||
if not self.is_reconnecting:
|
if not self.is_reconnecting:
|
||||||
self.on_error(error_msg)
|
self.on_error(error_msg)
|
||||||
|
|
||||||
def _handle_general_error(self, error):
|
def _handle_general_error(self, error):
|
||||||
"""处理一般错误"""
|
"""处理一般错误"""
|
||||||
error_msg = f"WebSocket连接异常: {type(error).__name__} - {str(error)}"
|
error_msg = f"WebSocket连接异常: {type(error).__name__} - {str(error)}"
|
||||||
print(f"[重连] {error_msg}")
|
self._log(error_msg, "ERROR")
|
||||||
if not self.is_reconnecting:
|
if not self.is_reconnecting:
|
||||||
self.on_error(error_msg)
|
self.on_error(error_msg)
|
||||||
|
|
||||||
async def _should_reconnect(self) -> bool:
|
async def _should_reconnect(self) -> bool:
|
||||||
"""判断是否应该重连"""
|
"""判断是否应该重连"""
|
||||||
if self.should_stop:
|
if self.should_stop:
|
||||||
print("[重连] 程序正在关闭,停止重连")
|
self._log("程序正在关闭,停止重连", "INFO")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.reconnect_attempts >= self.max_reconnect_attempts:
|
if self.reconnect_attempts >= self.max_reconnect_attempts:
|
||||||
print(f"[重连] 已达到最大重连次数({self.max_reconnect_attempts}),停止重连")
|
self._log(f"已达到最大重连次数({self.max_reconnect_attempts}),停止重连", "ERROR")
|
||||||
# 通知上层重连失败
|
# 通知上层重连失败
|
||||||
if not self.is_reconnecting:
|
if not self.is_reconnecting:
|
||||||
self.on_error(f"重连失败:已达到最大重连次数({self.max_reconnect_attempts})")
|
self.on_error(f"重连失败:已达到最大重连次数({self.max_reconnect_attempts})")
|
||||||
@@ -235,7 +265,7 @@ class BackendClient:
|
|||||||
self.reconnect_attempts += 1
|
self.reconnect_attempts += 1
|
||||||
self.is_reconnecting = True
|
self.is_reconnecting = True
|
||||||
|
|
||||||
print(f"[重连] 第{self.reconnect_attempts}次重连尝试,等待{delay:.1f}秒...")
|
self._log(f"第{self.reconnect_attempts}次重连尝试,等待{delay:.1f}秒...", "INFO")
|
||||||
|
|
||||||
# 分割等待时间,支持快速退出
|
# 分割等待时间,支持快速退出
|
||||||
wait_steps = max(1, int(delay))
|
wait_steps = max(1, int(delay))
|
||||||
@@ -265,7 +295,10 @@ class BackendClient:
|
|||||||
login: Callable = None,
|
login: Callable = None,
|
||||||
success: Callable = None,
|
success: Callable = None,
|
||||||
token_error: Callable = None,
|
token_error: Callable = None,
|
||||||
version: Callable = None):
|
version: Callable = None,
|
||||||
|
disconnect: Callable = None,
|
||||||
|
balance_insufficient: Callable = None,
|
||||||
|
log: Callable = None):
|
||||||
"""设置各种消息类型的回调函数"""
|
"""设置各种消息类型的回调函数"""
|
||||||
if store_list:
|
if store_list:
|
||||||
self.store_list_callback = store_list
|
self.store_list_callback = store_list
|
||||||
@@ -287,56 +320,77 @@ class BackendClient:
|
|||||||
self.token_error_callback = token_error
|
self.token_error_callback = token_error
|
||||||
if version:
|
if version:
|
||||||
self.version_callback = version
|
self.version_callback = version
|
||||||
|
if disconnect:
|
||||||
|
self.disconnect_callback = disconnect
|
||||||
|
if balance_insufficient:
|
||||||
|
self.balance_insufficient_callback = balance_insufficient
|
||||||
|
if log:
|
||||||
|
self.log_callback = log
|
||||||
|
|
||||||
def on_connected(self):
|
def on_connected(self, was_reconnecting: bool = False):
|
||||||
"""连接成功时的处理"""
|
"""连接成功时的处理"""
|
||||||
if self.reconnect_attempts > 0:
|
if was_reconnecting:
|
||||||
print(f"[重连] 后端WebSocket重连成功!(第{self.reconnect_attempts}次尝试)")
|
self._log("后端WebSocket重连成功!", "SUCCESS")
|
||||||
else:
|
# 重连成功后上报平台状态给后端
|
||||||
print("后端WebSocket连接成功")
|
|
||||||
|
|
||||||
# 重连成功后可选择上报状态给后端
|
|
||||||
if self.reconnect_attempts > 0:
|
|
||||||
self._report_reconnect_status()
|
self._report_reconnect_status()
|
||||||
|
else:
|
||||||
|
self._log("后端WebSocket首次连接成功", "SUCCESS")
|
||||||
|
|
||||||
# 不再主动请求 get_store,避免与后端不兼容导致协程未完成
|
# 不再主动请求 get_store,避免与后端不兼容导致协程未完成
|
||||||
|
|
||||||
def _report_reconnect_status(self):
|
def _report_reconnect_status(self):
|
||||||
"""重连成功后上报当前状态(可选)"""
|
"""重连成功后上报当前状态"""
|
||||||
try:
|
try:
|
||||||
# 获取当前已连接的平台列表
|
# 获取当前已连接的平台列表
|
||||||
from WebSocket.backend_singleton import get_websocket_manager
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
manager = get_websocket_manager()
|
manager = get_websocket_manager()
|
||||||
|
|
||||||
if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
|
if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
|
||||||
|
platform_count = len(manager.platform_listeners)
|
||||||
|
self._log(f"🔄 检测到 {platform_count} 个活跃平台连接,准备重新上报状态", "INFO")
|
||||||
|
|
||||||
|
# 延迟1秒,确保后端完全准备好
|
||||||
|
import time
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
for platform_key, listener_info in manager.platform_listeners.items():
|
for platform_key, listener_info in manager.platform_listeners.items():
|
||||||
store_id = listener_info.get('store_id')
|
store_id = listener_info.get('store_id')
|
||||||
platform = listener_info.get('platform')
|
platform = listener_info.get('platform')
|
||||||
|
store_name = listener_info.get('store_name', '')
|
||||||
|
|
||||||
if store_id and platform:
|
if store_id and platform:
|
||||||
# 上报平台仍在连接状态
|
# 上报平台仍在连接状态
|
||||||
try:
|
try:
|
||||||
|
store_display = store_name or store_id[:8] + "..."
|
||||||
reconnect_message = {
|
reconnect_message = {
|
||||||
"type": "connect_message",
|
"type": "connect_message",
|
||||||
"store_id": store_id,
|
"store_id": store_id,
|
||||||
"status": True,
|
"status": True,
|
||||||
"content": f"GUI重连成功,{platform}平台状态正常"
|
"cookies": "" # 重连时无需再次发送cookies
|
||||||
}
|
}
|
||||||
# 异步发送,不阻塞连接过程
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
# 同步发送,确保发送成功
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self._send_to_backend(reconnect_message),
|
self._send_to_backend(reconnect_message),
|
||||||
self.loop
|
self.loop
|
||||||
)
|
)
|
||||||
print(f"[重连] 已上报{platform}平台状态")
|
# 等待发送完成(最多2秒)
|
||||||
|
future.result(timeout=2)
|
||||||
|
|
||||||
|
self._log(f"✅ 已重新上报 {platform} 平台状态: {store_display}", "SUCCESS")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[重连] 上报{platform}平台状态失败: {e}")
|
self._log(f"❌ 上报 {platform} 平台状态失败: {e}", "ERROR")
|
||||||
|
import traceback
|
||||||
|
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG")
|
||||||
else:
|
else:
|
||||||
print("[重连] 当前无活跃平台连接,跳过状态上报")
|
self._log("当前无活跃平台连接,跳过状态上报", "INFO")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[重连] 状态上报过程异常: {e}")
|
self._log(f"状态上报过程异常: {e}", "ERROR")
|
||||||
|
import traceback
|
||||||
|
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG")
|
||||||
|
|
||||||
def _notify_connection_status(self, connected: bool):
|
def _notify_connection_status(self, connected: bool, is_reconnect: bool = False):
|
||||||
"""通知后端连接状态变化"""
|
"""通知后端连接状态变化"""
|
||||||
try:
|
try:
|
||||||
if not self.loop:
|
if not self.loop:
|
||||||
@@ -345,58 +399,59 @@ class BackendClient:
|
|||||||
# 获取当前活跃平台的store_id
|
# 获取当前活跃平台的store_id
|
||||||
active_store_id = None
|
active_store_id = None
|
||||||
try:
|
try:
|
||||||
from Utils.JD.JdUtils import WebsocketManager as JdManager
|
from WebSocket.backend_singleton import get_websocket_manager
|
||||||
from Utils.Dy.DyUtils import DouYinWebsocketManager as DyManager
|
manager = get_websocket_manager()
|
||||||
from Utils.Pdd.PddUtils import WebsocketManager as PddManager
|
|
||||||
|
|
||||||
# 检查各平台是否有活跃连接
|
# 从 platform_listeners 获取活跃平台
|
||||||
for mgr_class, platform_name in [(JdManager, "京东"), (DyManager, "抖音"), (PddManager, "拼多多")]:
|
if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
|
||||||
try:
|
# 获取第一个活跃平台的 store_id
|
||||||
mgr = mgr_class()
|
for platform_key, listener_info in manager.platform_listeners.items():
|
||||||
if hasattr(mgr, '_store') and mgr._store:
|
store_id = listener_info.get('store_id')
|
||||||
for shop_key, entry in mgr._store.items():
|
platform = listener_info.get('platform')
|
||||||
if entry and entry.get('platform'):
|
if store_id and platform:
|
||||||
# 从shop_key中提取store_id(格式:平台:store_id)
|
active_store_id = store_id
|
||||||
if ':' in shop_key:
|
self._log(f"检测到活跃{platform}平台: {store_id}", "DEBUG")
|
||||||
_, store_id = shop_key.split(':', 1)
|
break
|
||||||
active_store_id = store_id
|
|
||||||
print(f"[状态] 检测到活跃{platform_name}平台: {store_id}")
|
|
||||||
break
|
|
||||||
if active_store_id:
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[状态] 检查{platform_name}平台失败: {e}")
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[状态] 获取活跃平台信息失败: {e}")
|
self._log(f"获取活跃平台信息失败: {e}", "DEBUG")
|
||||||
|
|
||||||
status_message = {
|
status_message = {
|
||||||
"type": "connection_status",
|
"type": "connection_status",
|
||||||
"status": connected,
|
"status": connected,
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
"client_uuid": self.uuid
|
"client_uuid": self.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 🔥 新增:如果是重连,添加重连标识
|
||||||
|
if is_reconnect:
|
||||||
|
status_message["is_reconnect"] = True
|
||||||
|
|
||||||
# 如果有活跃平台,添加store_id
|
# 如果有活跃平台,添加store_id
|
||||||
if active_store_id:
|
if active_store_id:
|
||||||
status_message["store_id"] = active_store_id
|
status_message["store_id"] = active_store_id
|
||||||
print(f"[状态] 添加store_id到状态消息: {active_store_id}")
|
self._log(f"添加store_id到状态消息: {active_store_id}", "DEBUG")
|
||||||
else:
|
else:
|
||||||
print(f"[状态] 未检测到活跃平台,不添加store_id")
|
self._log("未检测到活跃平台,不添加store_id", "DEBUG")
|
||||||
|
|
||||||
# 异步发送状态通知
|
# 异步发送状态通知
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self._send_to_backend(status_message),
|
self._send_to_backend(status_message),
|
||||||
self.loop
|
self.loop
|
||||||
)
|
)
|
||||||
|
#
|
||||||
|
# # 🔥 等待发送完成(可选,避免警告)
|
||||||
|
# try:
|
||||||
|
# future.result(timeout=2) # 最多等待2秒
|
||||||
|
# except Exception as send_error:
|
||||||
|
# self._log(f"发送状态通知异常: {send_error}", "DEBUG")
|
||||||
|
|
||||||
status_text = "连接" if connected else "断开"
|
status_text = "连接" if connected else "断开"
|
||||||
print(f"[状态] 已通知后端GUI客户端{status_text}")
|
self._log(f"已通知后端GUI客户端{status_text}", "DEBUG")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[状态] 发送状态通知失败: {e}")
|
self._log(f"发送状态通知失败: {e}", "ERROR")
|
||||||
import traceback
|
import traceback
|
||||||
print(f"[状态] 详细错误: {traceback.format_exc()}")
|
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG")
|
||||||
|
|
||||||
def on_message_received(self, message: Dict[str, Any]):
|
def on_message_received(self, message: Dict[str, Any]):
|
||||||
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
|
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
|
||||||
@@ -443,6 +498,10 @@ class BackendClient:
|
|||||||
self._handle_staff_list(message)
|
self._handle_staff_list(message)
|
||||||
elif msg_type == 'version_response': # 新增:版本检查响应
|
elif msg_type == 'version_response': # 新增:版本检查响应
|
||||||
self._handle_version_response(message)
|
self._handle_version_response(message)
|
||||||
|
elif msg_type == 'disconnect': # 新增:被踢下线
|
||||||
|
self._handle_disconnect(message)
|
||||||
|
elif msg_type == 'balance_insufficient': # 新增:余额不足
|
||||||
|
self._handle_balance_insufficient(message)
|
||||||
else:
|
else:
|
||||||
print(f"未知消息类型: {msg_type}")
|
print(f"未知消息类型: {msg_type}")
|
||||||
|
|
||||||
@@ -658,7 +717,8 @@ class BackendClient:
|
|||||||
store_id = message.get('store_id', '')
|
store_id = message.get('store_id', '')
|
||||||
data = message.get('data')
|
data = message.get('data')
|
||||||
content = message.get('content', '')
|
content = message.get('content', '')
|
||||||
print(f"[{store_id}] [{message.get('msg_type', 'unknown')}] : {content}")
|
msg_type = message.get('msg_type', 'text') # 获取消息类型,默认为text
|
||||||
|
print(f"[{store_id}] [{msg_type}] : {content}")
|
||||||
|
|
||||||
# 尝试将后端AI/客服回复转发到对应平台
|
# 尝试将后端AI/客服回复转发到对应平台
|
||||||
try:
|
try:
|
||||||
@@ -669,13 +729,16 @@ class BackendClient:
|
|||||||
platform_type = self._get_platform_by_store_id(store_id)
|
platform_type = self._get_platform_by_store_id(store_id)
|
||||||
|
|
||||||
if platform_type == "京东":
|
if platform_type == "京东":
|
||||||
self._forward_to_jd(store_id, recv_pin, content)
|
# 🔥 传递msg_type参数,支持图片/视频等类型
|
||||||
|
self._forward_to_jd(store_id, recv_pin, content, msg_type)
|
||||||
elif platform_type == "抖音":
|
elif platform_type == "抖音":
|
||||||
self._forward_to_douyin(store_id, recv_pin, content)
|
# 传递msg_type参数,支持图片/视频等类型
|
||||||
|
self._forward_to_douyin(store_id, recv_pin, content, msg_type)
|
||||||
elif platform_type == "千牛":
|
elif platform_type == "千牛":
|
||||||
self._forward_to_qianniu(store_id, recv_pin, content)
|
self._forward_to_qianniu(store_id, recv_pin, content)
|
||||||
elif platform_type == "拼多多":
|
elif platform_type == "拼多多":
|
||||||
self._forward_to_pdd(store_id, recv_pin, content)
|
# 传递msg_type参数,支持图片/视频等类型
|
||||||
|
self._forward_to_pdd(store_id, recv_pin, content, msg_type)
|
||||||
else:
|
else:
|
||||||
print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
|
print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -699,8 +762,15 @@ class BackendClient:
|
|||||||
print(f"获取平台类型失败: {e}")
|
print(f"获取平台类型失败: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str):
|
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
||||||
"""转发消息到京东平台"""
|
"""转发消息到京东平台(支持文本/图片/视频)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store_id: 店铺ID
|
||||||
|
recv_pin: 接收者pin
|
||||||
|
content: 消息内容(文本内容或URL)
|
||||||
|
msg_type: 消息类型(text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
|
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
|
||||||
jd_mgr = JDWSManager()
|
jd_mgr = JDWSManager()
|
||||||
@@ -716,52 +786,62 @@ class BackendClient:
|
|||||||
pin_zj = platform_info.get('pin_zj')
|
pin_zj = platform_info.get('pin_zj')
|
||||||
vender_id = platform_info.get('vender_id')
|
vender_id = platform_info.get('vender_id')
|
||||||
loop = platform_info.get('loop')
|
loop = platform_info.get('loop')
|
||||||
|
cookies_str = platform_info.get('cookies_str') # 🔥 获取cookies用于文件上传
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} recv_pin={recv_pin}")
|
f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} has_cookies={bool(cookies_str)} recv_pin={recv_pin} msg_type={msg_type}")
|
||||||
|
|
||||||
if ws and aid and pin_zj and vender_id and loop and content:
|
if ws and aid and pin_zj and vender_id and loop and content:
|
||||||
|
# 🔥 获取 FixJdCookie 实例,使用其 send_message 方法(支持多媒体)
|
||||||
async def _send():
|
async def _send():
|
||||||
import hashlib as _hashlib
|
from Utils.JD.JdUtils import FixJdCookie
|
||||||
import time as _time
|
# 创建临时实例用于发送
|
||||||
import json as _json
|
jd_instance = FixJdCookie()
|
||||||
msg = {
|
# 🔥 设置认证信息(用于图片/视频上传)
|
||||||
"ver": "4.3",
|
jd_instance.cookies_str = cookies_str
|
||||||
"type": "chat_message",
|
jd_instance.current_aid = aid
|
||||||
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
|
jd_instance.current_pin_zj = pin_zj
|
||||||
"to": {"app": "im.customer", "pin": recv_pin},
|
# 调用支持多媒体的 send_message 方法
|
||||||
"id": _hashlib.md5(str(int(_time.time() * 1000)).encode()).hexdigest(),
|
await jd_instance.send_message(
|
||||||
"lang": "zh_CN",
|
ws=ws,
|
||||||
"aid": aid,
|
pin=recv_pin,
|
||||||
"timestamp": int(_time.time() * 1000),
|
aid=aid,
|
||||||
"readFlag": 0,
|
pin_zj=pin_zj,
|
||||||
"body": {
|
vender_id=vender_id,
|
||||||
"content": content,
|
content=content,
|
||||||
"translated": False,
|
msg_type=msg_type
|
||||||
"param": {"cusVenderId": vender_id},
|
)
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await ws.send(_json.dumps(msg))
|
|
||||||
|
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
|
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
|
||||||
try:
|
try:
|
||||||
_future.result(timeout=2)
|
_future.result(timeout=60) # 图片/视频需要更长时间
|
||||||
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
|
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}")
|
||||||
except Exception as fe:
|
except Exception as fe:
|
||||||
print(f"[JD Forward] 转发提交失败: {fe}")
|
print(f"[JD Forward] 转发提交失败: {fe}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print("[JD Forward] 条件不足,未转发:",
|
print("[JD Forward] 条件不足,未转发:",
|
||||||
{
|
{
|
||||||
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
|
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
|
||||||
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_content': bool(content)
|
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_cookies': bool(cookies_str),
|
||||||
|
'has_content': bool(content)
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[JD Forward] 转发失败: {e}")
|
print(f"[JD Forward] 转发失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str):
|
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
||||||
"""转发消息到抖音平台"""
|
"""转发消息到抖音平台
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store_id: 店铺ID
|
||||||
|
recv_pin: 接收者ID
|
||||||
|
content: 消息内容
|
||||||
|
msg_type: 消息类型(text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from Utils.Dy.DyUtils import DouYinWebsocketManager
|
from Utils.Dy.DyUtils import DouYinWebsocketManager
|
||||||
dy_mgr = DouYinWebsocketManager()
|
dy_mgr = DouYinWebsocketManager()
|
||||||
@@ -777,7 +857,7 @@ class BackendClient:
|
|||||||
message_handler = platform_info.get('message_handler')
|
message_handler = platform_info.get('message_handler')
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin}")
|
f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin} msg_type={msg_type}")
|
||||||
|
|
||||||
if douyin_bot and message_handler and content:
|
if douyin_bot and message_handler and content:
|
||||||
# 在消息处理器的事件循环中发送消息
|
# 在消息处理器的事件循环中发送消息
|
||||||
@@ -786,16 +866,16 @@ class BackendClient:
|
|||||||
# 获取消息处理器的事件循环
|
# 获取消息处理器的事件循环
|
||||||
loop = message_handler._loop
|
loop = message_handler._loop
|
||||||
if loop and not loop.is_closed():
|
if loop and not loop.is_closed():
|
||||||
# 在事件循环中执行发送
|
# 在事件循环中执行发送(传递msg_type参数)
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
message_handler.send_message_external(recv_pin, content),
|
message_handler.send_message_external(recv_pin, content, msg_type),
|
||||||
loop
|
loop
|
||||||
)
|
)
|
||||||
# 等待结果
|
# 等待结果
|
||||||
try:
|
try:
|
||||||
result = future.result(timeout=5)
|
result = future.result(timeout=30) # 图片/视频需要更长时间
|
||||||
if result:
|
if result:
|
||||||
print(f"[DY Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
|
print(f"[DY Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}")
|
||||||
else:
|
else:
|
||||||
print(f"[DY Forward] 转发失败: 消息处理器返回False")
|
print(f"[DY Forward] 转发失败: 消息处理器返回False")
|
||||||
except Exception as fe:
|
except Exception as fe:
|
||||||
@@ -829,8 +909,15 @@ class BackendClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[QN Forward] 转发失败: {e}")
|
print(f"[QN Forward] 转发失败: {e}")
|
||||||
|
|
||||||
def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str):
|
def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"):
|
||||||
"""转发消息到拼多多平台"""
|
"""转发消息到拼多多平台
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store_id: 店铺ID
|
||||||
|
recv_pin: 接收者ID
|
||||||
|
content: 消息内容(文本或图片URL)
|
||||||
|
msg_type: 消息类型(text/image/video)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
|
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
|
||||||
pdd_mgr = PDDWSManager()
|
pdd_mgr = PDDWSManager()
|
||||||
@@ -846,22 +933,22 @@ class BackendClient:
|
|||||||
loop = platform_info.get('loop')
|
loop = platform_info.get('loop')
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"[PDD Forward] shop_key={shop_key} has_pdd_instance={bool(pdd_instance)} has_loop={bool(loop)} recv_pin={recv_pin}")
|
f"[PDD Forward] shop_key={shop_key} has_pdd_instance={bool(pdd_instance)} has_loop={bool(loop)} recv_pin={recv_pin} msg_type={msg_type}")
|
||||||
|
|
||||||
if pdd_instance and loop and content:
|
if pdd_instance and loop and content:
|
||||||
# 在拼多多实例的事件循环中发送消息
|
# 在拼多多实例的事件循环中发送消息
|
||||||
def send_in_loop():
|
def send_in_loop():
|
||||||
try:
|
try:
|
||||||
# 在事件循环中执行发送
|
# 在事件循环中执行发送(传递msg_type参数)
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
pdd_instance.send_message_external(recv_pin, content),
|
pdd_instance.send_message_external(recv_pin, content, msg_type),
|
||||||
loop
|
loop
|
||||||
)
|
)
|
||||||
# 等待结果
|
# 等待结果
|
||||||
try:
|
try:
|
||||||
result = future.result(timeout=10) # 拼多多可能需要更长时间
|
result = future.result(timeout=10) # 拼多多可能需要更长时间
|
||||||
if result:
|
if result:
|
||||||
print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}")
|
print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, type={msg_type}, content_len={len(content)}")
|
||||||
else:
|
else:
|
||||||
print(f"[PDD Forward] 转发失败: 拼多多实例返回False")
|
print(f"[PDD Forward] 转发失败: 拼多多实例返回False")
|
||||||
except Exception as fe:
|
except Exception as fe:
|
||||||
@@ -1150,10 +1237,11 @@ class BackendClient:
|
|||||||
self.get_store()
|
self.get_store()
|
||||||
|
|
||||||
def _handle_login(self, message: Dict[str, Any]):
|
def _handle_login(self, message: Dict[str, Any]):
|
||||||
"""处理平台登录消息(新版:type=login, cookies/login_params, store_id, platform_name)"""
|
"""处理平台登录消息(新版:type=login, cookies/login_params, store_id, platform_name, store_name)"""
|
||||||
cookies = message.get('cookies', '')
|
cookies = message.get('cookies', '')
|
||||||
store_id = message.get('store_id', '')
|
store_id = message.get('store_id', '')
|
||||||
platform_name = message.get('platform_name', '')
|
platform_name = message.get('platform_name', '')
|
||||||
|
store_name = message.get('store_name', '') # 新增:获取店铺名称
|
||||||
content = message.get('content', '')
|
content = message.get('content', '')
|
||||||
data = message.get('data', {})
|
data = message.get('data', {})
|
||||||
|
|
||||||
@@ -1162,17 +1250,18 @@ class BackendClient:
|
|||||||
(("拼多多登录" in content and data.get('login_params')) or
|
(("拼多多登录" in content and data.get('login_params')) or
|
||||||
("抖音登录" in content and data.get('login_flow')))):
|
("抖音登录" in content and data.get('login_flow')))):
|
||||||
# 登录参数模式 - 传递完整的消息JSON给处理器
|
# 登录参数模式 - 传递完整的消息JSON给处理器
|
||||||
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
|
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_name or store_id}, 类型={content}")
|
||||||
if self.login_callback:
|
if self.login_callback:
|
||||||
# 传递完整的JSON消息,让处理器来解析登录参数
|
# 传递完整的JSON消息,让处理器来解析登录参数
|
||||||
import json
|
import json
|
||||||
full_message = json.dumps(message)
|
full_message = json.dumps(message)
|
||||||
self.login_callback(platform_name, store_id, full_message)
|
self.login_callback(platform_name, store_id, full_message, store_name)
|
||||||
else:
|
else:
|
||||||
# 普通Cookie模式
|
# 普通Cookie模式
|
||||||
print(f"收到登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
|
print(
|
||||||
|
f"收到登录指令: 平台={platform_name}, 店铺={store_name or store_id}, cookies_len={len(cookies) if cookies else 0}")
|
||||||
if self.login_callback:
|
if self.login_callback:
|
||||||
self.login_callback(platform_name, store_id, cookies)
|
self.login_callback(platform_name, store_id, cookies, store_name)
|
||||||
|
|
||||||
def _handle_error_message(self, message: Dict[str, Any]):
|
def _handle_error_message(self, message: Dict[str, Any]):
|
||||||
"""处理错误消息"""
|
"""处理错误消息"""
|
||||||
@@ -1229,6 +1318,29 @@ class BackendClient:
|
|||||||
if self.version_callback:
|
if self.version_callback:
|
||||||
self.version_callback(message)
|
self.version_callback(message)
|
||||||
|
|
||||||
|
def _handle_disconnect(self, message: Dict[str, Any]):
|
||||||
|
"""处理被踢下线消息"""
|
||||||
|
disconnect_message = message.get('content', '您的账号在其他设备登录,当前连接已断开')
|
||||||
|
|
||||||
|
print(f"[断开] 收到后端断开通知: {disconnect_message}")
|
||||||
|
|
||||||
|
# 停止重连机制(不再尝试重连)
|
||||||
|
self.should_stop = True
|
||||||
|
|
||||||
|
# 触发断开回调
|
||||||
|
if self.disconnect_callback:
|
||||||
|
self.disconnect_callback(disconnect_message)
|
||||||
|
|
||||||
|
def _handle_balance_insufficient(self, message: Dict[str, Any]):
|
||||||
|
"""处理余额不足消息"""
|
||||||
|
balance_message = message.get('content', '所有平台已断开。请充值条数后,重新连接')
|
||||||
|
|
||||||
|
self._log(f"⚠️ 余额不足: {balance_message}", "WARNING")
|
||||||
|
|
||||||
|
# 触发余额不足回调(在主线程中处理断开逻辑)
|
||||||
|
if self.balance_insufficient_callback:
|
||||||
|
self.balance_insufficient_callback(balance_message)
|
||||||
|
|
||||||
# ==================== 辅助方法 ====================
|
# ==================== 辅助方法 ====================
|
||||||
|
|
||||||
def set_token(self, token: str):
|
def set_token(self, token: str):
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import Optional, Callable
|
|||||||
import threading
|
import threading
|
||||||
import asyncio
|
import asyncio
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
# 创建新的后端客户端
|
# 创建新的后端客户端
|
||||||
from WebSocket.BackendClient import BackendClient
|
from WebSocket.BackendClient import BackendClient
|
||||||
from Utils.JD.JdUtils import JDListenerForGUI as JDListenerForGUI_WS
|
from Utils.JD.JdUtils import JDListenerForGUI as JDListenerForGUI_WS
|
||||||
@@ -11,6 +13,12 @@ from Utils.Dy.DyUtils import DouYinListenerForGUI as DYListenerForGUI_WS
|
|||||||
from Utils.Pdd.PddUtils import PddListenerForGUI as PDDListenerForGUI_WS
|
from Utils.Pdd.PddUtils import PddListenerForGUI as PDDListenerForGUI_WS
|
||||||
from Utils.QianNiu.QianNiuUtils import QianNiuListenerForGUI as QNListenerForGUI_WS
|
from Utils.QianNiu.QianNiuUtils import QianNiuListenerForGUI as QNListenerForGUI_WS
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformConnectionSignals(QObject):
|
||||||
|
"""平台连接信号(线程安全)"""
|
||||||
|
platform_connected = pyqtSignal(str, str) # (platform_name, store_id)
|
||||||
|
|
||||||
|
|
||||||
_backend_client = None
|
_backend_client = None
|
||||||
|
|
||||||
|
|
||||||
@@ -36,16 +44,27 @@ class WebSocketManager:
|
|||||||
self.gui_update_callback = None
|
self.gui_update_callback = None
|
||||||
self.platform_listeners = {} # 存储各平台的监听器
|
self.platform_listeners = {} # 存储各平台的监听器
|
||||||
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
|
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
|
||||||
|
|
||||||
|
# 平台连接信号(线程安全)
|
||||||
|
self.platform_signals = PlatformConnectionSignals()
|
||||||
|
self.platform_signals.platform_connected.connect(self._on_platform_signal_received)
|
||||||
|
|
||||||
|
# 🔥 会话超时信号(用于拼多多会话过期重试)
|
||||||
|
self.session_timeout_signal = None
|
||||||
|
|
||||||
self.callbacks = {
|
self.callbacks = {
|
||||||
'log': None,
|
'log': None,
|
||||||
'success': None,
|
'success': None,
|
||||||
'error': None,
|
'error': None,
|
||||||
'platform_connected': None,
|
'platform_connected': None,
|
||||||
'token_error': None,
|
'token_error': None,
|
||||||
|
'disconnect': None, # 新增:被踢下线回调
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
|
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
|
||||||
platform_connected: Callable = None, token_error: Callable = None):
|
platform_connected: Callable = None, platform_disconnected: Callable = None,
|
||||||
|
token_error: Callable = None, disconnect: Callable = None,
|
||||||
|
balance_insufficient: Callable = None):
|
||||||
"""设置回调函数"""
|
"""设置回调函数"""
|
||||||
if log:
|
if log:
|
||||||
self.callbacks['log'] = log
|
self.callbacks['log'] = log
|
||||||
@@ -55,8 +74,14 @@ class WebSocketManager:
|
|||||||
self.callbacks['error'] = error
|
self.callbacks['error'] = error
|
||||||
if platform_connected: # ← 新增
|
if platform_connected: # ← 新增
|
||||||
self.callbacks['platform_connected'] = platform_connected
|
self.callbacks['platform_connected'] = platform_connected
|
||||||
|
if platform_disconnected:
|
||||||
|
self.callbacks['platform_disconnected'] = platform_disconnected
|
||||||
if token_error:
|
if token_error:
|
||||||
self.callbacks['token_error'] = token_error
|
self.callbacks['token_error'] = token_error
|
||||||
|
if disconnect:
|
||||||
|
self.callbacks['disconnect'] = disconnect
|
||||||
|
if balance_insufficient:
|
||||||
|
self.callbacks['balance_insufficient'] = balance_insufficient
|
||||||
|
|
||||||
def _log(self, message: str, level: str = "INFO"):
|
def _log(self, message: str, level: str = "INFO"):
|
||||||
"""内部日志方法"""
|
"""内部日志方法"""
|
||||||
@@ -65,8 +90,16 @@ class WebSocketManager:
|
|||||||
else:
|
else:
|
||||||
print(f"[{level}] {message}")
|
print(f"[{level}] {message}")
|
||||||
|
|
||||||
|
def _on_platform_signal_received(self, platform_name: str, store_id: str):
|
||||||
|
"""接收平台连接信号(在主线程中执行)"""
|
||||||
|
try:
|
||||||
|
self._log(f"📡 收到平台连接信号: {platform_name} (店铺:{store_id})", "INFO")
|
||||||
|
self._notify_platform_connected(platform_name)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"处理平台连接信号失败: {e}", "ERROR")
|
||||||
|
|
||||||
def _notify_platform_connected(self, platform_name: str):
|
def _notify_platform_connected(self, platform_name: str):
|
||||||
"""通知GUI平台连接成功"""
|
"""通知GUI平台连接成功(仅在主线程中调用)"""
|
||||||
try:
|
try:
|
||||||
if platform_name not in self.connected_platforms:
|
if platform_name not in self.connected_platforms:
|
||||||
self.connected_platforms.append(platform_name)
|
self.connected_platforms.append(platform_name)
|
||||||
@@ -77,16 +110,103 @@ class WebSocketManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"通知平台连接失败: {e}", "ERROR")
|
self._log(f"通知平台连接失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def notify_platform_kicked(self, platform_name: str, store_name: str, reason: str = "账号在其他设备登录",
|
||||||
|
store_id: str = None):
|
||||||
|
"""通知GUI平台被踢下线(供平台监听器调用)"""
|
||||||
|
try:
|
||||||
|
self._log(f"⚠️ 平台被踢下线: {platform_name} - {store_name}, 原因: {reason}", "WARNING")
|
||||||
|
|
||||||
|
# 从连接列表中移除
|
||||||
|
if platform_name in self.connected_platforms:
|
||||||
|
self.connected_platforms.remove(platform_name)
|
||||||
|
|
||||||
|
# 🔥 发送平台断开消息给后端(status=false)
|
||||||
|
if store_id and self.backend_client:
|
||||||
|
try:
|
||||||
|
disconnect_message = {
|
||||||
|
"type": "connect_message",
|
||||||
|
"store_id": store_id,
|
||||||
|
"status": False,
|
||||||
|
"cookies": ""
|
||||||
|
}
|
||||||
|
self.backend_client.send_message(disconnect_message)
|
||||||
|
self._log(f"✅ 已通知后端 {platform_name} 平台断开: {store_id}", "INFO")
|
||||||
|
except Exception as send_error:
|
||||||
|
self._log(f"❌ 发送平台断开消息失败: {send_error}", "ERROR")
|
||||||
|
|
||||||
|
# 通知GUI显示弹窗
|
||||||
|
if self.callbacks['platform_disconnected']:
|
||||||
|
self.callbacks['platform_disconnected'](platform_name, store_name, reason)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"通知平台断开失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
def disconnect_all_async(self):
|
||||||
|
"""异步断开所有连接(余额不足时调用)- 不阻塞当前线程"""
|
||||||
|
try:
|
||||||
|
self._log("🔴 收到余额不足通知,开始断开所有连接...", "ERROR")
|
||||||
|
|
||||||
|
# 1. 断开所有平台连接
|
||||||
|
if self.platform_listeners:
|
||||||
|
platform_count = len(self.platform_listeners)
|
||||||
|
self._log(f"正在断开 {platform_count} 个平台连接...", "INFO")
|
||||||
|
|
||||||
|
# 复制键列表,避免遍历时修改字典
|
||||||
|
platform_keys = list(self.platform_listeners.keys())
|
||||||
|
|
||||||
|
for key in platform_keys:
|
||||||
|
try:
|
||||||
|
listener_info = self.platform_listeners.get(key)
|
||||||
|
if listener_info:
|
||||||
|
platform_type = listener_info.get('platform', '')
|
||||||
|
self._log(f"断开 {platform_type} 平台连接: {key}", "INFO")
|
||||||
|
|
||||||
|
# 移除连接记录
|
||||||
|
self.platform_listeners.pop(key, None)
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"断开平台连接失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
self._log(f"✅ 已断开所有 {platform_count} 个平台连接", "INFO")
|
||||||
|
|
||||||
|
# 清空连接列表
|
||||||
|
self.connected_platforms.clear()
|
||||||
|
|
||||||
|
# 2. 延迟断开后端连接(在新线程中执行,避免阻塞)
|
||||||
|
import threading
|
||||||
|
def _delayed_backend_disconnect():
|
||||||
|
try:
|
||||||
|
import time
|
||||||
|
time.sleep(0.5) # 延迟0.5秒,确保上面的操作完成
|
||||||
|
|
||||||
|
if self.backend_client:
|
||||||
|
self.backend_client.should_stop = True
|
||||||
|
self.backend_client.is_connected = False
|
||||||
|
self._log("✅ 已标记后端连接为断开状态", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"断开后端连接失败: {e}", "ERROR")
|
||||||
|
|
||||||
|
disconnect_thread = threading.Thread(target=_delayed_backend_disconnect, daemon=True)
|
||||||
|
disconnect_thread.start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"断开所有连接失败: {e}", "ERROR")
|
||||||
|
|
||||||
def connect_backend(self, token: str) -> bool:
|
def connect_backend(self, token: str) -> bool:
|
||||||
"""连接后端WebSocket"""
|
"""连接后端WebSocket"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# 1 保存token到配置
|
# 🔥 根据配置决定是否保存token
|
||||||
try:
|
# 生产模式:保存token,方便用户下次自动加载
|
||||||
from config import set_saved_token
|
# 测试模式:不保存,避免多实例冲突
|
||||||
set_saved_token(token)
|
import config as cfg
|
||||||
except Exception:
|
if not cfg.is_multi_instance_mode():
|
||||||
pass
|
try:
|
||||||
|
from config import set_saved_token
|
||||||
|
set_saved_token(token)
|
||||||
|
self._log("生产模式:已保存token到配置文件", "INFO")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"保存token失败: {e}", "WARNING")
|
||||||
|
else:
|
||||||
|
self._log("测试模式:不保存token(支持多实例运行)", "INFO")
|
||||||
|
|
||||||
# 2 获取或创建后端客户端
|
# 2 获取或创建后端客户端
|
||||||
backend = get_backend_client()
|
backend = get_backend_client()
|
||||||
@@ -115,19 +235,36 @@ class WebSocketManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"成功回调执行失败: {e}", "ERROR")
|
self._log(f"成功回调执行失败: {e}", "ERROR")
|
||||||
|
|
||||||
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
|
def _on_backend_login(platform_name: str, store_id: str, cookies: str, store_name: str = ""):
|
||||||
|
store_display = store_name or store_id
|
||||||
self._log(
|
self._log(
|
||||||
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
|
f"收到后端登录指令: 平台={platform_name}, 店铺={store_display}, 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, store_name)
|
||||||
|
|
||||||
def _on_token_error(error_content: str):
|
def _on_token_error(error_content: str):
|
||||||
self._log(f"Token验证失败: {error_content}", "ERROR")
|
self._log(f"Token验证失败: {error_content}", "ERROR")
|
||||||
if self.callbacks['token_error']:
|
if self.callbacks['token_error']:
|
||||||
self.callbacks['token_error'](error_content)
|
self.callbacks['token_error'](error_content)
|
||||||
|
|
||||||
|
def _on_disconnect(disconnect_msg: str):
|
||||||
|
self._log(f"被踢下线: {disconnect_msg}", "WARNING")
|
||||||
|
if self.callbacks['disconnect']:
|
||||||
|
self.callbacks['disconnect'](disconnect_msg)
|
||||||
|
|
||||||
|
def _on_balance_insufficient(balance_msg: str):
|
||||||
|
"""余额不足回调"""
|
||||||
|
if self.callbacks['balance_insufficient']:
|
||||||
|
self.callbacks['balance_insufficient'](balance_msg)
|
||||||
|
|
||||||
|
def _on_log(message: str, level: str = "INFO"):
|
||||||
|
"""Backend client log callback"""
|
||||||
|
self._log(message, level)
|
||||||
|
|
||||||
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
|
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
|
||||||
token_error=_on_token_error)
|
token_error=_on_token_error, disconnect=_on_disconnect,
|
||||||
|
balance_insufficient=_on_balance_insufficient,
|
||||||
|
log=_on_log)
|
||||||
|
|
||||||
if not backend.is_connected:
|
if not backend.is_connected:
|
||||||
backend.connect()
|
backend.connect()
|
||||||
@@ -141,11 +278,12 @@ class WebSocketManager:
|
|||||||
|
|
||||||
backend = BackendClient.from_exe_token(token)
|
backend = BackendClient.from_exe_token(token)
|
||||||
|
|
||||||
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
|
def _on_backend_login(platform_name: str, store_id: str, cookies: str, store_name: str = ""):
|
||||||
|
store_display = store_name or store_id
|
||||||
self._log(
|
self._log(
|
||||||
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
|
f"收到后端登录指令: 平台={platform_name}, 店铺={store_display}, 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, store_name)
|
||||||
|
|
||||||
def _on_backend_success():
|
def _on_backend_success():
|
||||||
try:
|
try:
|
||||||
@@ -162,7 +300,24 @@ class WebSocketManager:
|
|||||||
if self.callbacks['token_error']:
|
if self.callbacks['token_error']:
|
||||||
self.callbacks['token_error'](error_content)
|
self.callbacks['token_error'](error_content)
|
||||||
|
|
||||||
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error)
|
def _on_disconnect(disconnect_msg: str):
|
||||||
|
self._log(f"被踢下线: {disconnect_msg}", "ERROR")
|
||||||
|
if self.callbacks['disconnect']:
|
||||||
|
self.callbacks['disconnect'](disconnect_msg)
|
||||||
|
|
||||||
|
def _on_balance_insufficient(balance_msg: str):
|
||||||
|
"""余额不足回调"""
|
||||||
|
if self.callbacks['balance_insufficient']:
|
||||||
|
self.callbacks['balance_insufficient'](balance_msg)
|
||||||
|
|
||||||
|
def _on_log(message: str, level: str = "INFO"):
|
||||||
|
"""Backend client log callback"""
|
||||||
|
self._log(message, level)
|
||||||
|
|
||||||
|
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success,
|
||||||
|
token_error=_on_token_error, disconnect=_on_disconnect,
|
||||||
|
balance_insufficient=_on_balance_insufficient,
|
||||||
|
log=_on_log)
|
||||||
backend.connect()
|
backend.connect()
|
||||||
|
|
||||||
set_backend_client(backend)
|
set_backend_client(backend)
|
||||||
@@ -176,17 +331,106 @@ class WebSocketManager:
|
|||||||
self.callbacks['error'](str(e))
|
self.callbacks['error'](str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str):
|
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str, store_name: str = ""):
|
||||||
"""处理平台登录请求"""
|
"""处理平台登录请求"""
|
||||||
try:
|
try:
|
||||||
# 🔥 检查并清理当前店铺的旧连接
|
# 🔥 检查并断开当前店铺的旧连接(策略B:先断开旧连接,再建立新连接)
|
||||||
store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式
|
store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式
|
||||||
keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)]
|
keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)]
|
||||||
|
|
||||||
if keys_to_remove:
|
if keys_to_remove:
|
||||||
self._log(f"🔄 检测到店铺 {store_id} 重连,清理 {len(keys_to_remove)} 个旧连接", "INFO")
|
self._log(f"🔄 检测到店铺 {store_id} 重复登录,断开 {len(keys_to_remove)} 个旧连接", "INFO")
|
||||||
|
|
||||||
for key in keys_to_remove:
|
for key in keys_to_remove:
|
||||||
|
listener_info = self.platform_listeners.get(key)
|
||||||
|
if listener_info:
|
||||||
|
platform_type = listener_info.get('platform', '')
|
||||||
|
|
||||||
|
# 从各平台的 WebsocketManager 中获取连接并关闭WebSocket
|
||||||
|
try:
|
||||||
|
if platform_type == "京东":
|
||||||
|
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
|
||||||
|
jd_mgr = JDWSManager()
|
||||||
|
conn_info = jd_mgr.get_connection(key)
|
||||||
|
if conn_info and conn_info.get('platform'):
|
||||||
|
ws = conn_info['platform'].get('ws')
|
||||||
|
if ws and hasattr(ws, 'close'):
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = conn_info['platform'].get('loop')
|
||||||
|
if loop and not loop.is_closed():
|
||||||
|
asyncio.run_coroutine_threadsafe(ws.close(), loop)
|
||||||
|
self._log(f"✅ 已关闭京东WebSocket连接: {key}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
jd_mgr.remove_connection(key)
|
||||||
|
self._log(f"✅ 已从京东管理器移除连接: {key}", "DEBUG")
|
||||||
|
|
||||||
|
elif platform_type == "抖音":
|
||||||
|
from Utils.Dy.DyUtils import DouYinWebsocketManager as DYWSManager
|
||||||
|
dy_mgr = DYWSManager()
|
||||||
|
conn_info = dy_mgr.get_connection(key)
|
||||||
|
if conn_info and conn_info.get('platform'):
|
||||||
|
ws = conn_info['platform'].get('ws')
|
||||||
|
if ws and hasattr(ws, 'close'):
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = conn_info['platform'].get('loop')
|
||||||
|
if loop and not loop.is_closed():
|
||||||
|
asyncio.run_coroutine_threadsafe(ws.close(), loop)
|
||||||
|
self._log(f"✅ 已关闭抖音WebSocket连接: {key}", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
dy_mgr.remove_connection(key)
|
||||||
|
self._log(f"✅ 已从抖音管理器移除连接: {key}", "DEBUG")
|
||||||
|
|
||||||
|
elif platform_type == "千牛":
|
||||||
|
from Utils.QianNiu.QianNiuUtils import QianNiuWebsocketManager as QNWSManager
|
||||||
|
qn_mgr = QNWSManager()
|
||||||
|
qn_mgr.remove_connection(key)
|
||||||
|
self._log(f"✅ 已从千牛管理器移除连接: {key}", "DEBUG")
|
||||||
|
|
||||||
|
elif platform_type == "拼多多":
|
||||||
|
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
|
||||||
|
pdd_mgr = PDDWSManager()
|
||||||
|
conn_info = pdd_mgr.get_connection(key)
|
||||||
|
|
||||||
|
# 清理旧listener的定时器(避免遗留定时器触发)
|
||||||
|
old_listener = listener_info.get('listener')
|
||||||
|
if old_listener and hasattr(old_listener, 'pdd_bot') and old_listener.pdd_bot:
|
||||||
|
try:
|
||||||
|
old_listener.pdd_bot._cancel_session_timeout_check()
|
||||||
|
self._log(f"✅ [PDD] 旧监听器的定时器已清理", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if conn_info and conn_info.get('platform'):
|
||||||
|
# 关闭WebSocket连接
|
||||||
|
ws = conn_info['platform'].get('ws')
|
||||||
|
if ws and hasattr(ws, 'close'):
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = conn_info['platform'].get('loop')
|
||||||
|
if loop and not loop.is_closed():
|
||||||
|
asyncio.run_coroutine_threadsafe(ws.close(), loop)
|
||||||
|
self._log(f"✅ 已关闭拼多多WebSocket连接: {key}", "DEBUG")
|
||||||
|
except Exception as ws_e:
|
||||||
|
self._log(f"⚠️ 关闭WebSocket时出错: {ws_e}", "DEBUG")
|
||||||
|
pdd_mgr.remove_connection(key)
|
||||||
|
self._log(f"✅ 已从拼多多管理器移除连接: {key}", "DEBUG")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"⚠️ 移除{platform_type}连接时出错: {e}", "WARNING")
|
||||||
|
|
||||||
|
# 从监听器字典中移除
|
||||||
self.platform_listeners.pop(key, None)
|
self.platform_listeners.pop(key, None)
|
||||||
|
|
||||||
|
# 给WebSocket一点时间完全关闭
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
self._log(f"✅ 旧连接已全部断开,准备建立新连接", "INFO")
|
||||||
|
|
||||||
# 平台名称映射
|
# 平台名称映射
|
||||||
platform_map = {
|
platform_map = {
|
||||||
"淘宝": "千牛",
|
"淘宝": "千牛",
|
||||||
@@ -211,16 +455,16 @@ class WebSocketManager:
|
|||||||
if cookies == "login_success":
|
if cookies == "login_success":
|
||||||
self._log("⚠️ 千牛平台收到空cookies,但允许启动监听器", "WARNING")
|
self._log("⚠️ 千牛平台收到空cookies,但允许启动监听器", "WARNING")
|
||||||
cookies = "" # 清空cookies,千牛不需要真实cookies
|
cookies = "" # 清空cookies,千牛不需要真实cookies
|
||||||
self._start_qianniu_listener(store_id, cookies)
|
self._start_qianniu_listener(store_id, cookies, store_name)
|
||||||
|
|
||||||
elif normalized_platform == "京东":
|
elif normalized_platform == "京东":
|
||||||
self._start_jd_listener(store_id, cookies)
|
self._start_jd_listener(store_id, cookies, store_name)
|
||||||
|
|
||||||
elif normalized_platform == "抖音":
|
elif normalized_platform == "抖音":
|
||||||
self._start_douyin_listener(store_id, cookies)
|
self._start_douyin_listener(store_id, cookies, store_name)
|
||||||
|
|
||||||
elif normalized_platform == "拼多多":
|
elif normalized_platform == "拼多多":
|
||||||
self._start_pdd_listener(store_id, cookies)
|
self._start_pdd_listener(store_id, cookies, store_name)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._log(f"❌ 不支持的平台: {platform_name}", "ERROR")
|
self._log(f"❌ 不支持的平台: {platform_name}", "ERROR")
|
||||||
@@ -245,13 +489,20 @@ class WebSocketManager:
|
|||||||
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
|
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
|
||||||
|
|
||||||
def _on_update_available(self, latest_version, download_url):
|
def _on_update_available(self, latest_version, download_url):
|
||||||
"""发现新版本时的处理"""
|
"""发现新版本时的处理(在子线程中调用)"""
|
||||||
self._log(f"🔔 发现新版本 {latest_version}", "INFO")
|
self._log(f"🔔 发现新版本 {latest_version}", "INFO")
|
||||||
# 通知主GUI显示更新提醒
|
# 通知主GUI显示更新提醒(通过 Qt 信号机制,线程安全)
|
||||||
if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
|
if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
|
||||||
self.gui_update_callback(latest_version, download_url)
|
try:
|
||||||
|
# 直接调用回调(回调内部使用信号机制调度到主线程)
|
||||||
|
self.gui_update_callback(latest_version, download_url)
|
||||||
|
self._log(f"✅ 已调用更新回调", "DEBUG")
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"❌ 调用更新回调失败: {e}", "ERROR")
|
||||||
|
import traceback
|
||||||
|
self._log(f"详细错误: {traceback.format_exc()}", "ERROR")
|
||||||
|
|
||||||
def _start_jd_listener(self, store_id: str, cookies: str):
|
def _start_jd_listener(self, store_id: str, cookies: str, store_name: str = ""):
|
||||||
"""启动京东平台监听"""
|
"""启动京东平台监听"""
|
||||||
try:
|
try:
|
||||||
def _runner():
|
def _runner():
|
||||||
@@ -269,7 +520,8 @@ class WebSocketManager:
|
|||||||
self.platform_listeners[f"京东:{store_id}"] = {
|
self.platform_listeners[f"京东:{store_id}"] = {
|
||||||
'thread': thread,
|
'thread': thread,
|
||||||
'platform': '京东',
|
'platform': '京东',
|
||||||
'store_id': store_id
|
'store_id': store_id,
|
||||||
|
'store_name': store_name # 保存店铺名称
|
||||||
}
|
}
|
||||||
|
|
||||||
# 上报连接状态给后端
|
# 上报连接状态给后端
|
||||||
@@ -300,7 +552,7 @@ class WebSocketManager:
|
|||||||
except Exception as send_e:
|
except Exception as send_e:
|
||||||
self._log(f"失败状态下报连接状态也失败: {send_e}", "ERROR")
|
self._log(f"失败状态下报连接状态也失败: {send_e}", "ERROR")
|
||||||
|
|
||||||
def _start_douyin_listener(self, store_id: str, cookies: str):
|
def _start_douyin_listener(self, store_id: str, cookies: str, store_name: str = ""):
|
||||||
"""启动抖音平台监听"""
|
"""启动抖音平台监听"""
|
||||||
try:
|
try:
|
||||||
def _runner():
|
def _runner():
|
||||||
@@ -318,14 +570,13 @@ class WebSocketManager:
|
|||||||
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
|
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
|
||||||
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
|
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
|
||||||
|
|
||||||
# 🔥 详细的结果分析(与拼多多完全一致)
|
# 详细的结果分析(仅日志记录,GUI 已在主线程中通知)
|
||||||
if result == "need_verification_code":
|
if result == "need_verification_code":
|
||||||
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
|
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
|
||||||
elif result == "verification_code_error":
|
elif result == "verification_code_error":
|
||||||
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
|
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
|
||||||
elif result:
|
elif result:
|
||||||
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
|
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
|
||||||
self._notify_platform_connected("抖音")
|
|
||||||
else:
|
else:
|
||||||
self._log("❌ [DY] 登录失败", "ERROR")
|
self._log("❌ [DY] 登录失败", "ERROR")
|
||||||
else:
|
else:
|
||||||
@@ -350,10 +601,9 @@ class WebSocketManager:
|
|||||||
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
|
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
|
||||||
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
|
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
|
||||||
|
|
||||||
# Cookie启动成功时也要通知GUI
|
# Cookie启动成功时记录日志(GUI 已在主线程中通知)
|
||||||
if result:
|
if result:
|
||||||
self._log("✅ [DY] Cookie启动成功,平台连接已建立", "SUCCESS")
|
self._log("✅ [DY] Cookie启动成功,平台连接已建立", "SUCCESS")
|
||||||
self._notify_platform_connected("抖音")
|
|
||||||
|
|
||||||
# 🔥 移除:不再在backend_singleton中发送connect_message
|
# 🔥 移除:不再在backend_singleton中发送connect_message
|
||||||
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送,与拼多多保持一致
|
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送,与拼多多保持一致
|
||||||
@@ -373,13 +623,17 @@ class WebSocketManager:
|
|||||||
'thread': thread,
|
'thread': thread,
|
||||||
'platform': '抖音',
|
'platform': '抖音',
|
||||||
'store_id': store_id,
|
'store_id': store_id,
|
||||||
|
'store_name': store_name # 保存店铺名称
|
||||||
}
|
}
|
||||||
|
|
||||||
# 更新监听器状态
|
# 更新监听器状态
|
||||||
if f"抖音:{store_id}" in self.platform_listeners:
|
if f"抖音:{store_id}" in self.platform_listeners:
|
||||||
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
|
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
|
||||||
|
|
||||||
|
# ✅ 临时方案:启动后立即通知(因为 DY 监听器也会阻塞)
|
||||||
|
# DY 内部会处理验证码流程,失败时会向后端上报相应状态
|
||||||
self._log("已启动抖音平台监听", "SUCCESS")
|
self._log("已启动抖音平台监听", "SUCCESS")
|
||||||
|
self._notify_platform_connected("抖音") # ← 立即通知
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
|
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
|
||||||
@@ -387,7 +641,7 @@ class WebSocketManager:
|
|||||||
# 🔥 移除:确保失败时也不在这里上报状态
|
# 🔥 移除:确保失败时也不在这里上报状态
|
||||||
# 失败状态应该在DyLogin中处理,与拼多多保持一致
|
# 失败状态应该在DyLogin中处理,与拼多多保持一致
|
||||||
|
|
||||||
def _start_qianniu_listener(self, store_id: str, cookies: str):
|
def _start_qianniu_listener(self, store_id: str, cookies: str, store_name: str = ""):
|
||||||
"""启动千牛平台监听(单连接多店铺架构)"""
|
"""启动千牛平台监听(单连接多店铺架构)"""
|
||||||
try:
|
try:
|
||||||
# 获取用户token(从后端客户端获取)
|
# 获取用户token(从后端客户端获取)
|
||||||
@@ -416,6 +670,7 @@ class WebSocketManager:
|
|||||||
'thread': thread,
|
'thread': thread,
|
||||||
'platform': '千牛',
|
'platform': '千牛',
|
||||||
'store_id': store_id,
|
'store_id': store_id,
|
||||||
|
'store_name': store_name, # 保存店铺名称
|
||||||
'exe_token': exe_token
|
'exe_token': exe_token
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,49 +702,46 @@ class WebSocketManager:
|
|||||||
except Exception as send_e:
|
except Exception as send_e:
|
||||||
self._log(f"失败状态下报千牛平台连接状态也失败: {send_e}", "ERROR")
|
self._log(f"失败状态下报千牛平台连接状态也失败: {send_e}", "ERROR")
|
||||||
|
|
||||||
def _start_pdd_listener(self, store_id: str, data: str):
|
def _start_pdd_listener(self, store_id: str, data: str, store_name: str = ""):
|
||||||
"""启动拼多多平台监听"""
|
"""启动拼多多平台监听"""
|
||||||
try:
|
try:
|
||||||
|
# 在创建新实例前,清理旧实例的定时器(避免遗留定时器触发)
|
||||||
|
shop_key = f"拼多多:{store_id}"
|
||||||
|
old_listener_info = self.platform_listeners.get(shop_key)
|
||||||
|
if old_listener_info:
|
||||||
|
try:
|
||||||
|
old_listener = old_listener_info.get('listener')
|
||||||
|
if old_listener and hasattr(old_listener, 'pdd_bot') and old_listener.pdd_bot:
|
||||||
|
old_listener.pdd_bot._cancel_session_timeout_check()
|
||||||
|
self._log(f"✅ 已清理旧监听器定时器", "DEBUG")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _runner():
|
def _runner():
|
||||||
try:
|
try:
|
||||||
self._log("🚀 开始创建拼多多监听器实例", "DEBUG")
|
# 传递session_timeout_signal(用于会话过期重试)
|
||||||
listener = PDDListenerForGUI_WS(log_callback=self._log)
|
listener = PDDListenerForGUI_WS(
|
||||||
self._log("✅ 拼多多监听器实例创建成功", "DEBUG")
|
log_callback=self._log,
|
||||||
|
session_timeout_signal=self.session_timeout_signal
|
||||||
|
)
|
||||||
|
|
||||||
|
# 立即保存listener引用到platform_listeners(用于后续清理定时器)
|
||||||
|
shop_key = f"拼多多:{store_id}"
|
||||||
|
if shop_key in self.platform_listeners:
|
||||||
|
self.platform_listeners[shop_key]['listener'] = listener
|
||||||
|
|
||||||
# 判断是登录参数还是Cookie
|
# 判断是登录参数还是Cookie
|
||||||
if self._is_pdd_login_params(data):
|
if self._is_pdd_login_params(data):
|
||||||
# 使用登录参数启动
|
# 使用登录参数启动
|
||||||
self._log("📋 使用登录参数启动拼多多监听器", "INFO")
|
|
||||||
self._log("🔄 开始执行 start_with_login_params", "DEBUG")
|
|
||||||
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=data))
|
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=data))
|
||||||
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
|
|
||||||
|
|
||||||
# 详细的结果分析
|
|
||||||
if result == "need_verification_code":
|
|
||||||
self._log("✅ [PDD] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
|
|
||||||
elif result == "verification_code_error":
|
|
||||||
self._log("⚠️ [PDD] 验证码错误,已发送错误通知给后端", "WARNING")
|
|
||||||
elif result:
|
|
||||||
self._log("✅ [PDD] 登录成功,平台连接已建立", "SUCCESS")
|
|
||||||
self._notify_platform_connected("拼多多")
|
|
||||||
else:
|
|
||||||
self._log("❌ [PDD] 登录失败", "ERROR")
|
|
||||||
else:
|
else:
|
||||||
# 使用Cookie启动(兼容旧方式)
|
# 使用Cookie启动(兼容旧方式)
|
||||||
self._log("🍪 使用Cookie启动拼多多监听器", "INFO")
|
|
||||||
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
|
|
||||||
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
|
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
|
||||||
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
|
|
||||||
|
|
||||||
# Cookie启动成功时也要通知GUI
|
|
||||||
if result:
|
|
||||||
self._log("✅ [PDD] Cookie启动成功,平台连接已建立", "SUCCESS")
|
|
||||||
self._notify_platform_connected("拼多多")
|
|
||||||
|
|
||||||
# 根据实际登录结果上报状态给后端
|
# 根据实际登录结果上报状态给后端
|
||||||
if self.backend_client and result not in ["need_verification_code", "verification_code_error",
|
if self.backend_client and result not in ["need_verification_code", "verification_code_error",
|
||||||
"login_failure"]:
|
"login_failure", "cookie_expired"]:
|
||||||
# 如果是特殊状态,说明通知已经在PddLogin中发送了,不需要重复发送
|
# 🔥 如果是特殊状态(包括cookie_expired),说明通知已经发送,不需要重复发送
|
||||||
try:
|
try:
|
||||||
message = {
|
message = {
|
||||||
"type": "connect_message",
|
"type": "connect_message",
|
||||||
@@ -508,6 +760,8 @@ class WebSocketManager:
|
|||||||
self._log("验证码错误,错误通知已由PddLogin发送,等待后端处理", "INFO")
|
self._log("验证码错误,错误通知已由PddLogin发送,等待后端处理", "INFO")
|
||||||
elif result == "login_failure":
|
elif result == "login_failure":
|
||||||
self._log("登录失败,失败通知已由PddLogin发送,等待后端处理", "INFO")
|
self._log("登录失败,失败通知已由PddLogin发送,等待后端处理", "INFO")
|
||||||
|
elif result == "cookie_expired":
|
||||||
|
self._log("🔄 Cookie已过期,过期通知已发送,等待后端重新下发Cookie", "INFO")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -541,9 +795,13 @@ class WebSocketManager:
|
|||||||
'thread': thread,
|
'thread': thread,
|
||||||
'platform': '拼多多',
|
'platform': '拼多多',
|
||||||
'store_id': store_id,
|
'store_id': store_id,
|
||||||
|
'store_name': store_name # 保存店铺名称
|
||||||
}
|
}
|
||||||
|
|
||||||
self._log("拼多多平台监听线程已启动,等待登录结果...", "INFO")
|
# ✅ 临时方案:启动后立即通知(因为 PDD 监听器会阻塞,无法通过返回值判断)
|
||||||
|
# PDD 内部会处理验证码流程,失败时会向后端上报 status=False
|
||||||
|
self._log("已启动拼多多平台监听", "SUCCESS")
|
||||||
|
self._notify_platform_connected("拼多多") # ← 立即通知
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"启动拼多多平台监听失败: {e}", "ERROR")
|
self._log(f"启动拼多多平台监听失败: {e}", "ERROR")
|
||||||
@@ -563,29 +821,19 @@ class WebSocketManager:
|
|||||||
def _is_pdd_login_params(self, data: str) -> bool:
|
def _is_pdd_login_params(self, data: str) -> bool:
|
||||||
"""判断是否为拼多多登录参数"""
|
"""判断是否为拼多多登录参数"""
|
||||||
try:
|
try:
|
||||||
self._log(f"🔍 [DEBUG] 检查是否为登录参数,数据长度: {len(data)}", "DEBUG")
|
|
||||||
self._log(f"🔍 [DEBUG] 数据前100字符: {data[:100]}", "DEBUG")
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
parsed_data = json.loads(data)
|
parsed_data = json.loads(data)
|
||||||
self._log(f"🔍 [DEBUG] JSON解析成功,键: {list(parsed_data.keys())}", "DEBUG")
|
|
||||||
|
|
||||||
login_params = parsed_data.get("data", {}).get("login_params", {})
|
login_params = parsed_data.get("data", {}).get("login_params", {})
|
||||||
self._log(f"🔍 [DEBUG] login_params存在: {bool(login_params)}", "DEBUG")
|
|
||||||
|
|
||||||
if not login_params:
|
if not login_params:
|
||||||
self._log("🔍 [DEBUG] login_params为空,返回False", "DEBUG")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查必需的登录参数字段
|
# 检查必需的登录参数字段
|
||||||
required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"]
|
required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"]
|
||||||
has_all_fields = all(field in login_params for field in required_fields)
|
has_all_fields = all(field in login_params for field in required_fields)
|
||||||
self._log(f"🔍 [DEBUG] 包含所有必需字段: {has_all_fields}", "DEBUG")
|
|
||||||
self._log(f"🔍 [DEBUG] 现有字段: {list(login_params.keys())}", "DEBUG")
|
|
||||||
|
|
||||||
return has_all_fields
|
return has_all_fields
|
||||||
except Exception as e:
|
except Exception:
|
||||||
self._log(f"🔍 [DEBUG] 解析失败: {e}", "DEBUG")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def send_message(self, message: dict):
|
def send_message(self, message: dict):
|
||||||
@@ -621,3 +869,4 @@ def get_websocket_manager() -> WebSocketManager:
|
|||||||
if _websocket_manager is None:
|
if _websocket_manager is None:
|
||||||
_websocket_manager = WebSocketManager()
|
_websocket_manager = WebSocketManager()
|
||||||
return _websocket_manager
|
return _websocket_manager
|
||||||
|
|
||||||
|
|||||||
489
auto_updater.py
Normal file
489
auto_updater.py
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
自动更新模块 - 支持后台下载、静默安装和自动重启
|
||||||
|
优化版:带重试、超时、自动重启功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDownloader(QThread):
|
||||||
|
"""
|
||||||
|
优化版更新下载器线程
|
||||||
|
特性:
|
||||||
|
- 自动重试3次
|
||||||
|
- 30秒网络超时
|
||||||
|
- 文件大小验证
|
||||||
|
- 进度实时更新
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 信号定义
|
||||||
|
progress = pyqtSignal(int, int) # (已下载字节, 总字节)
|
||||||
|
finished = pyqtSignal(str) # 下载完成,返回文件路径
|
||||||
|
error = pyqtSignal(str) # 下载失败,返回错误信息
|
||||||
|
retry_info = pyqtSignal(int, int) # 重试信息 (当前重试次数, 总重试次数)
|
||||||
|
|
||||||
|
def __init__(self, download_url, version, max_retries=3):
|
||||||
|
super().__init__()
|
||||||
|
self.download_url = download_url
|
||||||
|
self.version = version
|
||||||
|
self.max_retries = max_retries # 最大重试次数
|
||||||
|
self.is_cancelled = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""执行下载(带重试机制)"""
|
||||||
|
for attempt in range(self.max_retries):
|
||||||
|
try:
|
||||||
|
if attempt > 0:
|
||||||
|
# 显示重试信息
|
||||||
|
self.retry_info.emit(attempt + 1, self.max_retries)
|
||||||
|
time.sleep(2) # 等待2秒后重试
|
||||||
|
|
||||||
|
# 执行实际下载
|
||||||
|
self._download_file()
|
||||||
|
return # 下载成功,退出
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
print(f"[UpdateDownloader] 下载失败 (尝试 {attempt + 1}/{self.max_retries}): {error_msg}")
|
||||||
|
|
||||||
|
if self.is_cancelled:
|
||||||
|
# 用户取消,立即退出
|
||||||
|
self.error.emit("下载已取消")
|
||||||
|
return
|
||||||
|
|
||||||
|
if attempt < self.max_retries - 1:
|
||||||
|
# 还有重试机会,继续
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 所有重试都失败了
|
||||||
|
self.error.emit(f"下载失败(已重试{self.max_retries}次)\n错误: {error_msg}")
|
||||||
|
|
||||||
|
def _download_file(self):
|
||||||
|
"""实际下载逻辑(带超时和验证)"""
|
||||||
|
# 设置网络超时
|
||||||
|
socket.setdefaulttimeout(30) # 30秒超时
|
||||||
|
|
||||||
|
# 准备临时目录和文件名
|
||||||
|
temp_dir = tempfile.gettempdir()
|
||||||
|
filename = f"ShuiDi_AI_Assistant_Setup_v{self.version}.exe"
|
||||||
|
file_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
print(f"[UpdateDownloader] 开始下载到: {file_path}")
|
||||||
|
print(f"[UpdateDownloader] 下载地址: {self.download_url}")
|
||||||
|
|
||||||
|
# 删除旧文件(如果存在)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
print(f"[UpdateDownloader] 已删除旧文件")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UpdateDownloader] 删除旧文件失败: {e}")
|
||||||
|
|
||||||
|
# 下载文件(带进度回调)
|
||||||
|
def progress_callback(block_num, block_size, total_size):
|
||||||
|
if self.is_cancelled:
|
||||||
|
raise Exception("用户取消下载")
|
||||||
|
|
||||||
|
downloaded = block_num * block_size
|
||||||
|
# 避免超过总大小
|
||||||
|
if total_size > 0 and downloaded > total_size:
|
||||||
|
downloaded = total_size
|
||||||
|
|
||||||
|
self.progress.emit(downloaded, total_size)
|
||||||
|
|
||||||
|
# 执行下载
|
||||||
|
urllib.request.urlretrieve(
|
||||||
|
self.download_url,
|
||||||
|
file_path,
|
||||||
|
reporthook=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证下载的文件
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise Exception("下载的文件不存在")
|
||||||
|
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
print(f"[UpdateDownloader] 下载完成,文件大小: {file_size / (1024*1024):.2f} MB")
|
||||||
|
|
||||||
|
# 验证文件大小(至少1MB,避免下载了错误页面)
|
||||||
|
min_file_size = 1024 * 1024 # 1MB(正式环境)
|
||||||
|
# 🧪 测试小文件时可以临时改为:min_file_size = 10 * 1024 # 10KB
|
||||||
|
|
||||||
|
if file_size < min_file_size:
|
||||||
|
raise Exception(f"下载的文件大小异常: {file_size} bytes(可能不是有效的安装包)")
|
||||||
|
|
||||||
|
# 发送完成信号
|
||||||
|
self.finished.emit(file_path)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""取消下载"""
|
||||||
|
print("[UpdateDownloader] 用户取消下载")
|
||||||
|
self.is_cancelled = True
|
||||||
|
|
||||||
|
|
||||||
|
def run_as_admin(script_path):
|
||||||
|
"""
|
||||||
|
以管理员身份运行脚本(Windows UAC提升)
|
||||||
|
|
||||||
|
特点:
|
||||||
|
- 在GUI退出前弹出UAC提示
|
||||||
|
- 用户点击"允许"后,脚本以管理员身份运行
|
||||||
|
- 用户点击"取消",返回False
|
||||||
|
|
||||||
|
Args:
|
||||||
|
script_path: 批处理脚本路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True表示成功启动(用户点击允许),False表示失败或用户取消
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"[RunAsAdmin] 请求管理员权限执行: {script_path}")
|
||||||
|
|
||||||
|
# 使用 ShellExecute 以管理员身份运行
|
||||||
|
# "runas" 参数会触发UAC提示
|
||||||
|
ret = ctypes.windll.shell32.ShellExecuteW(
|
||||||
|
None, # hwnd: 父窗口句柄(None表示无父窗口)
|
||||||
|
"runas", # lpOperation: 以管理员身份运行
|
||||||
|
"cmd.exe", # lpFile: 要执行的程序
|
||||||
|
f'/c "{script_path}"', # lpParameters: 命令行参数
|
||||||
|
None, # lpDirectory: 工作目录(None表示当前目录)
|
||||||
|
0 # nShowCmd: 0=隐藏窗口
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回值说明:
|
||||||
|
# >32: 成功
|
||||||
|
# <=32: 失败(具体错误码)
|
||||||
|
# - 5: 用户拒绝UAC(点击"否")
|
||||||
|
# - 2: 文件未找到
|
||||||
|
# - 其他: 其他错误
|
||||||
|
|
||||||
|
if ret > 32:
|
||||||
|
print(f"[RunAsAdmin] ✅ 成功:用户已授权,脚本正在以管理员身份运行")
|
||||||
|
return True
|
||||||
|
elif ret == 5:
|
||||||
|
print(f"[RunAsAdmin] ⚠️ 用户取消了UAC授权(返回码: {ret})")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"[RunAsAdmin] ❌ 启动失败(返回码: {ret})")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[RunAsAdmin] ❌ 异常: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_update_and_restart(installer_path):
|
||||||
|
"""
|
||||||
|
简化版更新安装(文件替换方式,不使用NSIS安装)
|
||||||
|
|
||||||
|
工作流程:
|
||||||
|
1. 将安装包移动到程序目录
|
||||||
|
2. 创建替换脚本
|
||||||
|
3. 以管理员身份启动脚本(提前弹出UAC)
|
||||||
|
4. 脚本等待程序退出
|
||||||
|
5. 脚本执行静默安装
|
||||||
|
6. 重启程序
|
||||||
|
|
||||||
|
Args:
|
||||||
|
installer_path: 安装包路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功启动安装
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取当前程序信息
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# 打包后的exe
|
||||||
|
current_exe = sys.executable
|
||||||
|
current_dir = os.path.dirname(current_exe)
|
||||||
|
exe_name = os.path.basename(current_exe)
|
||||||
|
else:
|
||||||
|
# 开发环境
|
||||||
|
current_exe = os.path.abspath("main.py")
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
exe_name = "main.exe"
|
||||||
|
|
||||||
|
print(f"[Updater] 当前程序: {current_exe}")
|
||||||
|
print(f"[Updater] 程序目录: {current_dir}")
|
||||||
|
print(f"[Updater] 主程序名: {exe_name}")
|
||||||
|
print(f"[Updater] 安装包路径: {installer_path}")
|
||||||
|
|
||||||
|
# 🔥 关键改进:将安装包移动到程序目录(避免跨盘符问题)
|
||||||
|
installer_name = os.path.basename(installer_path)
|
||||||
|
target_installer_path = os.path.join(current_dir, installer_name)
|
||||||
|
|
||||||
|
# 如果安装包不在程序目录,移动过去
|
||||||
|
if os.path.abspath(installer_path) != os.path.abspath(target_installer_path):
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
shutil.move(installer_path, target_installer_path)
|
||||||
|
print(f"[Updater] 安装包已移动到程序目录: {target_installer_path}")
|
||||||
|
installer_path = target_installer_path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Updater] 移动安装包失败,使用原路径: {e}")
|
||||||
|
|
||||||
|
# 创建更新脚本
|
||||||
|
update_script_path = create_update_installer_script(
|
||||||
|
installer_path,
|
||||||
|
current_dir,
|
||||||
|
exe_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🔥 关键修复:以管理员身份启动批处理脚本,提前触发UAC
|
||||||
|
print(f"[Updater] 以管理员身份启动更新脚本: {update_script_path}")
|
||||||
|
success = run_as_admin(update_script_path)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"[Updater] ✅ 更新脚本已以管理员身份启动")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"[Updater] ❌ 用户取消了UAC授权,更新已取消")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Updater] ❌ 启动更新失败: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_update_installer_script(installer_path, install_dir, exe_name):
|
||||||
|
"""
|
||||||
|
创建更新安装脚本(在程序目录下执行)
|
||||||
|
|
||||||
|
工作流程:
|
||||||
|
1. 等待主程序退出
|
||||||
|
2. 在程序目录下执行NSIS安装
|
||||||
|
3. 清理安装包
|
||||||
|
4. 重启程序
|
||||||
|
|
||||||
|
Args:
|
||||||
|
installer_path: 安装包路径(应该已经在程序目录下)
|
||||||
|
install_dir: 程序安装目录
|
||||||
|
exe_name: 主程序文件名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 脚本路径
|
||||||
|
"""
|
||||||
|
# 脚本放在程序目录下,避免权限问题
|
||||||
|
script_path = os.path.join(install_dir, "update_installer.bat")
|
||||||
|
|
||||||
|
# 转换为绝对路径(处理引号问题)
|
||||||
|
installer_path = os.path.abspath(installer_path)
|
||||||
|
install_dir = os.path.abspath(install_dir)
|
||||||
|
|
||||||
|
script_content = f"""@echo off
|
||||||
|
REM ShuiDi AI Assistant Auto Update Script
|
||||||
|
REM Encoding: UTF-8 BOM
|
||||||
|
|
||||||
|
title ShuiDi AI Assistant - Auto Update
|
||||||
|
|
||||||
|
echo ============================================
|
||||||
|
echo ShuiDi AI Assistant Auto Update
|
||||||
|
echo ============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Change to program directory
|
||||||
|
cd /d "{install_dir}"
|
||||||
|
echo INFO: Program directory: {install_dir}
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Wait for main program to exit (max 60 seconds)
|
||||||
|
echo INFO: Waiting for main program to exit...
|
||||||
|
set count=0
|
||||||
|
:wait_main_exit
|
||||||
|
tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL
|
||||||
|
if NOT ERRORLEVEL 1 (
|
||||||
|
if %count% LSS 60 (
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
set /a count+=1
|
||||||
|
goto wait_main_exit
|
||||||
|
) else (
|
||||||
|
echo WARN: Main program did not exit normally, forcing termination
|
||||||
|
taskkill /F /IM {exe_name} 2>nul
|
||||||
|
)
|
||||||
|
)
|
||||||
|
echo OK: Main program exited
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Critical fix: Wait 2 seconds to ensure all file handles are released
|
||||||
|
echo INFO: Waiting for file handles to release...
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
echo OK: File handles released
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Critical fix: Delete old _internal directory to force clean install
|
||||||
|
echo INFO: Removing old _internal directory...
|
||||||
|
if exist "{install_dir}\\_internal" (
|
||||||
|
rd /s /q "{install_dir}\\_internal" 2>nul
|
||||||
|
if exist "{install_dir}\\_internal" (
|
||||||
|
echo WARN: Failed to delete _internal directory
|
||||||
|
) else (
|
||||||
|
echo OK: Old _internal directory removed successfully
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo INFO: No old _internal directory found
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Execute NSIS silent installation
|
||||||
|
echo INFO: Starting installation...
|
||||||
|
echo INFO: Installer: "{installer_path}"
|
||||||
|
echo INFO: Target directory: "{install_dir}"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Execute installation (with admin rights from NSIS)
|
||||||
|
"{installer_path}" /S /D={install_dir}
|
||||||
|
|
||||||
|
REM Wait for installation to complete (extended to 35 seconds for full copy)
|
||||||
|
echo INFO: Waiting for installation to complete...
|
||||||
|
timeout /t 35 /nobreak > nul
|
||||||
|
|
||||||
|
REM Cleanup installer
|
||||||
|
echo INFO: Cleaning up installer...
|
||||||
|
del /f /q "{installer_path}" 2>nul
|
||||||
|
|
||||||
|
REM Start new version (with --after-update flag to show window on top)
|
||||||
|
echo INFO: Starting new version...
|
||||||
|
if exist "{install_dir}\\{exe_name}" (
|
||||||
|
start "" "{install_dir}\\{exe_name}" --after-update
|
||||||
|
echo OK: Program started successfully
|
||||||
|
) else (
|
||||||
|
echo ERROR: Program file not found: {exe_name}
|
||||||
|
echo INFO: Please start program manually
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Delete this script after delay
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
del /f /q "%~f0" 2>nul
|
||||||
|
exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(script_path, 'w', encoding='utf-8-sig') as f:
|
||||||
|
f.write(script_content)
|
||||||
|
print(f"[Updater] 更新脚本已创建: {script_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Updater] 创建更新脚本失败: {e}")
|
||||||
|
|
||||||
|
return script_path
|
||||||
|
|
||||||
|
|
||||||
|
def create_restart_launcher(install_dir, exe_name):
|
||||||
|
"""
|
||||||
|
创建重启启动器脚本
|
||||||
|
|
||||||
|
该脚本会:
|
||||||
|
1. 等待当前GUI进程退出
|
||||||
|
2. 等待NSIS安装完成(15秒)
|
||||||
|
3. 启动新版本GUI
|
||||||
|
4. 自我删除
|
||||||
|
|
||||||
|
Args:
|
||||||
|
install_dir: 安装目录
|
||||||
|
exe_name: 主程序文件名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 脚本路径
|
||||||
|
"""
|
||||||
|
script_path = os.path.join(tempfile.gettempdir(), "shuidrop_restart_launcher.bat")
|
||||||
|
|
||||||
|
# 构建批处理脚本
|
||||||
|
script_content = f"""@echo off
|
||||||
|
chcp 65001 > nul
|
||||||
|
title ShuiDi AI Assistant - Update Helper
|
||||||
|
|
||||||
|
echo [%time%] Updating ShuiDi AI Assistant...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Wait for main program to exit (max 10 seconds)
|
||||||
|
echo [%time%] Waiting for main program to exit...
|
||||||
|
set count=0
|
||||||
|
:wait_main_exit
|
||||||
|
tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL
|
||||||
|
if NOT ERRORLEVEL 1 (
|
||||||
|
if %count% LSS 10 (
|
||||||
|
timeout /t 1 /nobreak > nul
|
||||||
|
set /a count+=1
|
||||||
|
goto wait_main_exit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
echo [%time%] Main program exited
|
||||||
|
|
||||||
|
REM Wait for installation to complete (15 seconds)
|
||||||
|
echo [%time%] Waiting for installation to complete...
|
||||||
|
timeout /t 15 /nobreak > nul
|
||||||
|
|
||||||
|
REM Clean up old installer files
|
||||||
|
echo [%time%] Cleaning up temporary files...
|
||||||
|
del /f /q "%TEMP%\\ShuiDi_AI_Assistant_Setup_*.exe" 2>nul
|
||||||
|
|
||||||
|
REM Start new version
|
||||||
|
echo [%time%] Starting new version...
|
||||||
|
cd /d "{install_dir}"
|
||||||
|
if exist "{exe_name}" (
|
||||||
|
start "" "{install_dir}\\{exe_name}"
|
||||||
|
echo [%time%] Program started successfully
|
||||||
|
) else (
|
||||||
|
echo [%time%] ERROR: Program file not found: {exe_name}
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Delete this script after delay
|
||||||
|
timeout /t 2 /nobreak > nul
|
||||||
|
del /f /q "%~f0" 2>nul
|
||||||
|
exit
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Write script file with UTF-8 BOM encoding
|
||||||
|
try:
|
||||||
|
with open(script_path, 'w', encoding='utf-8-sig') as f:
|
||||||
|
f.write(script_content)
|
||||||
|
print(f"[Updater] Restart launcher created: {script_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Updater] Failed to create restart launcher: {e}")
|
||||||
|
|
||||||
|
return script_path
|
||||||
|
|
||||||
|
|
||||||
|
# 兼容性函数(保留旧接口)
|
||||||
|
def install_update_silently(installer_path):
|
||||||
|
"""
|
||||||
|
启动静默安装(不自动重启)
|
||||||
|
兼容旧接口,建议使用 install_update_and_restart
|
||||||
|
"""
|
||||||
|
return install_update_and_restart(installer_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试代码
|
||||||
|
print("自动更新模块测试")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 测试重启启动器创建
|
||||||
|
script_path = create_restart_launcher("C:\\Program Files\\ShuiDi AI Assistant", "main.exe")
|
||||||
|
print(f"测试脚本路径: {script_path}")
|
||||||
|
|
||||||
|
if os.path.exists(script_path):
|
||||||
|
print("✅ 重启启动器创建成功")
|
||||||
|
with open(script_path, 'r', encoding='utf-8-sig') as f:
|
||||||
|
print("\n脚本内容预览:")
|
||||||
|
print("-" * 50)
|
||||||
|
print(f.read())
|
||||||
|
else:
|
||||||
|
print("❌ 重启启动器创建失败")
|
||||||
|
|
||||||
@@ -1,129 +1,151 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
生产环境打包脚本 - 自动禁用日志功能
|
Production build script - Automatically disable logging functionality
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure script runs in correct directory
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
os.chdir(SCRIPT_DIR)
|
||||||
|
print(f"Working directory: {SCRIPT_DIR}")
|
||||||
|
|
||||||
def disable_logging():
|
def disable_logging():
|
||||||
"""自动禁用所有日志功能"""
|
"""Disable all logging functionality"""
|
||||||
print("🔧 正在禁用日志功能...")
|
print("Disabling logging functionality...")
|
||||||
|
|
||||||
# 备份原文件
|
# Backup original files
|
||||||
files_to_backup = ['main.py', 'exe_file_logger.py']
|
files_to_backup = ['main.py', 'exe_file_logger.py']
|
||||||
for file_name in files_to_backup:
|
for file_name in files_to_backup:
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
shutil.copy(file_name, f'{file_name}.backup')
|
shutil.copy(file_name, f'{file_name}.backup')
|
||||||
print(f"✅ 已备份 {file_name}")
|
print(f"Backed up {file_name}")
|
||||||
|
|
||||||
# 1. 修改 main.py - 注释掉日志初始化
|
# 1. Modify main.py - comment out logging initialization
|
||||||
if os.path.exists('main.py'):
|
if os.path.exists('main.py'):
|
||||||
with open('main.py', 'r', encoding='utf-8') as f:
|
with open('main.py', 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# 注释掉日志相关的导入和初始化
|
# Comment out logging related imports and initialization
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
'from exe_file_logger import setup_file_logging, log_to_file',
|
'from exe_file_logger import setup_file_logging, log_to_file',
|
||||||
'# from exe_file_logger import setup_file_logging, log_to_file # 生产环境禁用'
|
'# from exe_file_logger import setup_file_logging, log_to_file # Production disabled'
|
||||||
).replace(
|
).replace(
|
||||||
'setup_file_logging()',
|
'setup_file_logging()',
|
||||||
'# setup_file_logging() # 生产环境禁用'
|
'# setup_file_logging() # Production disabled'
|
||||||
).replace(
|
).replace(
|
||||||
'print("文件日志系统已在main.py中初始化")',
|
'print("文件日志系统已在main.py中初始化")',
|
||||||
'# print("文件日志系统已在main.py中初始化") # 生产环境禁用'
|
'# print("文件日志系统已在main.py中初始化") # Production disabled'
|
||||||
)
|
)
|
||||||
|
|
||||||
with open('main.py', 'w', encoding='utf-8') as f:
|
with open('main.py', 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
print("✅ 已禁用 main.py 中的日志初始化")
|
print("Disabled logging in main.py")
|
||||||
|
|
||||||
# 2. 修改 exe_file_logger.py - 让所有函数变成空操作
|
# 2. Modify exe_file_logger.py - make all functions no-op
|
||||||
if os.path.exists('exe_file_logger.py'):
|
if os.path.exists('exe_file_logger.py'):
|
||||||
with open('exe_file_logger.py', 'r', encoding='utf-8') as f:
|
with open('exe_file_logger.py', 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# 将 setup_file_logging 函数替换为空函数
|
# Replace setup_file_logging function with empty function
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
'def setup_file_logging():',
|
'def setup_file_logging():',
|
||||||
'def setup_file_logging():\n """生产环境 - 禁用文件日志"""\n return None\n\ndef setup_file_logging_original():'
|
'def setup_file_logging():\n """Production - Logging disabled"""\n return None\n\ndef setup_file_logging_original():'
|
||||||
).replace(
|
).replace(
|
||||||
'def log_to_file(message):',
|
'def log_to_file(message):',
|
||||||
'def log_to_file(message):\n """生产环境 - 禁用文件日志"""\n pass\n\ndef log_to_file_original(message):'
|
'def log_to_file(message):\n """Production - Logging disabled"""\n pass\n\ndef log_to_file_original(message):'
|
||||||
)
|
)
|
||||||
|
|
||||||
with open('exe_file_logger.py', 'w', encoding='utf-8') as f:
|
with open('exe_file_logger.py', 'w', encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
print("✅ 已禁用 exe_file_logger.py 中的日志功能")
|
print("Disabled logging in exe_file_logger.py")
|
||||||
|
|
||||||
print("✅ 所有日志功能已禁用")
|
print("All logging functionality disabled")
|
||||||
|
|
||||||
def restore_logging():
|
def restore_logging():
|
||||||
"""恢复日志功能"""
|
"""Restore logging functionality"""
|
||||||
|
print("Restoring development configuration...")
|
||||||
files_to_restore = ['main.py', 'exe_file_logger.py']
|
files_to_restore = ['main.py', 'exe_file_logger.py']
|
||||||
|
restored_count = 0
|
||||||
|
|
||||||
for file_name in files_to_restore:
|
for file_name in files_to_restore:
|
||||||
backup_file = f'{file_name}.backup'
|
backup_file = f'{file_name}.backup'
|
||||||
if os.path.exists(backup_file):
|
try:
|
||||||
shutil.copy(backup_file, file_name)
|
if os.path.exists(backup_file):
|
||||||
os.remove(backup_file)
|
# Ensure target file is writable
|
||||||
print(f"✅ 已恢复 {file_name}")
|
if os.path.exists(file_name):
|
||||||
print("✅ 所有文件已恢复到开发环境配置")
|
os.chmod(file_name, 0o666)
|
||||||
|
|
||||||
|
shutil.copy(backup_file, file_name)
|
||||||
|
os.remove(backup_file)
|
||||||
|
print(f"[OK] Restored {file_name}")
|
||||||
|
restored_count += 1
|
||||||
|
else:
|
||||||
|
print(f"[WARN] Backup file not found: {backup_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to restore {file_name}: {e}")
|
||||||
|
|
||||||
|
if restored_count == len(files_to_restore):
|
||||||
|
print("[OK] All files restored to development configuration")
|
||||||
|
else:
|
||||||
|
print(f"[WARN] Restored {restored_count}/{len(files_to_restore)} files")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""Main function"""
|
||||||
print("🔥 生产环境打包工具(无日志版本)")
|
print("Production Build Tool (No Logging Version)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 禁用日志功能
|
# 1. Disable logging functionality
|
||||||
disable_logging()
|
disable_logging()
|
||||||
|
|
||||||
# 2. 执行打包
|
# 2. Execute build
|
||||||
print("\n🚀 开始打包...")
|
print("\nStarting build...")
|
||||||
result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
|
result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
# 确保输出目录正确性
|
# Verify output (quick_build.py outputs to MultiPlatformGUI)
|
||||||
if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'):
|
|
||||||
print("🔄 修正输出目录名称...")
|
|
||||||
try:
|
|
||||||
os.rename('dist/main', 'dist/MultiPlatformGUI')
|
|
||||||
print("✅ 已重命名为: dist/MultiPlatformGUI/")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ 重命名失败: {e}")
|
|
||||||
|
|
||||||
# 验证最终输出
|
|
||||||
if os.path.exists('dist/MultiPlatformGUI'):
|
if os.path.exists('dist/MultiPlatformGUI'):
|
||||||
print("\n🎉 生产环境打包成功!")
|
print("\nProduction build completed successfully!")
|
||||||
print("📁 输出目录: dist/MultiPlatformGUI/")
|
print("Output directory: dist/MultiPlatformGUI/")
|
||||||
print("🔇 已禁用所有日志功能,客户端不会产生日志文件")
|
print("All logging disabled - client will not generate log files")
|
||||||
|
|
||||||
# 验证关键文件
|
# Verify key files
|
||||||
exe_path = 'dist/MultiPlatformGUI/main.exe'
|
exe_path = 'dist/MultiPlatformGUI/main.exe'
|
||||||
if os.path.exists(exe_path):
|
if os.path.exists(exe_path):
|
||||||
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
|
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
|
||||||
print(f"✅ 主程序: main.exe ({size:.1f} MB)")
|
print(f"Main executable: main.exe ({size:.1f} MB)")
|
||||||
else:
|
else:
|
||||||
print("⚠️ 主程序文件未找到")
|
print("WARNING: Main executable not found")
|
||||||
|
|
||||||
|
# 🔥 关键修复:添加PyMiniRacer DLL修复步骤
|
||||||
|
print("\nPyMiniRacer DLL fix phase started...")
|
||||||
|
try:
|
||||||
|
import fix_pyminiracer_dll
|
||||||
|
if fix_pyminiracer_dll.auto_fix_after_build():
|
||||||
|
print("PyMiniRacer DLL fix completed")
|
||||||
|
else:
|
||||||
|
print("WARNING: PyMiniRacer DLL fix failed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: PyMiniRacer DLL fix error: {e}")
|
||||||
else:
|
else:
|
||||||
print("\n❌ 输出目录验证失败")
|
print("\nERROR: Output directory verification failed - dist/MultiPlatformGUI not found")
|
||||||
if os.path.exists('dist/main'):
|
|
||||||
print("💡 发现: dist/main/ 目录,请手动重命名为 MultiPlatformGUI")
|
|
||||||
else:
|
else:
|
||||||
print("\n❌ 打包失败")
|
print("\nERROR: Build failed")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 打包过程出错: {e}")
|
print(f"ERROR: Build process error: {e}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# 3. 恢复日志功能(用于开发)
|
# 3. Restore logging functionality (for development)
|
||||||
print("\n🔄 恢复开发环境配置...")
|
print()
|
||||||
restore_logging()
|
restore_logging()
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
input("按Enter键退出...")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
300
config.py
300
config.py
@@ -1,131 +1,171 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
项目配置文件
|
项目配置文件
|
||||||
统一管理所有配置参数,避免硬编码
|
统一管理所有配置参数,避免硬编码
|
||||||
"""
|
"""
|
||||||
# 用户访问令牌(默认空字符串)
|
# 用户访问令牌(默认空字符串)
|
||||||
import os # 用于路径与目录操作(写入用户配置目录)
|
import os # 用于路径与目录操作(写入用户配置目录)
|
||||||
import json # 用于将令牌保存为 JSON 格式
|
import json # 用于将令牌保存为 JSON 格式
|
||||||
|
|
||||||
# 后端服务器配置
|
# 后端服务器配置
|
||||||
# BACKEND_HOST = "192.168.5.233"
|
# BACKEND_HOST = "192.168.5.233"
|
||||||
BACKEND_HOST = "192.168.5.12"
|
# BACKEND_HOST = "192.168.5.106"
|
||||||
# BACKEND_HOST = "shuidrop.com"
|
# BACKEND_HOST = "192.168.5.12"
|
||||||
# BACKEND_HOST = "test.shuidrop.com"
|
BACKEND_HOST = "shuidrop.com"
|
||||||
BACKEND_PORT = "8000"
|
# BACKEND_HOST = "test.shuidrop.com"
|
||||||
# BACKEND_PORT = ""
|
# BACKEND_PORT = "8000"
|
||||||
BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
|
BACKEND_PORT = ""
|
||||||
# BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
|
# BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
|
||||||
|
BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
|
||||||
# WebSocket配置
|
|
||||||
WS_CONNECT_TIMEOUT = 16.0
|
# WebSocket配置
|
||||||
WS_MESSAGE_TIMEOUT = 30.0
|
WS_CONNECT_TIMEOUT = 16.0
|
||||||
WS_PING_INTERVAL = 10 # 10秒ping间隔(提高检测频率)
|
WS_MESSAGE_TIMEOUT = 30.0
|
||||||
WS_PING_TIMEOUT = 5 # 5秒ping超时(更快检测断线)
|
WS_PING_INTERVAL = 15 # 10秒ping间隔(提高检测频率)
|
||||||
WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳
|
WS_PING_TIMEOUT = 10 # 5秒ping超时(更快检测断线)
|
||||||
WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复)
|
WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳
|
||||||
|
WS_ENABLE_APP_PING = False # 禁用应用层ping心跳(避免重复)
|
||||||
# AI处理超时配置
|
|
||||||
AI_PROCESS_TIMEOUT = 30 # AI处理超时时间(秒)
|
# AI处理超时配置
|
||||||
AI_LONG_PROCESS_THRESHOLD = 10 # AI长时间处理阈值(秒)
|
AI_PROCESS_TIMEOUT = 30 # AI处理超时时间(秒)
|
||||||
|
AI_LONG_PROCESS_THRESHOLD = 10 # AI长时间处理阈值(秒)
|
||||||
# 内存管理配置
|
|
||||||
MAX_PENDING_REPLIES = 100
|
# 内存管理配置
|
||||||
CLEANUP_INTERVAL = 300 # 5分钟
|
MAX_PENDING_REPLIES = 100
|
||||||
FUTURE_TIMEOUT = 300 # 5分钟
|
CLEANUP_INTERVAL = 300 # 5分钟
|
||||||
|
FUTURE_TIMEOUT = 300 # 5分钟
|
||||||
# 终端日志配置(简化)
|
|
||||||
LOG_LEVEL = "INFO"
|
# 终端日志配置(简化)
|
||||||
VERSION = "1.0"
|
LOG_LEVEL = "INFO"
|
||||||
# GUI配置
|
VERSION = "1.0"
|
||||||
WINDOW_TITLE = "AI回复连接入口-V1.0"
|
|
||||||
|
# GUI配置
|
||||||
# 平台特定配置
|
WINDOW_TITLE = "AI回复连接入口-V1.0"
|
||||||
PLATFORMS = {
|
|
||||||
"JD": {
|
# 应用版本号(用于版本检查)
|
||||||
"name": "京东",
|
APP_VERSION = "1.5.71"
|
||||||
"ws_url": "wss://dongdong.jd.com/workbench/websocket"
|
|
||||||
},
|
# 🔥 多实例运行模式开关
|
||||||
"DOUYIN": {
|
# - True: 测试模式(多实例,不保存token,避免冲突)
|
||||||
"name": "抖音",
|
# - False: 生产模式(单实例,保存token,自动加载)
|
||||||
"ws_url": None # 动态获取
|
#
|
||||||
},
|
# 使用方法:
|
||||||
"QIANNIU": {
|
# 1. 修改此值:MULTI_INSTANCE_MODE = False # 改为生产模式
|
||||||
"name": "千牛(淘宝)",
|
# 2. 或设置环境变量:SHUIDROP_MULTI_INSTANCE=0 # 临时切换到生产模式
|
||||||
"ws_url": "ws://127.0.0.1:3030"
|
MULTI_INSTANCE_MODE = True # 默认:测试模式
|
||||||
},
|
|
||||||
"PDD": {
|
|
||||||
"name": "拼多多",
|
def is_multi_instance_mode() -> bool:
|
||||||
"ws_url": None # 动态获取
|
"""
|
||||||
}
|
检查是否为多实例模式(支持环境变量覆盖)
|
||||||
}
|
|
||||||
|
优先级:
|
||||||
def get_backend_ws_url(platform: str, store_id: str) -> str:
|
1. 环境变量 SHUIDROP_MULTI_INSTANCE(0=生产,1=测试)
|
||||||
"""获取旧版后端WebSocket URL(按店铺建连接,保留兼容)"""
|
2. 配置文件 MULTI_INSTANCE_MODE
|
||||||
return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/"
|
|
||||||
|
Returns:
|
||||||
def get_gui_ws_url(exe_token: str) -> str:
|
bool: True=多实例模式,False=单实例模式
|
||||||
"""获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)"""
|
"""
|
||||||
return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/"
|
# 检查环境变量
|
||||||
|
env_value = os.getenv('SHUIDROP_MULTI_INSTANCE')
|
||||||
def get_config():
|
if env_value is not None:
|
||||||
"""获取所有配置"""
|
# 0, false, False, no, No → 生产模式
|
||||||
return {
|
if env_value.lower() in ('0', 'false', 'no'):
|
||||||
'backend_host': BACKEND_HOST,
|
return False
|
||||||
'backend_port': BACKEND_PORT,
|
# 1, true, True, yes, Yes → 测试模式
|
||||||
'backend_ws_url': BACKEND_WS_URL,
|
if env_value.lower() in ('1', 'true', 'yes'):
|
||||||
'ws_connect_timeout': WS_CONNECT_TIMEOUT,
|
return True
|
||||||
'ws_message_timeout': WS_MESSAGE_TIMEOUT,
|
|
||||||
'max_pending_replies': MAX_PENDING_REPLIES,
|
# 使用配置文件值 (如果不做设置我们可以直接用编码变量进行控制是否可以允许多实例的方式运行)
|
||||||
'cleanup_interval': CLEANUP_INTERVAL,
|
return MULTI_INSTANCE_MODE
|
||||||
'platforms': PLATFORMS
|
|
||||||
}
|
|
||||||
|
# 平台特定配置
|
||||||
|
PLATFORMS = {
|
||||||
|
"JD": {
|
||||||
APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名)
|
"name": "京东",
|
||||||
|
"ws_url": "wss://dongdong.jd.com/workbench/websocket"
|
||||||
API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用)
|
},
|
||||||
|
"DOUYIN": {
|
||||||
def _get_config_paths():
|
"name": "抖音",
|
||||||
"""返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json"""
|
"ws_url": None # 动态获取
|
||||||
base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录
|
},
|
||||||
cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径
|
"QIANNIU": {
|
||||||
cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径
|
"name": "千牛(淘宝)",
|
||||||
return cfg_dir, cfg_file
|
"ws_url": "ws://127.0.0.1:3030"
|
||||||
|
},
|
||||||
|
"PDD": {
|
||||||
def get_saved_token() -> str:
|
"name": "拼多多",
|
||||||
"""优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN"""
|
"ws_url": None # 动态获取
|
||||||
try:
|
}
|
||||||
cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径
|
}
|
||||||
if os.path.exists(cfg_file): # 如果配置文件存在
|
|
||||||
with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取
|
def get_backend_ws_url(platform: str, store_id: str) -> str:
|
||||||
data = json.load(f) # 解析 JSON 内容
|
"""获取旧版后端WebSocket URL(按店铺建连接,保留兼容)"""
|
||||||
token = data.get('token', '') # 读取 token 字段
|
return f"{BACKEND_WS_URL}/ws/platform/{platform.lower()}/{store_id}/"
|
||||||
if token: # 如果有效
|
|
||||||
return token # 返回读取到的令牌
|
def get_gui_ws_url(exe_token: str) -> str:
|
||||||
except Exception:
|
"""获取新版单连接GUI专用WebSocket URL(按用户token建一条连接)"""
|
||||||
pass # 读取失败时静默回退
|
return f"{BACKEND_WS_URL}/ws/gui/{exe_token}/"
|
||||||
return API_TOKEN # 回退为内置的默认值
|
|
||||||
|
def get_config():
|
||||||
|
"""获取所有配置"""
|
||||||
def set_saved_token(new_token: str) -> bool:
|
return {
|
||||||
"""将访问令牌写入外部 JSON 配置,并更新内存中的值
|
'backend_host': BACKEND_HOST,
|
||||||
- new_token: 新的访问令牌字符串
|
'backend_port': BACKEND_PORT,
|
||||||
返回: True 表示写入成功,False 表示失败
|
'backend_ws_url': BACKEND_WS_URL,
|
||||||
"""
|
'ws_connect_timeout': WS_CONNECT_TIMEOUT,
|
||||||
try:
|
'ws_message_timeout': WS_MESSAGE_TIMEOUT,
|
||||||
cfg_dir, cfg_file = _get_config_paths()
|
'max_pending_replies': MAX_PENDING_REPLIES,
|
||||||
os.makedirs(cfg_dir, exist_ok=True)
|
'cleanup_interval': CLEANUP_INTERVAL,
|
||||||
data = {'token': new_token}
|
'platforms': PLATFORMS
|
||||||
with open(cfg_file, 'w', encoding='utf-8') as f:
|
}
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
# 同步更新内存变量,保证运行期可立即生效
|
|
||||||
global API_TOKEN
|
|
||||||
API_TOKEN = new_token
|
APP_NAME = "ShuidropGUI" # 应用名称(作为配置目录名)
|
||||||
return True
|
|
||||||
except Exception as e:
|
API_TOKEN = 'sd_acF0TisgfFOtsBm4ytqb17MQbcxuX9Vp' # 默认回退令牌(仅当未找到外部配置时使用)
|
||||||
# 发生异常时打印提示并返回失败
|
|
||||||
print(f"写入令牌失败: {e}")
|
def _get_config_paths():
|
||||||
|
"""返回(配置目录, 配置文件路径),位于 %APPDATA%/ShuidropGUI/config.json"""
|
||||||
|
base_dir = os.getenv('APPDATA') or os.path.expanduser('~') # 优先使用 APPDATA,其次使用用户主目录
|
||||||
|
cfg_dir = os.path.join(base_dir, APP_NAME) # 组合配置目录路径
|
||||||
|
cfg_file = os.path.join(cfg_dir, 'config.json') # 组合配置文件路径
|
||||||
|
return cfg_dir, cfg_file
|
||||||
|
|
||||||
|
|
||||||
|
def get_saved_token() -> str:
|
||||||
|
"""优先从外部 JSON 配置读取令牌,不存在时回退到内置 API_TOKEN"""
|
||||||
|
try:
|
||||||
|
cfg_dir, cfg_file = _get_config_paths() # 获取目录与文件路径
|
||||||
|
if os.path.exists(cfg_file): # 如果配置文件存在
|
||||||
|
with open(cfg_file, 'r', encoding='utf-8') as f: # 以 UTF-8 读取
|
||||||
|
data = json.load(f) # 解析 JSON 内容
|
||||||
|
token = data.get('token', '') # 读取 token 字段
|
||||||
|
if token: # 如果有效
|
||||||
|
return token # 返回读取到的令牌
|
||||||
|
except Exception:
|
||||||
|
pass # 读取失败时静默回退
|
||||||
|
return API_TOKEN # 回退为内置的默认值
|
||||||
|
|
||||||
|
|
||||||
|
def set_saved_token(new_token: str) -> bool:
|
||||||
|
"""将访问令牌写入外部 JSON 配置,并更新内存中的值
|
||||||
|
- new_token: 新的访问令牌字符串
|
||||||
|
返回: True 表示写入成功,False 表示失败
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cfg_dir, cfg_file = _get_config_paths()
|
||||||
|
os.makedirs(cfg_dir, exist_ok=True)
|
||||||
|
data = {'token': new_token}
|
||||||
|
with open(cfg_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
# 同步更新内存变量,保证运行期可立即生效
|
||||||
|
global API_TOKEN
|
||||||
|
API_TOKEN = new_token
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
# 发生异常时打印提示并返回失败
|
||||||
|
print(f"写入令牌失败: {e}")
|
||||||
return False
|
return False
|
||||||
344
custom_update_dialog.py
Normal file
344
custom_update_dialog.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
自定义版本更新对话框
|
||||||
|
美观的布局和用户体验
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QPushButton, QTextEdit, QFrame)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont, QCursor
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateNotificationDialog(QDialog):
|
||||||
|
"""
|
||||||
|
版本更新提示对话框
|
||||||
|
|
||||||
|
布局:
|
||||||
|
- 标题区
|
||||||
|
- 更新内容区
|
||||||
|
- 下载地址区(可复制)
|
||||||
|
- 按钮区(两行,美观布局)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 用户选择(返回值)
|
||||||
|
CHOICE_UPDATE_NOW = 1 # 立即更新
|
||||||
|
CHOICE_UPDATE_LATER = 2 # 稍后更新
|
||||||
|
CHOICE_IGNORE = 3 # 忽略此版本
|
||||||
|
|
||||||
|
def __init__(self, version, download_url, update_content="", parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.version = version
|
||||||
|
self.download_url = download_url
|
||||||
|
self.update_content = update_content
|
||||||
|
self.user_choice = None
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""初始化UI"""
|
||||||
|
self.setWindowTitle("版本更新")
|
||||||
|
self.setFixedWidth(550)
|
||||||
|
self.setMinimumHeight(400)
|
||||||
|
|
||||||
|
# 禁用关闭按钮(只能通过按钮关闭)
|
||||||
|
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setSpacing(15)
|
||||||
|
layout.setContentsMargins(25, 20, 25, 20)
|
||||||
|
|
||||||
|
# ============ 标题区 ============
|
||||||
|
title_label = QLabel(f"🔔 发现新版本 v{self.version}")
|
||||||
|
title_label.setFont(QFont('Microsoft YaHei', 14, QFont.Bold))
|
||||||
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 5px;")
|
||||||
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
|
# 分隔线
|
||||||
|
line1 = QFrame()
|
||||||
|
line1.setFrameShape(QFrame.HLine)
|
||||||
|
line1.setStyleSheet("background: #ddd; max-height: 1px;")
|
||||||
|
layout.addWidget(line1)
|
||||||
|
|
||||||
|
# ============ 更新内容区(可选)============
|
||||||
|
# 🔥 只有在有更新内容时才显示
|
||||||
|
if self.update_content and self.update_content.strip():
|
||||||
|
content_label = QLabel("📝 更新内容:")
|
||||||
|
content_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold))
|
||||||
|
content_label.setStyleSheet("color: #34495e;")
|
||||||
|
layout.addWidget(content_label)
|
||||||
|
|
||||||
|
# 更新内容文本
|
||||||
|
content_text = QTextEdit()
|
||||||
|
content_text.setReadOnly(True)
|
||||||
|
content_text.setMaximumHeight(100)
|
||||||
|
content_text.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
content_text.setText(self.update_content)
|
||||||
|
layout.addWidget(content_text)
|
||||||
|
|
||||||
|
# ============ 下载地址区 ============
|
||||||
|
url_label = QLabel("📥 下载地址:")
|
||||||
|
url_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold))
|
||||||
|
url_label.setStyleSheet("color: #34495e; margin-top: 5px;")
|
||||||
|
layout.addWidget(url_label)
|
||||||
|
|
||||||
|
# 下载地址文本框(可选择复制)
|
||||||
|
self.url_text = QTextEdit()
|
||||||
|
self.url_text.setReadOnly(True)
|
||||||
|
self.url_text.setMaximumHeight(60)
|
||||||
|
self.url_text.setText(self.download_url)
|
||||||
|
self.url_text.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
border: 2px solid #4285f4;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #e3f2fd;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #1565c0;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
# 全选文本,方便复制
|
||||||
|
self.url_text.selectAll()
|
||||||
|
|
||||||
|
layout.addWidget(self.url_text)
|
||||||
|
|
||||||
|
# 复制提示
|
||||||
|
copy_hint = QLabel("💡 提示:上方地址已自动选中,按 Ctrl+C 即可复制")
|
||||||
|
copy_hint.setFont(QFont('Microsoft YaHei', 8))
|
||||||
|
copy_hint.setStyleSheet("color: #7f8c8d;")
|
||||||
|
layout.addWidget(copy_hint)
|
||||||
|
|
||||||
|
# 分隔线
|
||||||
|
line2 = QFrame()
|
||||||
|
line2.setFrameShape(QFrame.HLine)
|
||||||
|
line2.setStyleSheet("background: #ddd; max-height: 1px; margin-top: 10px;")
|
||||||
|
layout.addWidget(line2)
|
||||||
|
|
||||||
|
# ============ 操作说明区 ============
|
||||||
|
hint_label = QLabel(
|
||||||
|
"• 在线自动更新:自动下载并安装新版本\n"
|
||||||
|
"• 稍后更新:保留提示在状态栏,可随时点击更新\n"
|
||||||
|
"• 忽略此版本:本次运行不再提示此版本"
|
||||||
|
)
|
||||||
|
hint_label.setFont(QFont('Microsoft YaHei', 9))
|
||||||
|
hint_label.setStyleSheet("color: #666; margin: 5px 0;")
|
||||||
|
layout.addWidget(hint_label)
|
||||||
|
|
||||||
|
# ============ 按钮区(两行布局)============
|
||||||
|
# 第一行:主要操作
|
||||||
|
button_row1 = QHBoxLayout()
|
||||||
|
button_row1.setSpacing(10)
|
||||||
|
|
||||||
|
self.update_now_btn = QPushButton("🚀 在线自动更新")
|
||||||
|
self.update_now_btn.setFont(QFont('Microsoft YaHei', 11, QFont.Bold))
|
||||||
|
self.update_now_btn.setMinimumHeight(42)
|
||||||
|
self.update_now_btn.setCursor(QCursor(Qt.PointingHandCursor))
|
||||||
|
self.update_now_btn.clicked.connect(self.on_update_now)
|
||||||
|
|
||||||
|
self.update_later_btn = QPushButton("⏰ 稍后更新")
|
||||||
|
self.update_later_btn.setFont(QFont('Microsoft YaHei', 11))
|
||||||
|
self.update_later_btn.setMinimumHeight(42)
|
||||||
|
self.update_later_btn.setCursor(QCursor(Qt.PointingHandCursor))
|
||||||
|
self.update_later_btn.clicked.connect(self.on_update_later)
|
||||||
|
|
||||||
|
button_row1.addWidget(self.update_now_btn)
|
||||||
|
button_row1.addWidget(self.update_later_btn)
|
||||||
|
layout.addLayout(button_row1)
|
||||||
|
|
||||||
|
# 第二行:次要操作
|
||||||
|
button_row2 = QHBoxLayout()
|
||||||
|
button_row2.setSpacing(10)
|
||||||
|
|
||||||
|
self.ignore_btn = QPushButton("🚫 忽略此版本")
|
||||||
|
self.ignore_btn.setFont(QFont('Microsoft YaHei', 10))
|
||||||
|
self.ignore_btn.setMinimumHeight(38)
|
||||||
|
self.ignore_btn.setCursor(QCursor(Qt.PointingHandCursor))
|
||||||
|
self.ignore_btn.clicked.connect(self.on_ignore)
|
||||||
|
|
||||||
|
self.copy_btn = QPushButton("📋 复制下载地址")
|
||||||
|
self.copy_btn.setFont(QFont('Microsoft YaHei', 10))
|
||||||
|
self.copy_btn.setMinimumHeight(38)
|
||||||
|
self.copy_btn.setCursor(QCursor(Qt.PointingHandCursor))
|
||||||
|
self.copy_btn.clicked.connect(self.on_copy_url)
|
||||||
|
|
||||||
|
button_row2.addWidget(self.ignore_btn)
|
||||||
|
button_row2.addWidget(self.copy_btn)
|
||||||
|
layout.addLayout(button_row2)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 应用样式
|
||||||
|
self.apply_styles()
|
||||||
|
|
||||||
|
def apply_styles(self):
|
||||||
|
"""应用现代化样式"""
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f8fafb,
|
||||||
|
stop:1 #e8ecef
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主要操作按钮(立即更新、稍后更新)*/
|
||||||
|
QPushButton#updateNowBtn {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #4285f4,
|
||||||
|
stop:1 #1565c0
|
||||||
|
);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#updateNowBtn:hover {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #5294f5,
|
||||||
|
stop:1 #1e74c1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#updateLaterBtn {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #ff9800,
|
||||||
|
stop:1 #f57c00
|
||||||
|
);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton#updateLaterBtn:hover {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #ffa726,
|
||||||
|
stop:1 #fb8c00
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 次要操作按钮(忽略、复制)*/
|
||||||
|
QPushButton {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f5f5f5,
|
||||||
|
stop:1 #e0e0e0
|
||||||
|
);
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton:hover {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #e8e8e8,
|
||||||
|
stop:1 #d0d0d0
|
||||||
|
);
|
||||||
|
border: 2px solid #999;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 设置按钮对象名以应用特定样式
|
||||||
|
self.update_now_btn.setObjectName("updateNowBtn")
|
||||||
|
self.update_later_btn.setObjectName("updateLaterBtn")
|
||||||
|
|
||||||
|
def on_update_now(self):
|
||||||
|
"""立即更新"""
|
||||||
|
self.user_choice = self.CHOICE_UPDATE_NOW
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def on_update_later(self):
|
||||||
|
"""稍后更新"""
|
||||||
|
self.user_choice = self.CHOICE_UPDATE_LATER
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def on_ignore(self):
|
||||||
|
"""忽略此版本"""
|
||||||
|
self.user_choice = self.CHOICE_IGNORE
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def on_copy_url(self):
|
||||||
|
"""复制下载地址"""
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
|
clipboard = QApplication.clipboard()
|
||||||
|
clipboard.setText(self.download_url)
|
||||||
|
|
||||||
|
# 显示复制成功提示(不关闭主对话框)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"复制成功",
|
||||||
|
f"✅ 下载地址已复制到剪贴板!\n\n"
|
||||||
|
f"您可以直接粘贴(Ctrl+V)到浏览器下载。",
|
||||||
|
QMessageBox.Ok
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user_choice(self):
|
||||||
|
"""获取用户选择"""
|
||||||
|
return self.user_choice
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试对话框
|
||||||
|
import sys
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("测试更新对话框")
|
||||||
|
print("=" * 60)
|
||||||
|
print("\n场景1:有更新内容")
|
||||||
|
|
||||||
|
dialog = UpdateNotificationDialog(
|
||||||
|
version="1.5.64",
|
||||||
|
download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.64.exe",
|
||||||
|
update_content="• 修复了DY图片/视频上传问题\n• 优化了自动更新流程\n• 添加了圆形进度条"
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.exec_()
|
||||||
|
choice = dialog.get_user_choice()
|
||||||
|
|
||||||
|
if choice == UpdateNotificationDialog.CHOICE_UPDATE_NOW:
|
||||||
|
print("\n用户选择:在线自动更新")
|
||||||
|
elif choice == UpdateNotificationDialog.CHOICE_UPDATE_LATER:
|
||||||
|
print("\n用户选择:稍后更新")
|
||||||
|
elif choice == UpdateNotificationDialog.CHOICE_IGNORE:
|
||||||
|
print("\n用户选择:忽略此版本")
|
||||||
|
else:
|
||||||
|
print("\n用户关闭了对话框")
|
||||||
|
|
||||||
|
# 测试场景2:无更新内容
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("场景2:无更新内容(应该隐藏更新内容区域)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
dialog2 = UpdateNotificationDialog(
|
||||||
|
version="1.5.65",
|
||||||
|
download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.65.exe",
|
||||||
|
update_content="" # 空内容
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog2.exec_()
|
||||||
|
|
||||||
|
print("\n测试完成!")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
@@ -1,129 +1,129 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
EXE文件日志器 - 将所有输出重定向到文件
|
EXE文件日志器 - 将所有输出重定向到文件
|
||||||
适用于 pyinstaller -w 打包的exe
|
适用于 pyinstaller -w 打包的exe
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
class FileLogger:
|
class FileLogger:
|
||||||
"""文件日志器类"""
|
"""文件日志器类"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 确定日志文件路径
|
# 确定日志文件路径
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# 打包环境
|
# 打包环境
|
||||||
self.log_dir = os.path.dirname(sys.executable)
|
self.log_dir = os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# 开发环境
|
# 开发环境
|
||||||
self.log_dir = os.getcwd()
|
self.log_dir = os.getcwd()
|
||||||
|
|
||||||
# 创建日志文件
|
# 创建日志文件
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self.log_file = os.path.join(self.log_dir, f"MultiPlatformGUI_{timestamp}.log")
|
self.log_file = os.path.join(self.log_dir, f"MultiPlatformGUI_{timestamp}.log")
|
||||||
|
|
||||||
# 线程锁,确保多线程写入安全
|
# 线程锁,确保多线程写入安全
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
# 初始化日志文件
|
# 初始化日志文件
|
||||||
self.write_log("=" * 80)
|
self.write_log("=" * 80)
|
||||||
self.write_log("多平台客服GUI日志开始")
|
self.write_log("多平台客服GUI日志开始")
|
||||||
self.write_log(f"Python版本: {sys.version}")
|
self.write_log(f"Python版本: {sys.version}")
|
||||||
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
|
self.write_log(f"是否打包环境: {getattr(sys, 'frozen', False)}")
|
||||||
self.write_log(f"日志文件: {self.log_file}")
|
self.write_log(f"日志文件: {self.log_file}")
|
||||||
self.write_log("=" * 80)
|
self.write_log("=" * 80)
|
||||||
|
|
||||||
def write_log(self, message):
|
def write_log(self, message):
|
||||||
"""写入日志到文件"""
|
"""写入日志到文件"""
|
||||||
try:
|
try:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||||
log_entry = f"[{timestamp}] {message}\n"
|
log_entry = f"[{timestamp}] {message}\n"
|
||||||
|
|
||||||
with open(self.log_file, 'a', encoding='utf-8') as f:
|
with open(self.log_file, 'a', encoding='utf-8') as f:
|
||||||
f.write(log_entry)
|
f.write(log_entry)
|
||||||
f.flush()
|
f.flush()
|
||||||
os.fsync(f.fileno()) # 强制写入磁盘
|
os.fsync(f.fileno()) # 强制写入磁盘
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果写入失败,至少尝试输出到stderr
|
# 如果写入失败,至少尝试输出到stderr
|
||||||
try:
|
try:
|
||||||
sys.stderr.write(f"LOG_ERROR: {message} | Error: {e}\n")
|
sys.stderr.write(f"LOG_ERROR: {message} | Error: {e}\n")
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
"""实现write方法以支持作为sys.stdout使用"""
|
"""实现write方法以支持作为sys.stdout使用"""
|
||||||
if text.strip(): # 只记录非空内容
|
if text.strip(): # 只记录非空内容
|
||||||
self.write_log(text.strip())
|
self.write_log(text.strip())
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
"""实现flush方法"""
|
"""实现flush方法"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class TeeOutput:
|
class TeeOutput:
|
||||||
"""同时输出到原始输出和文件的类"""
|
"""同时输出到原始输出和文件的类"""
|
||||||
|
|
||||||
def __init__(self, original, file_logger):
|
def __init__(self, original, file_logger):
|
||||||
self.original = original
|
self.original = original
|
||||||
self.file_logger = file_logger
|
self.file_logger = file_logger
|
||||||
|
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
# 写入原始输出(如果存在)
|
# 写入原始输出(如果存在)
|
||||||
if self.original:
|
if self.original:
|
||||||
try:
|
try:
|
||||||
self.original.write(text)
|
self.original.write(text)
|
||||||
self.original.flush()
|
self.original.flush()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 写入文件日志
|
# 写入文件日志
|
||||||
if text.strip():
|
if text.strip():
|
||||||
self.file_logger.write_log(text.strip())
|
self.file_logger.write_log(text.strip())
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
if self.original:
|
if self.original:
|
||||||
try:
|
try:
|
||||||
self.original.flush()
|
self.original.flush()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 全局文件日志器实例
|
# 全局文件日志器实例
|
||||||
_file_logger = None
|
_file_logger = None
|
||||||
|
|
||||||
def setup_file_logging():
|
def setup_file_logging():
|
||||||
"""设置文件日志记录"""
|
"""设置文件日志记录"""
|
||||||
global _file_logger
|
global _file_logger
|
||||||
|
|
||||||
if _file_logger is None:
|
if _file_logger is None:
|
||||||
_file_logger = FileLogger()
|
_file_logger = FileLogger()
|
||||||
|
|
||||||
# 保存原始的stdout和stderr
|
# 保存原始的stdout和stderr
|
||||||
original_stdout = sys.stdout
|
original_stdout = sys.stdout
|
||||||
original_stderr = sys.stderr
|
original_stderr = sys.stderr
|
||||||
|
|
||||||
# 创建Tee输出,同时输出到原始输出和文件
|
# 创建Tee输出,同时输出到原始输出和文件
|
||||||
sys.stdout = TeeOutput(original_stdout, _file_logger)
|
sys.stdout = TeeOutput(original_stdout, _file_logger)
|
||||||
sys.stderr = TeeOutput(original_stderr, _file_logger)
|
sys.stderr = TeeOutput(original_stderr, _file_logger)
|
||||||
|
|
||||||
_file_logger.write_log("文件日志系统已设置完成")
|
_file_logger.write_log("文件日志系统已设置完成")
|
||||||
|
|
||||||
return _file_logger
|
return _file_logger
|
||||||
|
|
||||||
def log_to_file(message):
|
def log_to_file(message):
|
||||||
"""直接写入文件日志的函数"""
|
"""直接写入文件日志的函数"""
|
||||||
global _file_logger
|
global _file_logger
|
||||||
if _file_logger:
|
if _file_logger:
|
||||||
_file_logger.write_log(message)
|
_file_logger.write_log(message)
|
||||||
else:
|
else:
|
||||||
# 如果还没有初始化,先初始化
|
# 如果还没有初始化,先初始化
|
||||||
setup_file_logging()
|
setup_file_logging()
|
||||||
_file_logger.write_log(message)
|
_file_logger.write_log(message)
|
||||||
|
|
||||||
# 注释掉自动初始化,改为手动调用
|
# 注释掉自动初始化,改为手动调用
|
||||||
# if getattr(sys, 'frozen', False):
|
# if getattr(sys, 'frozen', False):
|
||||||
# setup_file_logging()
|
# setup_file_logging()
|
||||||
136
fix_pyminiracer_dll.py
Normal file
136
fix_pyminiracer_dll.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
PyMiniRacer DLL Auto-Fix Script
|
||||||
|
Automatically copy PyMiniRacer native DLL files after build
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def find_mini_racer_dll():
|
||||||
|
"""Find PyMiniRacer DLL files"""
|
||||||
|
print("Searching for PyMiniRacer DLL files...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import py_mini_racer
|
||||||
|
pkg_path = Path(py_mini_racer.__file__).parent
|
||||||
|
|
||||||
|
# Find DLL files
|
||||||
|
dll_files = list(pkg_path.glob("*.dll")) + list(pkg_path.glob("*.pyd"))
|
||||||
|
|
||||||
|
if dll_files:
|
||||||
|
print(f"OK: Found PyMiniRacer native library files:")
|
||||||
|
for dll in dll_files:
|
||||||
|
print(f" - {dll.name} ({dll.stat().st_size / 1024:.1f} KB)")
|
||||||
|
return dll_files
|
||||||
|
else:
|
||||||
|
print("WARN: No DLL files found")
|
||||||
|
# Try subdirectories
|
||||||
|
for subdir in pkg_path.iterdir():
|
||||||
|
if subdir.is_dir():
|
||||||
|
dll_files = list(subdir.glob("*.dll")) + list(subdir.glob("*.pyd"))
|
||||||
|
if dll_files:
|
||||||
|
print(f"OK: Found in subdirectory {subdir.name}:")
|
||||||
|
for dll in dll_files:
|
||||||
|
print(f" - {dll.name}")
|
||||||
|
return dll_files
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: PyMiniRacer is not installed")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to find DLL: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def copy_dll_to_dist():
|
||||||
|
"""Copy DLL files to dist directory"""
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("PyMiniRacer DLL Auto-Fix Tool")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Find dist directory
|
||||||
|
dist_dir = Path("dist/MultiPlatformGUI/_internal")
|
||||||
|
|
||||||
|
if not dist_dir.exists():
|
||||||
|
print(f"ERROR: dist directory not found: {dist_dir}")
|
||||||
|
print(" Please run: python quick_build.py or python build_production.py")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"OK: Found dist directory: {dist_dir}")
|
||||||
|
|
||||||
|
# Find source DLL
|
||||||
|
dll_files = find_mini_racer_dll()
|
||||||
|
|
||||||
|
if not dll_files:
|
||||||
|
print("\nERROR: Cannot find PyMiniRacer DLL files")
|
||||||
|
print(" Please ensure it is installed: pip install py-mini-racer")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Copy DLL files
|
||||||
|
print(f"\nCopying DLL files to dist directory...")
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
for dll_file in dll_files:
|
||||||
|
try:
|
||||||
|
# Target path (put in _internal root directory)
|
||||||
|
target = dist_dir / dll_file.name
|
||||||
|
|
||||||
|
# Copy file
|
||||||
|
shutil.copy2(dll_file, target)
|
||||||
|
print(f"OK: Copied {dll_file.name} -> {target}")
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
# Also copy to py_mini_racer subdirectory (if exists)
|
||||||
|
py_mini_racer_dir = dist_dir / "py_mini_racer"
|
||||||
|
if py_mini_racer_dir.exists():
|
||||||
|
target2 = py_mini_racer_dir / dll_file.name
|
||||||
|
shutil.copy2(dll_file, target2)
|
||||||
|
print(f"OK: Copied {dll_file.name} -> {target2}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to copy {dll_file.name}: {e}")
|
||||||
|
|
||||||
|
if success_count > 0:
|
||||||
|
print(f"\nSUCCESS: Copied {success_count} file(s)")
|
||||||
|
print("\nVerification:")
|
||||||
|
print(f" Checking if file exists: {dist_dir / 'mini_racer.dll'}")
|
||||||
|
|
||||||
|
if (dist_dir / 'mini_racer.dll').exists():
|
||||||
|
size = (dist_dir / 'mini_racer.dll').stat().st_size / (1024 * 1024)
|
||||||
|
print(f" OK: mini_racer.dll exists ({size:.1f} MB)")
|
||||||
|
|
||||||
|
print("\nYou can now run:")
|
||||||
|
print(" cd dist\\MultiPlatformGUI")
|
||||||
|
print(" .\\main.exe")
|
||||||
|
print("\n PDD platform should work properly now!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("\nERROR: No files were copied successfully")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def auto_fix_after_build():
|
||||||
|
"""Auto-fix after build (called by build script)"""
|
||||||
|
return copy_dll_to_dist()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
success = copy_dll_to_dist()
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
print("\nAlternative solutions:")
|
||||||
|
print(" 1. Manually install Node.js (to enable execjs)")
|
||||||
|
print(" 2. Check if PyMiniRacer is installed correctly")
|
||||||
|
print(" 3. Try a different version of py-mini-racer")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("\nDLL fix completed successfully!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
@@ -21,48 +21,69 @@ class NSISInstaller:
|
|||||||
self.assets_dir = self.script_dir / "assets"
|
self.assets_dir = self.script_dir / "assets"
|
||||||
self.nsis_script = self.script_dir / "installer.nsi"
|
self.nsis_script = self.script_dir / "installer.nsi"
|
||||||
|
|
||||||
# 应用程序信息
|
# 应用程序信息(使用英文避免CI/CD环境乱码)
|
||||||
self.app_name = "水滴AI客服智能助手"
|
self.app_name = "ShuiDi AI Assistant"
|
||||||
self.app_name_en = "ShuiDi AI Assistant"
|
self.app_name_en = "ShuiDi AI Assistant"
|
||||||
self.app_version = "1.0.0"
|
|
||||||
self.app_publisher = "水滴智能科技"
|
# 从 config.py 读取版本号(自动同步)
|
||||||
|
self.app_version = self._get_version_from_config()
|
||||||
|
|
||||||
|
self.app_publisher = "Shuidrop Technology"
|
||||||
self.app_url = "https://shuidrop.com/"
|
self.app_url = "https://shuidrop.com/"
|
||||||
self.exe_name = "main.exe"
|
self.exe_name = "main.exe"
|
||||||
|
|
||||||
|
def _get_version_from_config(self):
|
||||||
|
"""从 config.py 读取版本号"""
|
||||||
|
try:
|
||||||
|
config_file = self.project_root / "config.py"
|
||||||
|
if config_file.exists():
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
import re
|
||||||
|
match = re.search(r'APP_VERSION\s*=\s*["\']([^"\']+)["\']', content)
|
||||||
|
if match:
|
||||||
|
version = match.group(1)
|
||||||
|
print(f"Read version from config.py: v{version}")
|
||||||
|
return version
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: Failed to read version: {e}, using default version")
|
||||||
|
|
||||||
|
return "1.0.0"
|
||||||
|
|
||||||
def check_prerequisites(self):
|
def check_prerequisites(self):
|
||||||
"""检查构建前提条件"""
|
"""检查构建前提条件"""
|
||||||
print("🔍 检查构建前提条件...")
|
print("Checking build prerequisites...")
|
||||||
|
|
||||||
# 检查NSIS安装
|
# 检查NSIS安装
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['makensis', '/VERSION'],
|
result = subprocess.run(['makensis', '/VERSION'],
|
||||||
capture_output=True, text=True, check=True)
|
capture_output=True, text=True, check=True)
|
||||||
nsis_version = result.stdout.strip()
|
nsis_version = result.stdout.strip()
|
||||||
print(f"✅ NSIS 版本: {nsis_version}")
|
print(f"NSIS version: {nsis_version}")
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
print("❌ 错误: 未找到NSIS或makensis命令")
|
print("ERROR: NSIS or makensis command not found")
|
||||||
print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS")
|
print(" Please download and install NSIS from https://nsis.sourceforge.io/Download")
|
||||||
print(" 确保将NSIS添加到系统PATH环境变量")
|
print(" Make sure to add NSIS to system PATH")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查dist目录
|
# 检查dist目录
|
||||||
if not self.dist_dir.exists():
|
if not self.dist_dir.exists():
|
||||||
print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}")
|
print(f"ERROR: Build output directory not found: {self.dist_dir}")
|
||||||
print(" 请先运行 build_production.py 构建应用程序")
|
print(" Please run build_production.py first")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查主程序文件
|
# 检查主程序文件
|
||||||
exe_path = self.dist_dir / self.exe_name
|
exe_path = self.dist_dir / self.exe_name
|
||||||
if not exe_path.exists():
|
if not exe_path.exists():
|
||||||
print(f"❌ 错误: 未找到主程序文件 {exe_path}")
|
print(f"ERROR: Main executable not found: {exe_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(f"✅ 找到主程序: {exe_path}")
|
print(f"Found main executable: {exe_path}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def prepare_assets(self):
|
def prepare_assets(self):
|
||||||
"""准备安装包资源文件"""
|
"""准备安装包资源文件"""
|
||||||
print("📁 准备资源文件...")
|
print("Preparing asset files...")
|
||||||
|
|
||||||
# 创建assets目录
|
# 创建assets目录
|
||||||
self.assets_dir.mkdir(exist_ok=True)
|
self.assets_dir.mkdir(exist_ok=True)
|
||||||
@@ -86,14 +107,14 @@ class NSISInstaller:
|
|||||||
img = Image.open(icon_source)
|
img = Image.open(icon_source)
|
||||||
ico_path = self.assets_dir / "icon.ico"
|
ico_path = self.assets_dir / "icon.ico"
|
||||||
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
|
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
|
||||||
print(f"✅ 转换主程序图标: {icon_source.name} -> icon.ico")
|
print(f"Converted main icon: {icon_source.name} -> icon.ico")
|
||||||
icon_found = True
|
icon_found = True
|
||||||
break
|
break
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("⚠️ 未安装PIL库,跳过图标转换")
|
print("WARNING: PIL library not installed, skipping icon conversion")
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 主程序图标转换失败: {e}")
|
print(f"WARNING: Main icon conversion failed: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 转换卸载程序图标
|
# 转换卸载程序图标
|
||||||
@@ -104,17 +125,17 @@ class NSISInstaller:
|
|||||||
img = Image.open(uninstall_icon_source)
|
img = Image.open(uninstall_icon_source)
|
||||||
uninstall_ico_path = self.assets_dir / "uninstall_icon.ico"
|
uninstall_ico_path = self.assets_dir / "uninstall_icon.ico"
|
||||||
img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
|
img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
|
||||||
print(f"✅ 转换卸载程序图标: {uninstall_icon_source.name} -> uninstall_icon.ico")
|
print(f"Converted uninstall icon: {uninstall_icon_source.name} -> uninstall_icon.ico")
|
||||||
uninstall_icon_found = True
|
uninstall_icon_found = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("⚠️ 未安装PIL库,跳过卸载图标转换")
|
print("WARNING: PIL library not installed, skipping uninstall icon conversion")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 卸载程序图标转换失败: {e}")
|
print(f"WARNING: Uninstall icon conversion failed: {e}")
|
||||||
else:
|
else:
|
||||||
print("⚠️ 未找到卸载程序专用图标,将使用主程序图标")
|
print("WARNING: Uninstall icon not found, will use main icon")
|
||||||
|
|
||||||
if not icon_found:
|
if not icon_found:
|
||||||
print("⚠️ 未找到主程序图标文件,将使用默认图标")
|
print("WARNING: Main icon not found, will use default icon")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'main_icon': icon_found,
|
'main_icon': icon_found,
|
||||||
@@ -123,16 +144,24 @@ class NSISInstaller:
|
|||||||
|
|
||||||
def generate_nsis_script(self, icon_info):
|
def generate_nsis_script(self, icon_info):
|
||||||
"""生成NSIS安装脚本"""
|
"""生成NSIS安装脚本"""
|
||||||
print("📝 生成NSIS安装脚本...")
|
print("Generating NSIS installer script...")
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
# 标准化安装包命名(不带时间戳和空格,便于固定下载地址)
|
||||||
installer_name = f"{self.app_name_en}_Setup_{self.app_version}_{timestamp}.exe"
|
# 将应用名称中的空格替换为下划线
|
||||||
|
app_name_safe = self.app_name_en.replace(' ', '_')
|
||||||
|
installer_name = f"{app_name_safe}_Setup_v{self.app_version}.exe"
|
||||||
|
# 示例: ShuiDi_AI_Assistant_Setup_v1.4.12.exe
|
||||||
|
|
||||||
nsis_content = f'''# 水滴AI客服智能助手 NSIS 安装脚本
|
print(f"Installer name: {installer_name}")
|
||||||
|
|
||||||
|
nsis_content = f'''# ShuiDi AI Assistant NSIS Installer Script
|
||||||
# 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
# 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
|
|
||||||
Unicode True
|
Unicode True
|
||||||
|
|
||||||
|
# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题)
|
||||||
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
# 定义应用程序信息
|
# 定义应用程序信息
|
||||||
!define APP_NAME "{self.app_name}"
|
!define APP_NAME "{self.app_name}"
|
||||||
!define APP_NAME_EN "{self.app_name_en}"
|
!define APP_NAME_EN "{self.app_name_en}"
|
||||||
@@ -171,7 +200,7 @@ InstallDirRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallP
|
|||||||
# 版本信息
|
# 版本信息
|
||||||
VIProductVersion "{self.app_version}.0"
|
VIProductVersion "{self.app_version}.0"
|
||||||
VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}"
|
VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}"
|
||||||
VIAddVersionKey /LANG=2052 "Comments" "水滴AI客服智能助手"
|
VIAddVersionKey /LANG=2052 "Comments" "ShuiDi AI Assistant"
|
||||||
VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}"
|
VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}"
|
||||||
VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序"
|
VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序"
|
||||||
VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}"
|
VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}"
|
||||||
@@ -238,7 +267,7 @@ SectionEnd
|
|||||||
with open(self.nsis_script, 'w', encoding='utf-8-sig') as f:
|
with open(self.nsis_script, 'w', encoding='utf-8-sig') as f:
|
||||||
f.write(nsis_content)
|
f.write(nsis_content)
|
||||||
|
|
||||||
print(f"✅ NSIS脚本已生成: {self.nsis_script}")
|
print(f"NSIS script generated: {self.nsis_script}")
|
||||||
return installer_name
|
return installer_name
|
||||||
|
|
||||||
def create_license_file(self):
|
def create_license_file(self):
|
||||||
@@ -263,11 +292,11 @@ SectionEnd
|
|||||||
with open(license_file, 'w', encoding='utf-8-sig') as f:
|
with open(license_file, 'w', encoding='utf-8-sig') as f:
|
||||||
f.write(license_content)
|
f.write(license_content)
|
||||||
|
|
||||||
print(f"✅ 许可证文件已创建: {license_file}")
|
print(f"License file created: {license_file}")
|
||||||
|
|
||||||
def build_installer(self):
|
def build_installer(self):
|
||||||
"""构建安装包"""
|
"""构建安装包"""
|
||||||
print("🚀 开始构建NSIS安装包...")
|
print("Building NSIS installer...")
|
||||||
|
|
||||||
# 创建输出目录
|
# 创建输出目录
|
||||||
self.output_dir.mkdir(exist_ok=True)
|
self.output_dir.mkdir(exist_ok=True)
|
||||||
@@ -278,22 +307,22 @@ SectionEnd
|
|||||||
result = subprocess.run(cmd, cwd=str(self.script_dir),
|
result = subprocess.run(cmd, cwd=str(self.script_dir),
|
||||||
capture_output=True, text=True, check=True)
|
capture_output=True, text=True, check=True)
|
||||||
|
|
||||||
print("✅ NSIS编译成功")
|
print("NSIS compilation successful")
|
||||||
if result.stdout:
|
if result.stdout:
|
||||||
print("NSIS输出:")
|
print("NSIS output:")
|
||||||
print(result.stdout)
|
print(result.stdout)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print("❌ NSIS编译失败")
|
print("ERROR: NSIS compilation failed")
|
||||||
print(f"错误信息: {e.stderr}")
|
print(f"Error message: {e.stderr}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""执行完整的构建流程"""
|
"""执行完整的构建流程"""
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f"🔧 {self.app_name} 安装包构建工具")
|
print(f"Installer Build Tool for {self.app_name}")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -319,18 +348,18 @@ SectionEnd
|
|||||||
if installer_path.exists():
|
if installer_path.exists():
|
||||||
installer_size = installer_path.stat().st_size / (1024 * 1024)
|
installer_size = installer_path.stat().st_size / (1024 * 1024)
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("🎉 安装包构建成功!")
|
print("Installer build completed successfully!")
|
||||||
print(f"📁 安装包位置: {installer_path}")
|
print(f"Installer location: {installer_path}")
|
||||||
print(f"📏 文件大小: {installer_size:.1f} MB")
|
print(f"File size: {installer_size:.1f} MB")
|
||||||
print("🚀 可以直接分发给用户使用")
|
print("Ready to distribute to users")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("❌ 安装包文件未找到")
|
print("ERROR: Installer file not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 构建过程出错: {e}")
|
print(f"ERROR: Build process error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
206
quick_build.py
206
quick_build.py
@@ -12,54 +12,57 @@ from pathlib import Path
|
|||||||
|
|
||||||
def clean_build():
|
def clean_build():
|
||||||
"""清理构建目录"""
|
"""清理构建目录"""
|
||||||
print("🧹 清理旧的构建文件...")
|
print("Cleaning old build files...")
|
||||||
|
|
||||||
# 跳过进程结束步骤,避免影响当前脚本运行
|
# 跳过进程结束步骤,避免影响当前脚本运行
|
||||||
print("🔄 跳过进程结束步骤(避免脚本自终止)...")
|
print("Skipping process termination step...")
|
||||||
|
|
||||||
# 等待少量时间确保文件句柄释放
|
# 等待少量时间确保文件句柄释放
|
||||||
import time
|
import time
|
||||||
print("⏳ 等待文件句柄释放...")
|
print("Waiting for file handles to release...")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
dirs_to_clean = ['dist', 'build']
|
dirs_to_clean = ['dist', 'build']
|
||||||
for dir_name in dirs_to_clean:
|
for dir_name in dirs_to_clean:
|
||||||
print(f"🔍 检查目录: {dir_name}")
|
print(f"Checking directory: {dir_name}")
|
||||||
if os.path.exists(dir_name):
|
if os.path.exists(dir_name):
|
||||||
try:
|
try:
|
||||||
print(f"🗑️ 正在删除: {dir_name}")
|
print(f"Deleting: {dir_name}")
|
||||||
shutil.rmtree(dir_name)
|
shutil.rmtree(dir_name)
|
||||||
print(f"✅ 已删除: {dir_name}")
|
print(f"Deleted: {dir_name}")
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
print(f"⚠️ {dir_name} 被占用,尝试强制删除... 错误: {e}")
|
print(f"WARNING: {dir_name} is in use, attempting force delete... Error: {e}")
|
||||||
try:
|
try:
|
||||||
subprocess.run(['rd', '/s', '/q', dir_name],
|
subprocess.run(['rd', '/s', '/q', dir_name],
|
||||||
shell=True, check=True)
|
shell=True, check=True)
|
||||||
print(f"✅ 强制删除成功: {dir_name}")
|
print(f"Force delete successful: {dir_name}")
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
print(f"❌ 无法删除 {dir_name}: {e2}")
|
print(f"ERROR: Cannot delete {dir_name}: {e2}")
|
||||||
print("💡 请手动删除或运行 force_clean_dist.bat")
|
print("TIP: Please delete manually or run force_clean_dist.bat")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 删除 {dir_name} 时出错: {e}")
|
print(f"ERROR: Failed to delete {dir_name}: {e}")
|
||||||
else:
|
else:
|
||||||
print(f"ℹ️ {dir_name} 不存在,跳过")
|
print(f"INFO: {dir_name} does not exist, skipping")
|
||||||
|
|
||||||
print("✅ 清理阶段完成")
|
print("Cleaning phase completed")
|
||||||
|
|
||||||
|
|
||||||
def build_with_command():
|
def build_with_command():
|
||||||
"""使用命令行参数直接打包"""
|
"""使用命令行参数直接打包"""
|
||||||
print("🚀 开始打包...")
|
print("Starting build...")
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'pyinstaller',
|
'pyinstaller',
|
||||||
'--name=main',
|
'--name=MultiPlatformGUI', # 直接使用最终目录名
|
||||||
'--onedir', # 相当于 --exclude-binaries
|
'--onedir', # 相当于 --exclude-binaries
|
||||||
'--windowed', # 相当于 -w
|
'--windowed', # 相当于 -w
|
||||||
'--icon=static/ai_assistant_icon_64.png', # 添加主程序图标
|
'--icon=static/ai_assistant_icon_64.png', # 添加主程序图标
|
||||||
'--add-data=config.py;.',
|
'--add-data=config.py;.',
|
||||||
'--add-data=exe_file_logger.py;.',
|
'--add-data=exe_file_logger.py;.',
|
||||||
'--add-data=windows_taskbar_fix.py;.',
|
'--add-data=windows_taskbar_fix.py;.',
|
||||||
|
'--add-data=auto_updater.py;.',
|
||||||
|
'--add-data=update_dialog.py;.',
|
||||||
|
'--add-data=version_checker.py;.',
|
||||||
'--add-data=Utils/PythonNew32;Utils/PythonNew32',
|
'--add-data=Utils/PythonNew32;Utils/PythonNew32',
|
||||||
'--add-data=Utils/JD;Utils/JD',
|
'--add-data=Utils/JD;Utils/JD',
|
||||||
'--add-data=Utils/Dy;Utils/Dy',
|
'--add-data=Utils/Dy;Utils/Dy',
|
||||||
@@ -87,42 +90,44 @@ def build_with_command():
|
|||||||
'--hidden-import=WebSocket.backend_singleton',
|
'--hidden-import=WebSocket.backend_singleton',
|
||||||
'--hidden-import=WebSocket.BackendClient',
|
'--hidden-import=WebSocket.BackendClient',
|
||||||
'--hidden-import=windows_taskbar_fix',
|
'--hidden-import=windows_taskbar_fix',
|
||||||
|
'--hidden-import=py_mini_racer', # PDD平台内置JavaScript引擎
|
||||||
|
'--hidden-import=py_mini_racer.py_mini_racer',
|
||||||
|
'--collect-all=py_mini_racer', # 🔧 收集所有PyMiniRacer文件(包括DLL)
|
||||||
'main.py'
|
'main.py'
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"执行命令: {' '.join(cmd[:5])}... (共{len(cmd)}个参数)")
|
print(f"go to compile: {' '.join(cmd[:5])}... (all{len(cmd)} args)")
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print("✅ 打包成功!")
|
print("Build successful!")
|
||||||
print("📁 打包结果: dist/main/")
|
print("Build result: dist/MultiPlatformGUI/")
|
||||||
|
|
||||||
# 重命名目录为统一的输出路径
|
# 验证输出目录(不需要重命名了)
|
||||||
if os.path.exists('dist/main'):
|
if os.path.exists('dist/MultiPlatformGUI/MultiPlatformGUI.exe'):
|
||||||
|
# PyInstaller 生成的 exe 名称是 MultiPlatformGUI.exe
|
||||||
|
# 重命名为 main.exe(保持兼容性)
|
||||||
try:
|
try:
|
||||||
# 如果目标目录已存在,先删除
|
old_exe = 'dist/MultiPlatformGUI/MultiPlatformGUI.exe'
|
||||||
if os.path.exists('dist/MultiPlatformGUI'):
|
new_exe = 'dist/MultiPlatformGUI/main.exe'
|
||||||
print("🗑️ 删除旧的 MultiPlatformGUI 目录...")
|
|
||||||
shutil.rmtree('dist/MultiPlatformGUI')
|
if os.path.exists(new_exe):
|
||||||
|
os.remove(new_exe)
|
||||||
# 重命名
|
|
||||||
os.rename('dist/main', 'dist/MultiPlatformGUI')
|
os.rename(old_exe, new_exe)
|
||||||
print("✅ 已重命名为: dist/MultiPlatformGUI/")
|
print("Main executable renamed: MultiPlatformGUI.exe -> main.exe")
|
||||||
|
|
||||||
# 验证重命名结果
|
exe_size = os.path.getsize(new_exe) / 1024 / 1024
|
||||||
if os.path.exists('dist/MultiPlatformGUI/main.exe'):
|
print(f"Main executable verified: main.exe ({exe_size:.1f} MB)")
|
||||||
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"WARNING: Failed to rename main executable: {e}")
|
||||||
print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI")
|
elif os.path.exists('dist/MultiPlatformGUI/main.exe'):
|
||||||
print("📁 当前可用路径: dist/main/")
|
# 如果已经是 main.exe,直接验证
|
||||||
|
exe_size = os.path.getsize('dist/MultiPlatformGUI/main.exe') / 1024 / 1024
|
||||||
|
print(f"Main executable verified: main.exe ({exe_size:.1f} MB)")
|
||||||
else:
|
else:
|
||||||
print("⚠️ 未找到 dist/main 目录,重命名跳过")
|
print("WARNING: Main executable file not found")
|
||||||
|
|
||||||
# 创建使用说明
|
# 创建使用说明
|
||||||
try:
|
try:
|
||||||
@@ -132,14 +137,14 @@ def build_with_command():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("❌ 打包失败")
|
print("ERROR: Build failed")
|
||||||
if result.stderr:
|
if result.stderr:
|
||||||
print("错误信息:")
|
print("Error messages:")
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 打包过程出错: {e}")
|
print(f"ERROR: Build process error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -198,9 +203,9 @@ MultiPlatformGUI/
|
|||||||
try:
|
try:
|
||||||
with open('dist/MultiPlatformGUI/使用说明.txt', 'w', encoding='utf-8') as f:
|
with open('dist/MultiPlatformGUI/使用说明.txt', 'w', encoding='utf-8') as f:
|
||||||
f.write(guide_content)
|
f.write(guide_content)
|
||||||
print("✅ 已生成使用说明文件")
|
print("Usage guide file generated")
|
||||||
except:
|
except:
|
||||||
print("⚠️ 使用说明生成失败")
|
print("WARNING: Usage guide generation failed")
|
||||||
|
|
||||||
|
|
||||||
def create_usage_guide_fallback():
|
def create_usage_guide_fallback():
|
||||||
@@ -228,139 +233,148 @@ cd dist/main
|
|||||||
try:
|
try:
|
||||||
with open('dist/main/使用说明.txt', 'w', encoding='utf-8') as f:
|
with open('dist/main/使用说明.txt', 'w', encoding='utf-8') as f:
|
||||||
f.write(guide_content)
|
f.write(guide_content)
|
||||||
print("✅ 已生成使用说明文件 (备用路径)")
|
print("Usage guide file generated (fallback path)")
|
||||||
except:
|
except:
|
||||||
print("⚠️ 使用说明生成失败")
|
print("WARNING: Usage guide generation failed")
|
||||||
|
|
||||||
|
|
||||||
def verify_result():
|
def verify_result():
|
||||||
"""验证打包结果"""
|
"""验证打包结果"""
|
||||||
print("\n🔍 验证打包结果...")
|
print("\nVerifying build result...")
|
||||||
|
|
||||||
# 优先检查MultiPlatformGUI(标准输出目录)
|
# 检查MultiPlatformGUI目录(统一输出目录)
|
||||||
target_dir = "dist/MultiPlatformGUI"
|
target_dir = "dist/MultiPlatformGUI"
|
||||||
fallback_dir = "dist/main"
|
|
||||||
|
|
||||||
if os.path.exists(target_dir):
|
if os.path.exists(target_dir):
|
||||||
return _verify_directory(target_dir)
|
return _verify_directory(target_dir)
|
||||||
elif os.path.exists(fallback_dir):
|
|
||||||
print(f"⚠️ 发现备用目录: {fallback_dir}")
|
|
||||||
print("💡 建议重命名为: dist/MultiPlatformGUI")
|
|
||||||
return _verify_directory(fallback_dir)
|
|
||||||
else:
|
else:
|
||||||
print("❌ 未找到任何打包输出目录")
|
print("ERROR: Build output directory not found: dist/MultiPlatformGUI")
|
||||||
|
print("TIP: Please check if PyInstaller executed successfully")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _verify_directory(base_dir):
|
def _verify_directory(base_dir):
|
||||||
"""验证指定目录的打包结果"""
|
"""验证指定目录的打包结果"""
|
||||||
|
# 检查可能的 exe 名称
|
||||||
exe_path = f"{base_dir}/main.exe"
|
exe_path = f"{base_dir}/main.exe"
|
||||||
|
if not os.path.exists(exe_path):
|
||||||
|
exe_path = f"{base_dir}/MultiPlatformGUI.exe"
|
||||||
|
|
||||||
internal_dir = f"{base_dir}/_internal"
|
internal_dir = f"{base_dir}/_internal"
|
||||||
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll"
|
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll"
|
||||||
static_dir = f"{base_dir}/_internal/static"
|
static_dir = f"{base_dir}/_internal/static"
|
||||||
|
|
||||||
print(f"📁 验证目录: {base_dir}")
|
print(f"Verifying directory: {base_dir}")
|
||||||
|
|
||||||
# 检查主程序
|
# 检查主程序
|
||||||
if os.path.exists(exe_path):
|
if os.path.exists(exe_path):
|
||||||
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
|
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
|
||||||
print(f"✅ 主程序: main.exe ({size:.1f} MB)")
|
print(f"Main executable: main.exe ({size:.1f} MB)")
|
||||||
else:
|
else:
|
||||||
print("❌ 主程序 main.exe 不存在")
|
print("ERROR: Main executable main.exe does not exist")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查_internal目录
|
# 检查_internal目录
|
||||||
if os.path.exists(internal_dir):
|
if os.path.exists(internal_dir):
|
||||||
file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))])
|
file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))])
|
||||||
dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))])
|
dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))])
|
||||||
print(f"✅ 依赖目录: _internal/ ({file_count} 个文件, {dir_count} 个子目录)")
|
print(f"Dependencies directory: _internal/ ({file_count} files, {dir_count} subdirectories)")
|
||||||
else:
|
else:
|
||||||
print("❌ _internal 目录不存在")
|
print("ERROR: _internal directory does not exist")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查关键DLL(可选)
|
# 检查关键DLL(可选)
|
||||||
if os.path.exists(dll_path):
|
if os.path.exists(dll_path):
|
||||||
dll_size = os.path.getsize(dll_path) / 1024 # KB
|
dll_size = os.path.getsize(dll_path) / 1024 # KB
|
||||||
print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:.1f} KB)")
|
print(f"QianNiu DLL: SaiNiuApi.dll ({dll_size:.1f} KB)")
|
||||||
else:
|
else:
|
||||||
print("⚠️ 千牛DLL不存在(可能正常)")
|
print("WARNING: QianNiu DLL does not exist (may be normal)")
|
||||||
|
|
||||||
# 检查静态资源
|
# 检查静态资源
|
||||||
if os.path.exists(static_dir):
|
if os.path.exists(static_dir):
|
||||||
icon_files = list(Path(static_dir).glob("*.png"))
|
icon_files = list(Path(static_dir).glob("*.png"))
|
||||||
print(f"✅ 静态资源: static/ ({len(icon_files)} 个图标文件)")
|
print(f"Static resources: static/ ({len(icon_files)} icon files)")
|
||||||
else:
|
else:
|
||||||
print("⚠️ static 目录不存在")
|
print("WARNING: static directory does not exist")
|
||||||
|
|
||||||
print(f"✅ 目录 {base_dir} 验证通过")
|
print(f"Directory {base_dir} verification passed")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
print("🔥 多平台客服GUI快速打包工具")
|
print("Multi-Platform Customer Service GUI Quick Build Tool")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 检查依赖
|
# 检查依赖
|
||||||
print("🔍 检查打包依赖...")
|
print("Checking build dependencies...")
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
result = subprocess.run(['pyinstaller', '--version'],
|
result = subprocess.run(['pyinstaller', '--version'],
|
||||||
capture_output=True, text=True, check=False)
|
capture_output=True, text=True, check=False)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"✅ PyInstaller 版本: {result.stdout.strip()}")
|
print(f"PyInstaller version: {result.stdout.strip()}")
|
||||||
else:
|
else:
|
||||||
print("❌ PyInstaller 未安装或不可用")
|
print("ERROR: PyInstaller not installed or unavailable")
|
||||||
print("💡 请运行: pip install pyinstaller")
|
print("TIP: Run: pip install pyinstaller")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 检查PyInstaller时出错: {e}")
|
print(f"ERROR: Failed to check PyInstaller: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 清理
|
# 清理
|
||||||
print("\n📍 开始清理阶段...")
|
print("\nCleaning phase started...")
|
||||||
clean_build()
|
clean_build()
|
||||||
print("📍 清理阶段完成")
|
print("Cleaning phase completed")
|
||||||
|
|
||||||
# 打包
|
# 打包
|
||||||
print("\n📍 开始打包阶段...")
|
print("\nBuild phase started...")
|
||||||
if not build_with_command():
|
if not build_with_command():
|
||||||
print("❌ 打包阶段失败")
|
print("ERROR: Build phase failed")
|
||||||
return False
|
return False
|
||||||
print("📍 打包阶段完成")
|
print("Build phase completed")
|
||||||
|
|
||||||
# 验证
|
# 验证
|
||||||
print("\n📍 开始验证阶段...")
|
print("\nVerification phase started...")
|
||||||
if not verify_result():
|
if not verify_result():
|
||||||
print("❌ 验证阶段失败")
|
print("ERROR: Verification phase failed")
|
||||||
return False
|
return False
|
||||||
print("📍 验证阶段完成")
|
print("Verification phase completed")
|
||||||
|
|
||||||
|
# 🔧 修复PyMiniRacer DLL
|
||||||
|
print("\nPyMiniRacer DLL fix phase started...")
|
||||||
|
try:
|
||||||
|
import fix_pyminiracer_dll
|
||||||
|
if fix_pyminiracer_dll.auto_fix_after_build():
|
||||||
|
print("PyMiniRacer DLL fix completed")
|
||||||
|
else:
|
||||||
|
print("WARNING: PyMiniRacer DLL fix failed - PDD platform may not work without Node.js")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: PyMiniRacer DLL fix error: {e}")
|
||||||
|
print(" PDD platform may need Node.js environment")
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("🎉 打包完成!")
|
print("Build completed successfully!")
|
||||||
|
|
||||||
# 智能显示可用路径
|
# 显示打包结果
|
||||||
if os.path.exists("dist/MultiPlatformGUI"):
|
if os.path.exists("dist/MultiPlatformGUI"):
|
||||||
print("📁 打包结果: dist/MultiPlatformGUI/")
|
print("Build result: dist/MultiPlatformGUI/")
|
||||||
print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe")
|
print("Run command: cd dist/MultiPlatformGUI && .\\main.exe")
|
||||||
print("📦 安装包构建: python installer/build_installer.py")
|
print("Create installer: python installer/build_installer.py")
|
||||||
elif os.path.exists("dist/main"):
|
|
||||||
print("📁 打包结果: dist/main/")
|
|
||||||
print("🚀 运行方式: cd dist/main && .\\main.exe")
|
|
||||||
print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI")
|
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 打包失败: {e}")
|
print(f"ERROR: Build failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = main()
|
success = main()
|
||||||
if success:
|
if success:
|
||||||
print("\n✅ 可以开始测试了!")
|
print("\nReady for testing!")
|
||||||
else:
|
else:
|
||||||
print("\n❌ 请检查错误信息并重试")
|
print("\nERROR: Please check error messages and retry")
|
||||||
BIN
static/hengfu.jpg
Normal file
BIN
static/hengfu.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
@@ -1,4 +1,4 @@
|
|||||||
window = globalThis;;
|
window = globalThis;
|
||||||
|
|
||||||
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
||||||
var C = {};
|
var C = {};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
window = globalThis;;
|
window = globalThis;
|
||||||
|
|
||||||
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
||||||
var C = {};
|
var C = {};
|
||||||
|
|||||||
474
static/js/sign.js
Normal file
474
static/js/sign.js
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
|
||||||
|
var CryptoJS = CryptoJS || (function (Math, undefined) {
|
||||||
|
var C = {};
|
||||||
|
var C_lib = C.lib = {};
|
||||||
|
var Base = C_lib.Base = (function () {
|
||||||
|
function F() {};
|
||||||
|
return {
|
||||||
|
extend: function (overrides) {
|
||||||
|
F.prototype = this;
|
||||||
|
var subtype = new F();
|
||||||
|
if (overrides) {
|
||||||
|
subtype.mixIn(overrides);
|
||||||
|
}
|
||||||
|
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
|
||||||
|
subtype.init = function () {
|
||||||
|
subtype.$super.init.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
subtype.init.prototype = subtype;
|
||||||
|
subtype.$super = this;
|
||||||
|
return subtype;
|
||||||
|
}, create: function () {
|
||||||
|
var instance = this.extend();
|
||||||
|
instance.init.apply(instance, arguments);
|
||||||
|
return instance;
|
||||||
|
}, init: function () {}, mixIn: function (properties) {
|
||||||
|
for (var propertyName in properties) {
|
||||||
|
if (properties.hasOwnProperty(propertyName)) {
|
||||||
|
this[propertyName] = properties[propertyName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (properties.hasOwnProperty('toString')) {
|
||||||
|
this.toString = properties.toString;
|
||||||
|
}
|
||||||
|
}, clone: function () {
|
||||||
|
return this.init.prototype.extend(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
var WordArray = C_lib.WordArray = Base.extend({
|
||||||
|
init: function (words, sigBytes) {
|
||||||
|
words = this.words = words || [];
|
||||||
|
if (sigBytes != undefined) {
|
||||||
|
this.sigBytes = sigBytes;
|
||||||
|
} else {
|
||||||
|
this.sigBytes = words.length * 4;
|
||||||
|
}
|
||||||
|
}, toString: function (encoder) {
|
||||||
|
return (encoder || Hex).stringify(this);
|
||||||
|
}, concat: function (wordArray) {
|
||||||
|
var thisWords = this.words;
|
||||||
|
var thatWords = wordArray.words;
|
||||||
|
var thisSigBytes = this.sigBytes;
|
||||||
|
var thatSigBytes = wordArray.sigBytes;
|
||||||
|
this.clamp();
|
||||||
|
if (thisSigBytes % 4) {
|
||||||
|
for (var i = 0; i < thatSigBytes; i++) {
|
||||||
|
var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
|
||||||
|
}
|
||||||
|
} else if (thatWords.length > 0xffff) {
|
||||||
|
for (var i = 0; i < thatSigBytes; i += 4) {
|
||||||
|
thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thisWords.push.apply(thisWords, thatWords);
|
||||||
|
}
|
||||||
|
this.sigBytes += thatSigBytes;
|
||||||
|
return this;
|
||||||
|
}, clamp: function () {
|
||||||
|
var words = this.words;
|
||||||
|
var sigBytes = this.sigBytes;
|
||||||
|
words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
|
||||||
|
words.length = Math.ceil(sigBytes / 4);
|
||||||
|
}, clone: function () {
|
||||||
|
var clone = Base.clone.call(this);
|
||||||
|
clone.words = this.words.slice(0);
|
||||||
|
return clone;
|
||||||
|
}, random: function (nBytes) {
|
||||||
|
var words = [];
|
||||||
|
var r = (function (m_w) {
|
||||||
|
var m_w = m_w;
|
||||||
|
var m_z = 0x3ade68b1;
|
||||||
|
var mask = 0xffffffff;
|
||||||
|
return function () {
|
||||||
|
m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
|
||||||
|
m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
|
||||||
|
var result = ((m_z << 0x10) + m_w) & mask;
|
||||||
|
result /= 0x100000000;
|
||||||
|
result += 0.5;
|
||||||
|
return result * (Math.random() > .5 ? 1 : -1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (var i = 0, rcache; i < nBytes; i += 4) {
|
||||||
|
var _r = r((rcache || Math.random()) * 0x100000000);
|
||||||
|
rcache = _r() * 0x3ade67b7;
|
||||||
|
words.push((_r() * 0x100000000) | 0);
|
||||||
|
}
|
||||||
|
return new WordArray.init(words, nBytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var C_enc = C.enc = {};
|
||||||
|
var Hex = C_enc.Hex = {
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
var words = wordArray.words;
|
||||||
|
var sigBytes = wordArray.sigBytes;
|
||||||
|
var hexChars = [];
|
||||||
|
for (var i = 0; i < sigBytes; i++) {
|
||||||
|
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
hexChars.push((bite >>> 4).toString(16));
|
||||||
|
hexChars.push((bite & 0x0f).toString(16));
|
||||||
|
}
|
||||||
|
return hexChars.join('');
|
||||||
|
}, parse: function (hexStr) {
|
||||||
|
var hexStrLength = hexStr.length;
|
||||||
|
var words = [];
|
||||||
|
for (var i = 0; i < hexStrLength; i += 2) {
|
||||||
|
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
|
||||||
|
}
|
||||||
|
return new WordArray.init(words, hexStrLength / 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var Latin1 = C_enc.Latin1 = {
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
var words = wordArray.words;
|
||||||
|
var sigBytes = wordArray.sigBytes;
|
||||||
|
var latin1Chars = [];
|
||||||
|
for (var i = 0; i < sigBytes; i++) {
|
||||||
|
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
|
||||||
|
latin1Chars.push(String.fromCharCode(bite));
|
||||||
|
}
|
||||||
|
return latin1Chars.join('');
|
||||||
|
}, parse: function (latin1Str) {
|
||||||
|
var latin1StrLength = latin1Str.length;
|
||||||
|
var words = [];
|
||||||
|
for (var i = 0; i < latin1StrLength; i++) {
|
||||||
|
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
|
||||||
|
}
|
||||||
|
return new WordArray.init(words, latin1StrLength);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var Utf8 = C_enc.Utf8 = {
|
||||||
|
stringify: function (wordArray) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(Latin1.stringify(wordArray)));
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Malformed UTF-8 data');
|
||||||
|
}
|
||||||
|
}, parse: function (utf8Str) {
|
||||||
|
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
|
||||||
|
reset: function () {
|
||||||
|
this._data = new WordArray.init();
|
||||||
|
this._nDataBytes = 0;
|
||||||
|
}, _append: function (data) {
|
||||||
|
if (typeof data == 'string') {
|
||||||
|
data = Utf8.parse(data);
|
||||||
|
}
|
||||||
|
this._data.concat(data);
|
||||||
|
this._nDataBytes += data.sigBytes;
|
||||||
|
}, _process: function (doFlush) {
|
||||||
|
var data = this._data;
|
||||||
|
var dataWords = data.words;
|
||||||
|
var dataSigBytes = data.sigBytes;
|
||||||
|
var blockSize = this.blockSize;
|
||||||
|
var blockSizeBytes = blockSize * 4;
|
||||||
|
var nBlocksReady = dataSigBytes / blockSizeBytes;
|
||||||
|
if (doFlush) {
|
||||||
|
nBlocksReady = Math.ceil(nBlocksReady);
|
||||||
|
} else {
|
||||||
|
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
|
||||||
|
}
|
||||||
|
var nWordsReady = nBlocksReady * blockSize;
|
||||||
|
var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
|
||||||
|
if (nWordsReady) {
|
||||||
|
for (var offset = 0; offset < nWordsReady; offset += blockSize) {
|
||||||
|
this._doProcessBlock(dataWords, offset);
|
||||||
|
}
|
||||||
|
var processedWords = dataWords.splice(0, nWordsReady);
|
||||||
|
data.sigBytes -= nBytesReady;
|
||||||
|
}
|
||||||
|
return new WordArray.init(processedWords, nBytesReady);
|
||||||
|
}, clone: function () {
|
||||||
|
var clone = Base.clone.call(this);
|
||||||
|
clone._data = this._data.clone();
|
||||||
|
return clone;
|
||||||
|
}, _minBufferSize: 0
|
||||||
|
});
|
||||||
|
var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
|
||||||
|
cfg: Base.extend(),
|
||||||
|
init: function (cfg) {
|
||||||
|
this.cfg = this.cfg.extend(cfg);
|
||||||
|
this.reset();
|
||||||
|
}, reset: function () {
|
||||||
|
BufferedBlockAlgorithm.reset.call(this);
|
||||||
|
this._doReset();
|
||||||
|
}, update: function (messageUpdate) {
|
||||||
|
this._append(messageUpdate);
|
||||||
|
this._process();
|
||||||
|
return this;
|
||||||
|
}, finalize: function (messageUpdate) {
|
||||||
|
if (messageUpdate) {
|
||||||
|
this._append(messageUpdate);
|
||||||
|
}
|
||||||
|
var hash = this._doFinalize();
|
||||||
|
return hash;
|
||||||
|
}, blockSize: 512 / 32,
|
||||||
|
_createHelper: function (hasher) {
|
||||||
|
return function (message, cfg) {
|
||||||
|
return new hasher.init(cfg).finalize(message);
|
||||||
|
};
|
||||||
|
}, _createHmacHelper: function (hasher) {
|
||||||
|
return function (message, key) {
|
||||||
|
return new C_algo.HMAC.init(hasher, key).finalize(message);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var C_algo = C.algo = {};
|
||||||
|
return C;
|
||||||
|
}(Math));
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var C = CryptoJS;
|
||||||
|
var C_lib = C.lib;
|
||||||
|
var Base = C_lib.Base;
|
||||||
|
var C_enc = C.enc;
|
||||||
|
var Utf8 = C_enc.Utf8;
|
||||||
|
var C_algo = C.algo;
|
||||||
|
var HMAC = C_algo.HMAC = Base.extend({
|
||||||
|
init: function (hasher, key) {
|
||||||
|
hasher = this._hasher = new hasher.init();
|
||||||
|
if (typeof key == 'string') {
|
||||||
|
key = Utf8.parse(key);
|
||||||
|
}
|
||||||
|
var hasherBlockSize = hasher.blockSize;
|
||||||
|
var hasherBlockSizeBytes = hasherBlockSize * 4;
|
||||||
|
if (key.sigBytes > hasherBlockSizeBytes) {
|
||||||
|
key = hasher.finalize(key);
|
||||||
|
}
|
||||||
|
key.clamp();
|
||||||
|
var oKey = this._oKey = key.clone();
|
||||||
|
var iKey = this._iKey = key.clone();
|
||||||
|
var oKeyWords = oKey.words;
|
||||||
|
var iKeyWords = iKey.words;
|
||||||
|
for (var i = 0; i < hasherBlockSize; i++) {
|
||||||
|
oKeyWords[i] ^= 0x5c5c5c5c;
|
||||||
|
iKeyWords[i] ^= 0x36363636;
|
||||||
|
}
|
||||||
|
oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;
|
||||||
|
this.reset();
|
||||||
|
}, reset: function () {
|
||||||
|
var hasher = this._hasher;
|
||||||
|
hasher.reset();
|
||||||
|
hasher.update(this._iKey);
|
||||||
|
}, update: function (messageUpdate) {
|
||||||
|
this._hasher.update(messageUpdate);
|
||||||
|
return this;
|
||||||
|
}, finalize: function (messageUpdate) {
|
||||||
|
var hasher = this._hasher;
|
||||||
|
var innerHash = hasher.finalize(messageUpdate);
|
||||||
|
hasher.reset();
|
||||||
|
var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));
|
||||||
|
return hmac;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
|
||||||
|
(function (Math) {
|
||||||
|
var C = CryptoJS;
|
||||||
|
var C_lib = C.lib;
|
||||||
|
var WordArray = C_lib.WordArray;
|
||||||
|
var Hasher = C_lib.Hasher;
|
||||||
|
var C_algo = C.algo;
|
||||||
|
var H = [];
|
||||||
|
var K = [];
|
||||||
|
(function () {
|
||||||
|
function isPrime(n) {
|
||||||
|
var sqrtN = Math.sqrt(n);
|
||||||
|
for (var factor = 2; factor <= sqrtN; factor++) {
|
||||||
|
if (!(n % factor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function getFractionalBits(n) {
|
||||||
|
return ((n - (n | 0)) * 0x100000000) | 0;
|
||||||
|
}
|
||||||
|
var n = 2;
|
||||||
|
var nPrime = 0;
|
||||||
|
while (nPrime < 64) {
|
||||||
|
if (isPrime(n)) {
|
||||||
|
if (nPrime < 8) {
|
||||||
|
H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
|
||||||
|
}
|
||||||
|
K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
|
||||||
|
nPrime++;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
var W = [];
|
||||||
|
var SHA256 = C_algo.SHA256 = Hasher.extend({
|
||||||
|
_doReset: function () {
|
||||||
|
this._hash = new WordArray.init(H.slice(0));
|
||||||
|
}, _doProcessBlock: function (M, offset) {
|
||||||
|
var H = this._hash.words;
|
||||||
|
var a = H[0];
|
||||||
|
var b = H[1];
|
||||||
|
var c = H[2];
|
||||||
|
var d = H[3];
|
||||||
|
var e = H[4];
|
||||||
|
var f = H[5];
|
||||||
|
var g = H[6];
|
||||||
|
var h = H[7];
|
||||||
|
for (var i = 0; i < 64; i++) {
|
||||||
|
if (i < 16) {
|
||||||
|
W[i] = M[offset + i] | 0;
|
||||||
|
} else {
|
||||||
|
var gamma0x = W[i - 15];
|
||||||
|
var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3);
|
||||||
|
var gamma1x = W[i - 2];
|
||||||
|
var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10);
|
||||||
|
W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
|
||||||
|
}
|
||||||
|
var ch = (e & f) ^ (~e & g);
|
||||||
|
var maj = (a & b) ^ (a & c) ^ (b & c);
|
||||||
|
var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
|
||||||
|
var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25));
|
||||||
|
var t1 = h + sigma1 + ch + K[i] + W[i];
|
||||||
|
var t2 = sigma0 + maj;
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = (d + t1) | 0;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = (t1 + t2) | 0;
|
||||||
|
}
|
||||||
|
H[0] = (H[0] + a) | 0;
|
||||||
|
H[1] = (H[1] + b) | 0;
|
||||||
|
H[2] = (H[2] + c) | 0;
|
||||||
|
H[3] = (H[3] + d) | 0;
|
||||||
|
H[4] = (H[4] + e) | 0;
|
||||||
|
H[5] = (H[5] + f) | 0;
|
||||||
|
H[6] = (H[6] + g) | 0;
|
||||||
|
H[7] = (H[7] + h) | 0;
|
||||||
|
}, _doFinalize: function () {
|
||||||
|
var data = this._data;
|
||||||
|
var dataWords = data.words;
|
||||||
|
var nBitsTotal = this._nDataBytes * 8;
|
||||||
|
var nBitsLeft = data.sigBytes * 8;
|
||||||
|
dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
|
||||||
|
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
|
||||||
|
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
|
||||||
|
data.sigBytes = dataWords.length * 4;
|
||||||
|
this._process();
|
||||||
|
return this._hash;
|
||||||
|
}, clone: function () {
|
||||||
|
var clone = Hasher.clone.call(this);
|
||||||
|
clone._hash = this._hash.clone();
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
C.SHA256 = Hasher._createHelper(SHA256);
|
||||||
|
C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
|
||||||
|
}(Math));
|
||||||
|
|
||||||
|
|
||||||
|
function u(e) {
|
||||||
|
try {
|
||||||
|
return encodeURIComponent(e).replace(/[^A-Za-z0-9_.~\-%]+/g, escape).replace(/[*]/g, (function(e) {
|
||||||
|
return "%".concat(e.charCodeAt(0).toString(16).toUpperCase())
|
||||||
|
}
|
||||||
|
))
|
||||||
|
} catch (e) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function d(e) {
|
||||||
|
return Object.keys(e).sort().map((function(t) {
|
||||||
|
var n = e[t];
|
||||||
|
if (null != n) {
|
||||||
|
var r = u(t);
|
||||||
|
if (r)
|
||||||
|
return Array.isArray(n) ? "".concat(r, "=").concat(n.map(u).sort().join("&".concat(r, "="))) : "".concat(r, "=").concat(u(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)).filter((function(e) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
)).join("&")
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var glo = ""
|
||||||
|
|
||||||
|
function canonicalString(method, params, x_amz_date, x_amz_security_token, data){
|
||||||
|
var date = "x-amz-date:" + x_amz_date;
|
||||||
|
var token = "x-amz-security-token:" + x_amz_security_token;
|
||||||
|
|
||||||
|
if (data){
|
||||||
|
var sha512 = CryptoJS.SHA256(JSON.stringify(data).replace(" ", "")).toString()
|
||||||
|
glo = sha512;
|
||||||
|
var content = "x-amz-content-sha256:" + sha512;
|
||||||
|
var sss = content + "\n" + date + "\n" + token
|
||||||
|
var ttt = JSON.stringify(data).replace(" ", "");
|
||||||
|
var zzz = "x-amz-content-sha256;x-amz-date;x-amz-security-token"
|
||||||
|
}else{
|
||||||
|
var sss = date + "\n" + token
|
||||||
|
var zzz = "x-amz-date;x-amz-security-token"
|
||||||
|
var ttt = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var e = []
|
||||||
|
, t = "/";
|
||||||
|
return e.push(method),
|
||||||
|
e.push(t),
|
||||||
|
e.push(d(params) || ""),
|
||||||
|
e.push("".concat((sss), "\n")),
|
||||||
|
e.push(zzz),
|
||||||
|
e.push(CryptoJS.SHA256(ttt)),
|
||||||
|
e.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data){
|
||||||
|
var t = [];
|
||||||
|
return t.push("AWS4-HMAC-SHA256"),
|
||||||
|
t.push(x_amz_date),
|
||||||
|
t.push(key[0] + "/cn-north-1/" + type + "/aws4_request"),
|
||||||
|
t.push(CryptoJS.SHA256(canonicalString(method, params, x_amz_date, x_amz_security_token, data))),
|
||||||
|
t.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function signature(method, type, secret_access_key, x_amz_date, x_amz_security_token, params, data){
|
||||||
|
var key = x_amz_date.split("T");
|
||||||
|
var o = CryptoJS.HmacSHA256(key[0], secret_access_key);
|
||||||
|
var i = CryptoJS.HmacSHA256("cn-north-1", o);
|
||||||
|
var s = CryptoJS.HmacSHA256(type, i);
|
||||||
|
var n = CryptoJS.HmacSHA256("aws4_request", s);
|
||||||
|
|
||||||
|
return CryptoJS.HmacSHA256(stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data), n).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(signature("POST", "vod", "AWS4D2OsX7tT/swv6JehKT62QmrUjrnR+KYH63SNma7buX3P46UoZqcHH+JHvykozVlr", "20251024T033313Z", "STS2eyJMVEFjY2Vzc0tleUlEIjoiQUtMVE5tRmhZVGhsWTJJM1lqbGpORGc0WkRsbE1UTXlZelJoWkRkaU1EaGlaV1EiLCJBY2Nlc3NLZXlJRCI6IkFLVFBOVEF6TVRSalpqSTNZV00wTkRjd1lUazBNak14WVRrelpEUXhPRGxsWXpjIiwiU2lnbmVkU2VjcmV0QWNjZXNzS2V5IjoiTWcrU1laeXlCajNyc05UL0dKaFlHYTcxUEloWURvMms1Z1J4Q2QyQk5nUDV6NXdZaWpyeEhwUmYzT1dKNW9PRWNwK3FCTVNGNXhsaG40Y3hhOHhDZmkrQWgvZU0wWS95QVhqbGhMZWJmYU09IiwiRXhwaXJlZFRpbWUiOjE3NjEyODAxNDksIlBvbGljeVN0cmluZyI6IntcIlN0YXRlbWVudFwiOlt7XCJFZmZlY3RcIjpcIkFsbG93XCIsXCJBY3Rpb25cIjpbXCJ2b2Q6QXBwbHlVcGxvYWRcIixcInZvZDpBcHBseVVwbG9hZElubmVyXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkSW5uZXJcIixcInZvZDpHZXRVcGxvYWRDYW5kaWRhdGVzXCIsXCJJbWFnZVg6QXBwbHlJbWFnZVVwbG9hZFwiLFwiSW1hZ2VYOkNvbW1pdEltYWdlVXBsb2FkXCIsXCJJbWFnZVg6QXBwbHlVcGxvYWRJbWFnZUZpbGVcIixcIkltYWdlWDpDb21taXRVcGxvYWRJbWFnZUZpbGVcIl0sXCJSZXNvdXJjZVwiOltcIipcIl0sXCJDb25kaXRpb25cIjpcIntcXFwiUFNNXFxcIjpcXFwiY21wLmVjb20ucGlnZW9uX2FwaVxcXCJ9XCJ9XX0iLCJTaWduYXR1cmUiOiIxOTBmNzkxZTU2NTk0MzgwOGIzNTllYTJhOWI4OGExOTAyZDE5MTEwODBhMmNmYmVhMTQwZmVmYWNhYTViNGU5In0=", {
|
||||||
|
// "Action": "CommitUploadInner",
|
||||||
|
// "Version": "2020-11-19",
|
||||||
|
// "SpaceName": "pigeon-video"
|
||||||
|
// }, {
|
||||||
|
// "SessionKey": "eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyNzY3OTI3Nzk2NDYzMTUiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlWSnJSM2xRUkVNaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VG1wTmVFOVVTWE5KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2V1hwTlZHdDVURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUbENZbTFhVDFKWFZrWlJibXg0WWxab1EwOUZXbTVoVmtGNlVrZHNTVk51UW5CbFdGcElZakJHVlZaVVVYZGhSVTB5U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM0eVh6UlNVakpXYkROeWN6SmFUV3B5ZGt4eFEyTmxkVE54WTI1RWFGa3RPWGhmU1RkTGFtRXdUVE5KSW4wPTo0ZTU0MTNhZTY2OTFjM2U2ZTU2ODc0NGEyYmQzNzZiMDFiODY0YjI4OGRhODg0MzY4ZDA1YzJiMTE4ZWE1MGM4IiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L29BbmZORWVFQnlxbVhCOEZnaVAzRGlISnBpeXZHb0FUVTQwaEM2IiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0ZjZ1N29nNjVvcGJ1YWk1cTAifQ==",
|
||||||
|
// "Functions": [
|
||||||
|
// {
|
||||||
|
// "name": "GetMeta"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "name": "Snapshot",
|
||||||
|
// "input": {
|
||||||
|
// "SnapshotTime": 0
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }))
|
||||||
|
|
||||||
|
//console.log(d({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]}))
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(CryptoJS.SHA256(JSON.stringify({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]}).replace(" ")).toString())
|
||||||
518
update_dialog.py
Normal file
518
update_dialog.py
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
现代化圆形进度条更新对话框
|
||||||
|
展示完整更新流程:下载 → UAC授权 → 安装 → 重启
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel,
|
||||||
|
QPushButton, QHBoxLayout, QWidget)
|
||||||
|
from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty
|
||||||
|
from PyQt5.QtGui import QFont, QPainter, QColor, QPen, QLinearGradient, QConicalGradient
|
||||||
|
from PyQt5.QtCore import QRectF, QPointF
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
class CircularProgressBar(QWidget):
|
||||||
|
"""
|
||||||
|
自定义圆形进度条组件
|
||||||
|
|
||||||
|
特性:
|
||||||
|
- 顺时针旋转动画
|
||||||
|
- 渐变色圆环
|
||||||
|
- 中心显示百分比
|
||||||
|
- 支持平滑过渡
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._value = 0 # 当前进度值(0-100)
|
||||||
|
self._max_value = 100
|
||||||
|
self._text = "0%"
|
||||||
|
self.setMinimumSize(200, 200)
|
||||||
|
|
||||||
|
# 动画效果
|
||||||
|
self.animation = QPropertyAnimation(self, b"value")
|
||||||
|
self.animation.setDuration(500) # 500ms过渡动画
|
||||||
|
self.animation.setEasingCurve(QEasingCurve.OutCubic)
|
||||||
|
|
||||||
|
@pyqtProperty(int)
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, val):
|
||||||
|
self._value = val
|
||||||
|
self.update() # 触发重绘
|
||||||
|
|
||||||
|
def setValue(self, val):
|
||||||
|
"""设置进度值(带动画)"""
|
||||||
|
if val != self._value:
|
||||||
|
self.animation.stop()
|
||||||
|
self.animation.setStartValue(self._value)
|
||||||
|
self.animation.setEndValue(val)
|
||||||
|
self.animation.start()
|
||||||
|
|
||||||
|
def setText(self, text):
|
||||||
|
"""设置中心文本"""
|
||||||
|
self._text = text
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""绘制圆形进度条"""
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿
|
||||||
|
|
||||||
|
# 计算中心和半径
|
||||||
|
width = self.width()
|
||||||
|
height = self.height()
|
||||||
|
size = min(width, height)
|
||||||
|
center = QPointF(width / 2, height / 2)
|
||||||
|
radius = size / 2 - 15 # 留出边距
|
||||||
|
|
||||||
|
# 绘制背景圆环(灰色)
|
||||||
|
pen = QPen()
|
||||||
|
pen.setWidth(12)
|
||||||
|
pen.setColor(QColor(230, 230, 230))
|
||||||
|
pen.setCapStyle(Qt.RoundCap)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
rect = QRectF(center.x() - radius, center.y() - radius,
|
||||||
|
radius * 2, radius * 2)
|
||||||
|
painter.drawArc(rect, 90 * 16, -360 * 16) # 绘制完整圆环
|
||||||
|
|
||||||
|
# 绘制进度圆环(渐变色)
|
||||||
|
if self._value > 0:
|
||||||
|
# 创建圆锥渐变(顺时针旋转效果)
|
||||||
|
gradient = QConicalGradient(center, 90) # 从顶部开始
|
||||||
|
gradient.setColorAt(0.0, QColor(66, 133, 244)) # 蓝色
|
||||||
|
gradient.setColorAt(0.5, QColor(25, 118, 210)) # 深蓝
|
||||||
|
gradient.setColorAt(1.0, QColor(21, 101, 192)) # 更深蓝
|
||||||
|
|
||||||
|
pen.setBrush(gradient)
|
||||||
|
pen.setColor(QColor(66, 133, 244))
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
# 计算进度角度(顺时针,从顶部开始)
|
||||||
|
span_angle = int(-(self._value / self._max_value) * 360 * 16)
|
||||||
|
painter.drawArc(rect, 90 * 16, span_angle)
|
||||||
|
|
||||||
|
# 绘制中心文本(百分比)
|
||||||
|
painter.setPen(QColor(44, 62, 80))
|
||||||
|
font = QFont('Microsoft YaHei', 28, QFont.Bold)
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.drawText(rect, Qt.AlignCenter, self._text)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateProgressDialog(QDialog):
|
||||||
|
"""
|
||||||
|
现代化更新进度对话框
|
||||||
|
|
||||||
|
更新流程阶段:
|
||||||
|
1. 下载安装包 (0-50%)
|
||||||
|
2. 等待UAC授权 (50-60%)
|
||||||
|
3. 执行安装 (60-90%)
|
||||||
|
4. 等待程序重启 (90-100%)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 流程阶段定义
|
||||||
|
STAGE_DOWNLOAD = 0 # 下载阶段 (0-50%)
|
||||||
|
STAGE_UAC = 1 # UAC授权阶段 (50-60%)
|
||||||
|
STAGE_INSTALL = 2 # 安装阶段 (60-90%)
|
||||||
|
STAGE_RESTART = 3 # 重启阶段 (90-100%)
|
||||||
|
|
||||||
|
def __init__(self, version, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.version = version
|
||||||
|
self.downloader = None
|
||||||
|
self.current_stage = self.STAGE_DOWNLOAD
|
||||||
|
self.download_progress = 0 # 下载进度(0-100)
|
||||||
|
self.restart_progress = 0 # 重启阶段进度(0-100),用于外部检查
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""初始化UI"""
|
||||||
|
self.setWindowTitle("正在更新")
|
||||||
|
self.setFixedSize(500, 600)
|
||||||
|
# 禁止关闭按钮(只能通过取消按钮关闭)
|
||||||
|
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
|
||||||
|
|
||||||
|
# 主布局
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setSpacing(20)
|
||||||
|
layout.setContentsMargins(30, 30, 30, 30)
|
||||||
|
|
||||||
|
# ============ 标题区域 ============
|
||||||
|
self.title_label = QLabel(f"正在更新到 v{self.version}")
|
||||||
|
self.title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold))
|
||||||
|
self.title_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.title_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
|
||||||
|
layout.addWidget(self.title_label)
|
||||||
|
|
||||||
|
# ============ 圆形进度条 ============
|
||||||
|
self.circular_progress = CircularProgressBar()
|
||||||
|
layout.addWidget(self.circular_progress, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
|
# ============ 阶段提示 ============
|
||||||
|
self.stage_label = QLabel("📥 正在下载安装包...")
|
||||||
|
self.stage_label.setFont(QFont('Microsoft YaHei', 12))
|
||||||
|
self.stage_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.stage_label.setStyleSheet("color: #34495e; margin-top: 10px;")
|
||||||
|
layout.addWidget(self.stage_label)
|
||||||
|
|
||||||
|
# ============ 详细状态 ============
|
||||||
|
self.status_label = QLabel("准备开始下载...")
|
||||||
|
self.status_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.status_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
|
||||||
|
self.status_label.setMinimumHeight(25)
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# ============ 重试信息(默认隐藏)============
|
||||||
|
self.retry_label = QLabel("")
|
||||||
|
self.retry_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.retry_label.setStyleSheet("color: #ff9800; font-size: 11px; font-weight: bold;")
|
||||||
|
self.retry_label.setMinimumHeight(25)
|
||||||
|
self.retry_label.hide()
|
||||||
|
layout.addWidget(self.retry_label)
|
||||||
|
|
||||||
|
# ============ 按钮区域 ============
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
self.cancel_button = QPushButton("取消更新")
|
||||||
|
self.cancel_button.setFixedWidth(120)
|
||||||
|
self.cancel_button.setFixedHeight(36)
|
||||||
|
self.cancel_button.clicked.connect(self.cancel_download)
|
||||||
|
button_layout.addWidget(self.cancel_button)
|
||||||
|
|
||||||
|
button_layout.addStretch()
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 应用样式
|
||||||
|
self.apply_styles()
|
||||||
|
|
||||||
|
def apply_styles(self):
|
||||||
|
"""应用现代化样式"""
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f8fafb,
|
||||||
|
stop:0.5 #f1f4f6,
|
||||||
|
stop:1 #e8ecef
|
||||||
|
);
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #f5f5f5,
|
||||||
|
stop:1 #e0e0e0
|
||||||
|
);
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Microsoft YaHei';
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background: qlineargradient(
|
||||||
|
x1:0, y1:0, x2:0, y2:1,
|
||||||
|
stop:0 #e8e8e8,
|
||||||
|
stop:1 #d0d0d0
|
||||||
|
);
|
||||||
|
border: 2px solid #999;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
QPushButton:disabled {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #aaa;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def update_progress(self, downloaded, total):
|
||||||
|
"""
|
||||||
|
更新下载进度(阶段1:0-50%)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
downloaded: 已下载字节数
|
||||||
|
total: 总字节数
|
||||||
|
"""
|
||||||
|
if self.current_stage != self.STAGE_DOWNLOAD:
|
||||||
|
return # 不在下载阶段,忽略
|
||||||
|
|
||||||
|
if total > 0:
|
||||||
|
# 计算下载百分比(0-100)
|
||||||
|
download_percent = int((downloaded / total) * 100)
|
||||||
|
self.download_progress = download_percent
|
||||||
|
|
||||||
|
# 映射到总进度的0-50%
|
||||||
|
overall_progress = int(download_percent * 0.5)
|
||||||
|
|
||||||
|
# 更新圆形进度条
|
||||||
|
self.circular_progress.setValue(overall_progress)
|
||||||
|
self.circular_progress.setText(f"{overall_progress}%")
|
||||||
|
|
||||||
|
# 转换为MB显示
|
||||||
|
downloaded_mb = downloaded / (1024 * 1024)
|
||||||
|
total_mb = total / (1024 * 1024)
|
||||||
|
|
||||||
|
# 更新状态文本
|
||||||
|
self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB")
|
||||||
|
else:
|
||||||
|
# 无法获取总大小时显示不确定状态
|
||||||
|
self.status_label.setText("正在下载,请稍候...")
|
||||||
|
|
||||||
|
def show_retry_info(self, current_retry, max_retries):
|
||||||
|
"""
|
||||||
|
显示重试信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_retry: 当前重试次数
|
||||||
|
max_retries: 最大重试次数
|
||||||
|
"""
|
||||||
|
self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})")
|
||||||
|
self.retry_label.show()
|
||||||
|
|
||||||
|
# 重置下载进度
|
||||||
|
self.download_progress = 0
|
||||||
|
self.circular_progress.setValue(0)
|
||||||
|
self.circular_progress.setText("0%")
|
||||||
|
self.status_label.setText("正在重试连接...")
|
||||||
|
|
||||||
|
# 更新标题
|
||||||
|
self.title_label.setText(f"正在重试下载 v{self.version}")
|
||||||
|
|
||||||
|
def download_finished(self, file_path):
|
||||||
|
"""
|
||||||
|
下载完成,进入UAC授权阶段(阶段2:50-60%)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 下载的文件路径
|
||||||
|
"""
|
||||||
|
self.current_stage = self.STAGE_UAC
|
||||||
|
|
||||||
|
# 更新进度到50%
|
||||||
|
self.circular_progress.setValue(50)
|
||||||
|
self.circular_progress.setText("50%")
|
||||||
|
|
||||||
|
# 更新阶段提示
|
||||||
|
self.stage_label.setText("🔐 等待管理员授权...")
|
||||||
|
self.title_label.setText(f"✅ 下载完成")
|
||||||
|
self.status_label.setText("请在UAC窗口点击「允许」以继续...")
|
||||||
|
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
|
||||||
|
|
||||||
|
# 禁用取消按钮
|
||||||
|
self.cancel_button.setEnabled(False)
|
||||||
|
self.retry_label.hide()
|
||||||
|
|
||||||
|
# 启动UAC等待动画(50% → 60%)
|
||||||
|
self.start_uac_waiting_animation()
|
||||||
|
|
||||||
|
def start_uac_waiting_animation(self):
|
||||||
|
"""启动UAC等待动画(进度条在50-60%之间缓慢移动)"""
|
||||||
|
self.uac_timer = QTimer(self)
|
||||||
|
self.uac_progress = 50
|
||||||
|
|
||||||
|
def update_uac_progress():
|
||||||
|
self.uac_progress += 0.5 # 每次增加0.5%
|
||||||
|
if self.uac_progress <= 60:
|
||||||
|
self.circular_progress.setValue(int(self.uac_progress))
|
||||||
|
self.circular_progress.setText(f"{int(self.uac_progress)}%")
|
||||||
|
else:
|
||||||
|
# 超过60%停止(等待用户授权)
|
||||||
|
self.uac_timer.stop()
|
||||||
|
|
||||||
|
self.uac_timer.timeout.connect(update_uac_progress)
|
||||||
|
self.uac_timer.start(500) # 每500ms更新一次
|
||||||
|
|
||||||
|
def uac_authorized(self):
|
||||||
|
"""
|
||||||
|
用户已授权UAC,进入安装阶段(阶段3:60-90%)
|
||||||
|
"""
|
||||||
|
# 停止UAC等待动画
|
||||||
|
if hasattr(self, 'uac_timer'):
|
||||||
|
self.uac_timer.stop()
|
||||||
|
|
||||||
|
self.current_stage = self.STAGE_INSTALL
|
||||||
|
|
||||||
|
# 更新进度到60%
|
||||||
|
self.circular_progress.setValue(60)
|
||||||
|
self.circular_progress.setText("60%")
|
||||||
|
|
||||||
|
# 更新阶段提示
|
||||||
|
self.stage_label.setText("⚙️ 正在安装新版本...")
|
||||||
|
self.title_label.setText("正在安装")
|
||||||
|
self.status_label.setText("正在替换程序文件,请稍候...")
|
||||||
|
self.status_label.setStyleSheet("color: #007bff; font-size: 11px; font-weight: bold;")
|
||||||
|
|
||||||
|
# 启动安装进度动画(60% → 90%)
|
||||||
|
self.start_install_animation()
|
||||||
|
|
||||||
|
def start_install_animation(self):
|
||||||
|
"""启动安装进度动画(60% → 90%)"""
|
||||||
|
self.install_timer = QTimer(self)
|
||||||
|
self.install_progress = 60
|
||||||
|
|
||||||
|
def update_install_progress():
|
||||||
|
self.install_progress += 1 # 每次增加1%
|
||||||
|
if self.install_progress <= 90:
|
||||||
|
self.circular_progress.setValue(self.install_progress)
|
||||||
|
self.circular_progress.setText(f"{self.install_progress}%")
|
||||||
|
else:
|
||||||
|
# 安装完成,进入重启阶段
|
||||||
|
self.install_timer.stop()
|
||||||
|
self.prepare_restart()
|
||||||
|
|
||||||
|
self.install_timer.timeout.connect(update_install_progress)
|
||||||
|
self.install_timer.start(800) # 每800ms增加1%(共24秒)
|
||||||
|
|
||||||
|
def prepare_restart(self):
|
||||||
|
"""
|
||||||
|
准备重启(阶段4:90-100%)
|
||||||
|
"""
|
||||||
|
self.current_stage = self.STAGE_RESTART
|
||||||
|
|
||||||
|
# 更新进度到90%
|
||||||
|
self.circular_progress.setValue(90)
|
||||||
|
self.circular_progress.setText("90%")
|
||||||
|
|
||||||
|
# 更新阶段提示
|
||||||
|
self.stage_label.setText("🚀 准备启动新版本...")
|
||||||
|
self.title_label.setText("即将完成")
|
||||||
|
self.status_label.setText("程序即将重启,请稍候...")
|
||||||
|
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
|
||||||
|
|
||||||
|
# 启动重启倒计时动画(90% → 100%)
|
||||||
|
self.start_restart_animation()
|
||||||
|
|
||||||
|
def start_restart_animation(self):
|
||||||
|
"""启动重启倒计时动画(90% → 100%)"""
|
||||||
|
self.restart_timer = QTimer(self)
|
||||||
|
self.restart_progress = 90
|
||||||
|
|
||||||
|
def update_restart_progress():
|
||||||
|
self.restart_progress += 2 # 每次增加2%
|
||||||
|
if self.restart_progress <= 100:
|
||||||
|
self.circular_progress.setValue(self.restart_progress)
|
||||||
|
self.circular_progress.setText(f"{self.restart_progress}%")
|
||||||
|
|
||||||
|
if self.restart_progress == 100:
|
||||||
|
# 🔥 到达100%,更新提示但不自动关闭
|
||||||
|
self.status_label.setText("✅ 安装完成!等待启动确认...")
|
||||||
|
self.stage_label.setText("🎉 更新成功!")
|
||||||
|
self.restart_timer.stop() # 停止定时器
|
||||||
|
else:
|
||||||
|
self.restart_timer.stop()
|
||||||
|
|
||||||
|
self.restart_timer.timeout.connect(update_restart_progress)
|
||||||
|
self.restart_timer.start(500) # 每500ms增加2%(共2.5秒)
|
||||||
|
|
||||||
|
def download_error(self, error_msg):
|
||||||
|
"""
|
||||||
|
下载失败
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error_msg: 错误信息
|
||||||
|
"""
|
||||||
|
self.title_label.setText(f"❌ 下载失败")
|
||||||
|
|
||||||
|
# 错误信息截断(避免太长)
|
||||||
|
if len(error_msg) > 80:
|
||||||
|
display_msg = error_msg[:80] + "..."
|
||||||
|
else:
|
||||||
|
display_msg = error_msg
|
||||||
|
|
||||||
|
self.stage_label.setText("⚠️ 更新失败")
|
||||||
|
self.status_label.setText(display_msg)
|
||||||
|
self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;")
|
||||||
|
|
||||||
|
self.cancel_button.setText("关闭")
|
||||||
|
self.cancel_button.setEnabled(True)
|
||||||
|
self.retry_label.hide()
|
||||||
|
|
||||||
|
def cancel_download(self):
|
||||||
|
"""取消下载"""
|
||||||
|
if self.current_stage == self.STAGE_DOWNLOAD and self.downloader and self.downloader.isRunning():
|
||||||
|
# 下载阶段可以取消
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"确认取消",
|
||||||
|
"确定要取消更新吗?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
self.downloader.cancel()
|
||||||
|
self.downloader.wait()
|
||||||
|
self.status_label.setText("更新已取消")
|
||||||
|
self.reject() # 会自动清理定时器
|
||||||
|
else:
|
||||||
|
# 其他阶段或下载已完成,直接关闭
|
||||||
|
self.reject() # 会自动清理定时器
|
||||||
|
|
||||||
|
def cleanup_timers(self):
|
||||||
|
"""清理所有定时器"""
|
||||||
|
if hasattr(self, 'uac_timer'):
|
||||||
|
self.uac_timer.stop()
|
||||||
|
if hasattr(self, 'install_timer'):
|
||||||
|
self.install_timer.stop()
|
||||||
|
if hasattr(self, 'restart_timer'):
|
||||||
|
self.restart_timer.stop()
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""对话框接受时清理资源"""
|
||||||
|
self.cleanup_timers()
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
"""对话框拒绝时清理资源"""
|
||||||
|
self.cleanup_timers()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试代码
|
||||||
|
import sys
|
||||||
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
# 创建测试对话框
|
||||||
|
dialog = UpdateProgressDialog("1.5.55")
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
# 模拟完整更新流程
|
||||||
|
def simulate_update():
|
||||||
|
# 阶段1:模拟下载 (0-50%)
|
||||||
|
def simulate_download():
|
||||||
|
total_size = 100 * 1024 * 1024 # 100MB
|
||||||
|
for i in range(0, 101, 5):
|
||||||
|
downloaded = int(i * total_size / 100)
|
||||||
|
dialog.update_progress(downloaded, total_size)
|
||||||
|
QApplication.processEvents()
|
||||||
|
QTimer.singleShot(100, lambda: None) # 延迟
|
||||||
|
|
||||||
|
# 下载完成
|
||||||
|
QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe"))
|
||||||
|
|
||||||
|
# 阶段2:模拟UAC授权 (50-60%)
|
||||||
|
QTimer.singleShot(5000, lambda: dialog.uac_authorized())
|
||||||
|
|
||||||
|
simulate_download()
|
||||||
|
|
||||||
|
QTimer.singleShot(500, simulate_update)
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
216
version_checker.py
Normal file
216
version_checker.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
版本检查器
|
||||||
|
定期检查后端是否有新版本,并通知GUI更新
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionChecker:
|
||||||
|
"""
|
||||||
|
版本检查器
|
||||||
|
定期向后端发送版本检查请求,如果有新版本则触发更新回调
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, backend_client, update_callback: Optional[Callable] = None):
|
||||||
|
"""
|
||||||
|
初始化版本检查器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backend_client: 后端WebSocket客户端实例
|
||||||
|
update_callback: 发现新版本时的回调函数 callback(latest_version, download_url)
|
||||||
|
"""
|
||||||
|
self.backend_client = backend_client
|
||||||
|
self.update_callback = update_callback
|
||||||
|
self.is_running = False
|
||||||
|
self.check_interval = 600 # 10分钟检查一次
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
# 从config获取当前版本
|
||||||
|
try:
|
||||||
|
from config import APP_VERSION
|
||||||
|
self.current_version = APP_VERSION
|
||||||
|
logger.info(f"✅ 版本检查器初始化成功,当前版本: v{self.current_version}")
|
||||||
|
except ImportError:
|
||||||
|
self.current_version = "1.0.0"
|
||||||
|
logger.warning("⚠️ 无法从config读取APP_VERSION,使用默认版本: 1.0.0")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动版本检查线程"""
|
||||||
|
if self.is_running:
|
||||||
|
logger.warning("版本检查器已在运行中")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_running = True
|
||||||
|
self.thread = threading.Thread(target=self._check_loop, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logger.info(f"✅ 版本检查器已启动,每{self.check_interval}秒检查一次更新")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止版本检查"""
|
||||||
|
self.is_running = False
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join(timeout=2)
|
||||||
|
logger.info("版本检查器已停止")
|
||||||
|
|
||||||
|
def _check_loop(self):
|
||||||
|
"""版本检查循环"""
|
||||||
|
# 等待5秒后首次检查(避免启动时立即检查)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
self._perform_version_check()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"版本检查失败: {e}")
|
||||||
|
|
||||||
|
# 等待下次检查
|
||||||
|
time.sleep(self.check_interval)
|
||||||
|
|
||||||
|
def _perform_version_check(self):
|
||||||
|
"""执行版本检查"""
|
||||||
|
try:
|
||||||
|
if not self.backend_client or not self.backend_client.is_connected:
|
||||||
|
logger.debug("后端未连接,跳过版本检查")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 向后端发送版本检查请求
|
||||||
|
message = {
|
||||||
|
'type': 'version_check',
|
||||||
|
'current_version': self.current_version,
|
||||||
|
'client_type': 'gui'
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(f"发送版本检查请求: v{self.current_version}")
|
||||||
|
self.backend_client.send_message(message)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送版本检查请求失败: {e}")
|
||||||
|
|
||||||
|
def handle_version_response(self, response: dict):
|
||||||
|
"""
|
||||||
|
处理后端返回的版本检查响应
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: 后端响应消息
|
||||||
|
{
|
||||||
|
"type": "version_response",
|
||||||
|
"has_update": true,
|
||||||
|
"latest_version": "1.5.0",
|
||||||
|
"current_version": "1.4.7",
|
||||||
|
"download_url": "https://example.com/download/v1.5.0",
|
||||||
|
"update_content": "修复了若干Bug"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
has_update = response.get('has_update', None) # 改为 None,便于后续判断
|
||||||
|
latest_version = response.get('latest_version', '')
|
||||||
|
current_version = response.get('current_version', self.current_version)
|
||||||
|
download_url = response.get('download_url', '')
|
||||||
|
update_content = response.get('update_content', '')
|
||||||
|
|
||||||
|
logger.info(f"📡 收到版本检查响应: 当前v{current_version}, 最新v{latest_version}")
|
||||||
|
|
||||||
|
# 如果后端没有返回 has_update 字段,自动通过版本比较判断
|
||||||
|
if has_update is None and latest_version:
|
||||||
|
# 自动比较版本号
|
||||||
|
compare_result = self.compare_versions(latest_version, current_version)
|
||||||
|
has_update = compare_result > 0 # latest > current 表示有更新
|
||||||
|
logger.info(f"🔍 后端未返回has_update字段,通过版本比较判断: {has_update}")
|
||||||
|
|
||||||
|
if has_update and latest_version:
|
||||||
|
logger.info(f"🔔 发现新版本: v{latest_version}")
|
||||||
|
if update_content:
|
||||||
|
logger.info(f"📝 更新内容: {update_content[:100]}")
|
||||||
|
|
||||||
|
# 触发更新回调
|
||||||
|
if self.update_callback:
|
||||||
|
try:
|
||||||
|
self.update_callback(latest_version, download_url)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行更新回调失败: {e}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"✅ 当前已是最新版本: v{current_version}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理版本响应失败: {e}")
|
||||||
|
|
||||||
|
def compare_versions(self, version1: str, version2: str) -> int:
|
||||||
|
"""
|
||||||
|
比较两个版本号
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version1: 版本1 (如 "1.4.7")
|
||||||
|
version2: 版本2 (如 "1.5.0")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
1: version1 > version2
|
||||||
|
0: version1 == version2
|
||||||
|
-1: version1 < version2
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
v1_parts = [int(x) for x in version1.split('.')]
|
||||||
|
v2_parts = [int(x) for x in version2.split('.')]
|
||||||
|
|
||||||
|
# 补齐长度
|
||||||
|
max_len = max(len(v1_parts), len(v2_parts))
|
||||||
|
v1_parts.extend([0] * (max_len - len(v1_parts)))
|
||||||
|
v2_parts.extend([0] * (max_len - len(v2_parts)))
|
||||||
|
|
||||||
|
# 逐位比较
|
||||||
|
for v1, v2 in zip(v1_parts, v2_parts):
|
||||||
|
if v1 > v2:
|
||||||
|
return 1
|
||||||
|
elif v1 < v2:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"版本比较失败: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 模拟使用
|
||||||
|
class MockBackendClient:
|
||||||
|
is_connected = True
|
||||||
|
def send_message(self, msg):
|
||||||
|
print(f"发送消息: {msg}")
|
||||||
|
|
||||||
|
def on_update_available(version, url):
|
||||||
|
print(f"🔔 发现新版本: {version}, 下载地址: {url}")
|
||||||
|
|
||||||
|
client = MockBackendClient()
|
||||||
|
checker = VersionChecker(client, on_update_available)
|
||||||
|
checker.start()
|
||||||
|
|
||||||
|
# 模拟接收后端响应
|
||||||
|
time.sleep(2)
|
||||||
|
response = {
|
||||||
|
"type": "version_response",
|
||||||
|
"has_update": True,
|
||||||
|
"latest_version": "1.5.0",
|
||||||
|
"current_version": "1.4.7",
|
||||||
|
"download_url": "https://example.com/download",
|
||||||
|
"update_content": "修复了若干Bug"
|
||||||
|
}
|
||||||
|
checker.handle_version_response(response)
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
checker.stop()
|
||||||
|
|
||||||
802
version_history.json
Normal file
802
version_history.json
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"version": "1.5.71",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.70",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "14991ae2aa2c36ee51e4b8ef080d333712e4faac",
|
||||||
|
"commit_short_hash": "14991ae2",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-11-04 16:45:16",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.71.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.70",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.69",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "32a17fb255f3ace34262c8b34679b2129147f01c",
|
||||||
|
"commit_short_hash": "32a17fb2",
|
||||||
|
"branch": "jiang",
|
||||||
|
"release_time": "2025-11-04 14:24:14",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.70.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.69",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修复因自动更新回归代码config.py因覆盖逻辑导致的新增代码被覆盖的逻辑",
|
||||||
|
"author": "kris 郝",
|
||||||
|
"commit_hash": "4f17092305e1f73e7c19b28d751d5bbaf6895022",
|
||||||
|
"commit_short_hash": "4f170923",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-11-03 15:51:19",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.69.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 25,
|
||||||
|
"lines_deleted": 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.67",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.66",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "2083516b8c66377b5f13df0989fce0eeb82c105e",
|
||||||
|
"commit_short_hash": "2083516b",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-11-03 11:31:45",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.67.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.66",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.65",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "27b15773e0fffa15a65e4a69f3b9cd2097d8c24b",
|
||||||
|
"commit_short_hash": "27b15773",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-31 13:49:16",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.66.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.65",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.64",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "5440aa7d6dd075237772087f3c2ded8ebeb0d3b9",
|
||||||
|
"commit_short_hash": "5440aa7d",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-30 16:54:42",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.65.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.64",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 处理js内置环境dll补充",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "30ac26160ba7dbf90b1f17c9b062f3a965987649",
|
||||||
|
"commit_short_hash": "30ac2616",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-30 14:58:17",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.64.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 2,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.60",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.59",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "a807bdb74de2cb1934dac1d7b83109407877acd6",
|
||||||
|
"commit_short_hash": "a807bdb7",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-30 09:25:26",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.60.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.59",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.58",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "791f98bf61f88e3d9b05e4941f8145a0d9b29f8a",
|
||||||
|
"commit_short_hash": "791f98bf",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-29 17:05:23",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.59.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.58",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 新增自动更新功能 模块 优化更新后打开--不被遮挡",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "e024150c5aaa2d376f7ca371af7f819d6a458ced",
|
||||||
|
"commit_short_hash": "e024150c",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-29 15:56:21",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.58.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 2,
|
||||||
|
"lines_deleted": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.56",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.55",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "8f1b51ef4f7b7a832c06e008afea81a000d144cc",
|
||||||
|
"commit_short_hash": "8f1b51ef",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-29 14:58:30",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.56.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.55",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 新增自动更新功能 模块Test-center",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "16d5d95c4ef1dd29537a718de5c87fb4f2aa4f48",
|
||||||
|
"commit_short_hash": "16d5d95c",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-28 17:16:13",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.55.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 133,
|
||||||
|
"lines_deleted": 32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.54",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.53",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "3d155fb0976f7efb2233d4e1444ded916b9d962f",
|
||||||
|
"commit_short_hash": "3d155fb0",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-28 16:01:33",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.54.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.53",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 新增DY 图片 视频上传 发送等方法逻辑集成 优化抖音心跳维护与弹性发送心跳包 DY集成内置js环境 PDD取消过滤系统机器消息",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "1f1deb5f7ff9886951f2d996cc39716a1f9d10ac",
|
||||||
|
"commit_short_hash": "1f1deb5f",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-27 17:34:09",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.53.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 5,
|
||||||
|
"lines_added": 1556,
|
||||||
|
"lines_deleted": 64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.51",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 因pdd内部回复消息接口与登录接口不同步导致监听过程中发送消息接口返回会话过期处理优化逻辑 并设计开发提示用户 并提供用户是否重新发送会话过期消息体按钮",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "7a1ad474396b151b921f5a55925e63b87f039564",
|
||||||
|
"commit_short_hash": "7a1ad474",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-25 09:09:31",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.51.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 393,
|
||||||
|
"lines_deleted": 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.49",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 重连机制新增is_reconnect参数",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "9f0c81ea82af30d82b9db7ec9b9668f444ba62b5",
|
||||||
|
"commit_short_hash": "9f0c81ea",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-23 09:14:58",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.49.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 8,
|
||||||
|
"lines_deleted": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.47",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修复抖音消息格式 过滤抖音系统消息 失效后发送结构体给后端",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "782c56f9b634d05dfd7e913017cf57fdf197ff10",
|
||||||
|
"commit_short_hash": "782c56f9",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-22 17:14:33",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.47.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 92,
|
||||||
|
"lines_deleted": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.46",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.45",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "099c3e2db2b367b575477578eb46b018972925ac",
|
||||||
|
"commit_short_hash": "099c3e2d",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-22 14:50:54",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.46.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 18,
|
||||||
|
"lines_deleted": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.45",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] DY-token检测逻辑修改 修改本地打包环境路径锁定",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "ff6203d387aedb0bbe7c7728efaa2a4fdba7d882",
|
||||||
|
"commit_short_hash": "ff6203d3",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-22 13:16:48",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.45.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 141,
|
||||||
|
"lines_deleted": 59
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.43",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 禁用ks3代理",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "cf72bc68271a7952940bc07b24b5b9f3b73250ae",
|
||||||
|
"commit_short_hash": "cf72bc68",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-21 16:19:34",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.43.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 1,
|
||||||
|
"lines_deleted": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.38",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 处理PDD登录时get_auth_token方法报出的response返回异常未处理报错问题",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "449ebf83cbf88852ca6918420f8a4e491dcdeaa4",
|
||||||
|
"commit_short_hash": "449ebf83",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-18 17:15:36",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.38.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 2,
|
||||||
|
"lines_deleted": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.36",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 新增用户余额不足 交互模式代码",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "8eb03ddedcad2d276ad1f2d4aa68252f963633c2",
|
||||||
|
"commit_short_hash": "8eb03dde",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-17 17:38:19",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.36.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 4,
|
||||||
|
"lines_added": 225,
|
||||||
|
"lines_deleted": 77
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.35",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 新增拼多多处理聊天页商品卡片",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "3853b8faf8a55095c133730ce9b080ca3a68b3dd",
|
||||||
|
"commit_short_hash": "3853b8fa",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-17 17:05:28",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.35.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 15,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.34",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] config配置修改 ping机制",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "fe2b29da9c483ae82d6e7f6a95c66f561683731f",
|
||||||
|
"commit_short_hash": "fe2b29da",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-17 15:50:54",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.34.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 4,
|
||||||
|
"lines_deleted": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.33",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.32",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "90bf763bded8c6cb163ded90bf014cbdab61c312",
|
||||||
|
"commit_short_hash": "90bf763b",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-17 15:21:22",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.33.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 20,
|
||||||
|
"lines_deleted": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.32",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.31",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "e2ab13d599a6d5484f097826358764c5e3ad1436",
|
||||||
|
"commit_short_hash": "e2ab13d5",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-17 11:29:44",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.32.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 150,
|
||||||
|
"lines_deleted": 134
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.31",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.30",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "e15c1db49b2feb7aee650de5d23681bd9842ede8",
|
||||||
|
"commit_short_hash": "e15c1db4",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-15 16:27:08",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.31.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.30",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 优化提交检测合并逻辑",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "340670742c41a6de65a76fb24b25959c82618793",
|
||||||
|
"commit_short_hash": "34067074",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-15 09:55:32",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.30.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 19,
|
||||||
|
"lines_deleted": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.28",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改打包逻辑 优化js代码因环境不兼容问题处理方案开发",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "9a4e2bba7982129b4c0f305482dac0d0e97dfb24",
|
||||||
|
"commit_short_hash": "9a4e2bba",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-14 18:07:38",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.28.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 5,
|
||||||
|
"lines_deleted": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.27",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正京东丢失客服列表功能",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "7312d3f91a5810eae8c6d41688bdc735def78459",
|
||||||
|
"commit_short_hash": "7312d3f9",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-14 17:27:26",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.27.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 82,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.26",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.5.25",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "5e9e87bbba56a98f27e1d115018c856e1e266f3f",
|
||||||
|
"commit_short_hash": "5e9e87bb",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-13 17:07:40",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.26.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.25",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改打包固定项缓存复用 提升打包效率",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "3c57b990649e9061e09f6665f92aa7a09bdcf79f",
|
||||||
|
"commit_short_hash": "3c57b990",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-13 15:32:15",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.25.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 1,
|
||||||
|
"lines_deleted": 44
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.21",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正pdd不识别系统消息的问题",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "f651e0e3f02a42f14c86a1d54f366e06cd9b5c49",
|
||||||
|
"commit_short_hash": "f651e0e3",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-13 14:04:33",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.21.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 3259,
|
||||||
|
"lines_deleted": 3254
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.20",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "5896b422f73104cb7b9dba00d19936736f584a4b",
|
||||||
|
"commit_short_hash": "5896b422",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-13 13:40:48",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.20.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 171,
|
||||||
|
"lines_deleted": 171
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.13",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正ks3问题且优化pillow依赖安装时间",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "d8154025ea712deafa30b7c0a8434d1ba4c2cab7",
|
||||||
|
"commit_short_hash": "d8154025",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 17:11:50",
|
||||||
|
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.13.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 6,
|
||||||
|
"lines_added": 53,
|
||||||
|
"lines_deleted": 21
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.12",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正ks3问题",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "5c73bbf8bce718195e1d4b4f727ede53a3921c1a",
|
||||||
|
"commit_short_hash": "5c73bbf8",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:54:44",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.12.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 29,
|
||||||
|
"lines_deleted": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.11",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正ks3包名问题",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "bcb76c0a99d8ec689610a12d2942bd486e381358",
|
||||||
|
"commit_short_hash": "bcb76c0a",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:44:27",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.11.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 5,
|
||||||
|
"lines_added": 29,
|
||||||
|
"lines_deleted": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.10",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正Pillow依赖冲突问题",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "99446eca748e93c289012a8465a94e4ae7ca8961",
|
||||||
|
"commit_short_hash": "99446eca",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:34:25",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.10.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 30,
|
||||||
|
"lines_deleted": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.9",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修正Pillow安装步骤",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "14726466248048dc40e1e4d84119a0bc5da41432",
|
||||||
|
"commit_short_hash": "14726466",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:30:23",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.9.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 29,
|
||||||
|
"lines_deleted": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.8",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 安装Pillow依赖",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "b685e64e7119e680cb55f323c882e036b08c0784",
|
||||||
|
"commit_short_hash": "b685e64e",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:27:55",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.8.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 21,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.7",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 测试是否安装成功",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "ee7c76ef68dd8e2672ec1a6b984ee65edab583bf",
|
||||||
|
"commit_short_hash": "ee7c76ef",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:24:59",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.7.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 22,
|
||||||
|
"lines_deleted": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.6",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改nsis安装流程",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "afcd360603aa89b41f32ac59c4527103b1aa33f9",
|
||||||
|
"commit_short_hash": "afcd3606",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:20:24",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.6.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 121,
|
||||||
|
"lines_deleted": 96
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.5",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改nsis安装流程",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "3352a59ff9c90d09b080f82e04e06c881becea80",
|
||||||
|
"commit_short_hash": "3352a59f",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:16:08",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.5.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 96,
|
||||||
|
"lines_deleted": 54
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.4",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 修改nsis安装流程",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "468a092fd93c81e08481b212fd730f11964ff928",
|
||||||
|
"commit_short_hash": "468a092f",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:11:56",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.4.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 97,
|
||||||
|
"lines_deleted": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.3",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 解决乱码bug",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "7c0667cb39c2795ce9901e141e1a9ff698a4955b",
|
||||||
|
"commit_short_hash": "7c0667cb",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:09:00",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.3.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 3,
|
||||||
|
"lines_added": 60,
|
||||||
|
"lines_deleted": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.2",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 解决乱码bug",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "2d05024e82ac0c16eff30625195486f7c6862ea8",
|
||||||
|
"commit_short_hash": "2d05024e",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 16:05:03",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.2.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 5,
|
||||||
|
"lines_added": 207,
|
||||||
|
"lines_deleted": 192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.1",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] 添加develop用于测试",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "2dddcc9804ad4a6966439f2139110ab639ca1452",
|
||||||
|
"commit_short_hash": "2dddcc98",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 15:56:40",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.1.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 1,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.5.0",
|
||||||
|
"update_type": "minor",
|
||||||
|
"content": "实现更新版本管理",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "8ecec1edbed34ef40869cf7a0c07fc33e81d9c37",
|
||||||
|
"commit_short_hash": "8ecec1ed",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 15:47:09",
|
||||||
|
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.0.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 6,
|
||||||
|
"lines_added": 265,
|
||||||
|
"lines_deleted": 168
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.29",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[skip ci] Update version to v1.4.28",
|
||||||
|
"author": "Gitea Actions Bot",
|
||||||
|
"commit_hash": "2dd384e8e48ed7f142bae43821b08db862c2bcca",
|
||||||
|
"commit_short_hash": "2dd384e8",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 15:30:32",
|
||||||
|
"download_url": "https://shuidrop.com/download/gui/ShuiDi_AI_Assistant_Setup_v1.4.29.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 2,
|
||||||
|
"lines_added": 17,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.4.28",
|
||||||
|
"update_type": "patch",
|
||||||
|
"content": "[patch] PDD登录状态GUI回调优化 与 非原生git指令导致check任务失败",
|
||||||
|
"author": "haosicheng",
|
||||||
|
"commit_hash": "c6ae22c7e10ff560a117f7dec0929f43c224d720",
|
||||||
|
"commit_short_hash": "c6ae22c7",
|
||||||
|
"branch": "develop",
|
||||||
|
"release_time": "2025-10-11 10:17:19",
|
||||||
|
"download_url": "https://shuidrop.com/download/gui/ShuiDi_AI_Assistant_Setup_v1.4.28.exe",
|
||||||
|
"stats": {
|
||||||
|
"files_changed": 1,
|
||||||
|
"lines_added": 0,
|
||||||
|
"lines_deleted": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user