155 Commits

Author SHA1 Message Date
Gitea Actions Bot
7fb90ace65 [skip ci] Update version to v1.5.71 2025-11-04 17:05:46 +08:00
kris 郝
01aed205fe [patch] 正式环境代码提交 2025-11-04 16:44:56 +08:00
Gitea Actions Bot
14991ae2aa [skip ci] Update version to v1.5.70 2025-11-04 14:35:46 +08:00
kris 郝
1de233b197 [patch] 更新PDD过滤消息基础逻辑 过滤部分系统机器人消息 2025-11-04 14:23:53 +08:00
Gitea Actions Bot
32a17fb255 [skip ci] Update version to v1.5.69 2025-11-03 16:13:35 +08:00
kris 郝
4f17092305 [patch] 修复因自动更新回归代码config.py因覆盖逻辑导致的新增代码被覆盖的逻辑 2025-11-03 15:51:00 +08:00
kris 郝
58868c9c18 [patch] 修复因自动更新回归代码config.py因覆盖逻辑导致的新增代码被覆盖的逻辑 2025-11-03 14:56:01 +08:00
Gitea Actions Bot
7054beebeb [skip ci] Update version to v1.5.67 2025-11-03 11:52:42 +08:00
kris 郝
8a5da5e906 [patch] 新增多实例测试开关控制流 逻辑 2025-11-03 11:31:20 +08:00
Gitea Actions Bot
2083516b8c [skip ci] Update version to v1.5.66 2025-10-31 14:01:18 +08:00
f11e5ec210 [patch] 新增JD图片视频类型数据发送方法编写(强化send_message方法) 新增下载视频临时处理存储逻辑 2025-10-31 13:48:52 +08:00
Gitea Actions Bot
27b15773e0 [skip ci] Update version to v1.5.65 2025-10-30 17:26:16 +08:00
8575de6ea5 [patch] 优化更新提示逻辑 与 样式细节 2025-10-30 16:54:24 +08:00
Gitea Actions Bot
5440aa7d6d [skip ci] Update version to v1.5.64 2025-10-30 15:26:47 +08:00
30ac26160b [patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 处理js内置环境dll补充 2025-10-30 14:57:58 +08:00
b884cc97a0 [patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 处理js内置环境dll补充 2025-10-30 14:03:40 +08:00
f1b9e35b09 [patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 处理js内置环境dll补充 2025-10-30 12:01:58 +08:00
Gitea Actions Bot
5945e8c761 [skip ci] Update version to v1.5.60 2025-10-30 09:41:54 +08:00
cd96935c6b [patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 2025-10-30 09:25:07 +08:00
Gitea Actions Bot
a807bdb74d [skip ci] Update version to v1.5.59 2025-10-29 17:31:51 +08:00
a95278368d [patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 2025-10-29 17:05:06 +08:00
Gitea Actions Bot
791f98bf61 [skip ci] Update version to v1.5.58 2025-10-29 16:20:10 +08:00
e024150c5a [patch] 新增自动更新功能 模块 优化更新后打开--不被遮挡 2025-10-29 15:56:04 +08:00
89d09777a0 [patch] 新增自动更新功能 模块 优化更新后打开--不被遮挡 2025-10-29 15:55:53 +08:00
Gitea Actions Bot
aa61e8a931 [skip ci] Update version to v1.5.56 2025-10-29 15:21:56 +08:00
8c0580d140 [patch] 新增自动更新功能 模块 优化进度条显示 横幅图片资源上传 2025-10-29 14:58:09 +08:00
Gitea Actions Bot
8f1b51ef4f [skip ci] Update version to v1.5.55 2025-10-28 17:40:46 +08:00
16d5d95c4e [patch] 新增自动更新功能 模块Test-center 2025-10-28 17:15:56 +08:00
Gitea Actions Bot
5945e1b0e7 [skip ci] Update version to v1.5.54 2025-10-28 16:27:01 +08:00
bd363d0e33 [minor] 新增自动更新功能 模块Test 2025-10-28 15:59:18 +08:00
Gitea Actions Bot
3d155fb097 [skip ci] Update version to v1.5.53 2025-10-27 18:02:54 +08:00
1f1deb5f7f [patch] 新增DY 图片 视频上传 发送等方法逻辑集成 优化抖音心跳维护与弹性发送心跳包 DY集成内置js环境 PDD取消过滤系统机器消息 2025-10-27 16:32:07 +08:00
Gitea Actions Bot
787c52a8dc [skip ci] Update version to v1.5.51 2025-10-25 09:35:23 +08:00
7a1ad47439 [patch] 因pdd内部回复消息接口与登录接口不同步导致监听过程中发送消息接口返回会话过期处理优化逻辑 并设计开发提示用户 并提供用户是否重新发送会话过期消息体按钮 2025-10-24 17:47:28 +08:00
Gitea Actions Bot
9effc859ac [skip ci] Update version to v1.5.49 2025-10-23 09:26:08 +08:00
8d31834112 [patch] 重连机制新增is_reconnect参数 2025-10-23 09:14:43 +08:00
9f0c81ea82 [patch] 重连机制新增is_reconnect参数 2025-10-22 17:45:06 +08:00
Gitea Actions Bot
df7f38bbd8 [skip ci] Update version to v1.5.47 2025-10-22 17:39:20 +08:00
Gitea Actions Bot
782c56f9b6 [patch] 修复抖音消息格式 过滤抖音系统消息 失效后发送结构体给后端 2025-10-22 17:14:05 +08:00
Gitea Actions Bot
38267640c3 [skip ci] Update version to v1.5.46 2025-10-22 15:04:00 +08:00
1b08ff46b7 [patch] DY日志优化2 2025-10-22 14:50:36 +08:00
Gitea Actions Bot
099c3e2db2 [skip ci] Update version to v1.5.45 2025-10-22 13:29:08 +08:00
ff6203d387 [patch] DY-token检测逻辑修改 修改本地打包环境路径锁定 2025-10-22 13:16:26 +08:00
286d7916e9 [patch] 优化DY日志结构 降低冗余代码 2025-10-22 11:53:30 +08:00
Gitea Actions Bot
a7864a05ee [skip ci] Update version to v1.5.43 2025-10-21 16:45:57 +08:00
cf72bc6827 [patch] 禁用ks3代理 2025-10-21 16:19:17 +08:00
03457a7f9e [patch] 设计PDD的处理消息架构为msg_type 通用模式 处理PDD支持图片类型和 商品卡片类型的消息的发送回复 修改GUI托盘内部样式 2025-10-21 16:04:29 +08:00
0c92d8e4c2 [patch] 设计PDD的处理消息架构为msg_type 通用模式 处理PDD支持图片类型和 商品卡片类型的消息的发送回复 修改GUI托盘内部样式 2025-10-21 15:38:17 +08:00
2f1f409710 [patch] 设计PDD的处理消息架构为msg_type 通用模式 处理PDD支持图片类型和 商品卡片类型的消息的发送回复 修改GUI托盘内部样式 2025-10-21 15:19:35 +08:00
9b21287bd0 [patch] 设计PDD的处理消息架构为msg_type 通用模式 处理PDD支持图片类型和 商品卡片类型的消息的发送回复 修改GUI托盘内部样式 2025-10-21 14:20:19 +08:00
Gitea Actions Bot
f7606f09cf [skip ci] Update version to v1.5.38 2025-10-18 17:45:47 +08:00
449ebf83cb [patch] 处理PDD登录时get_auth_token方法报出的response返回异常未处理报错问题 2025-10-18 17:15:16 +08:00
fd6dbfc8da [patch] 处理PDD登录时get_auth_token方法报出的response返回异常未处理报错问题 2025-10-18 16:12:57 +08:00
Gitea Actions Bot
ceb5d83d75 [skip ci] Update version to v1.5.36 2025-10-17 18:05:28 +08:00
8eb03ddedc [patch] 新增用户余额不足 交互模式代码 2025-10-17 17:38:02 +08:00
Gitea Actions Bot
a5c2a44512 [skip ci] Update version to v1.5.35 2025-10-17 17:29:41 +08:00
Gitea Actions Bot
3853b8faf8 [patch] 新增拼多多处理聊天页商品卡片 2025-10-17 17:05:02 +08:00
Gitea Actions Bot
67f83e94dc [skip ci] Update version to v1.5.34 2025-10-17 16:14:35 +08:00
fe2b29da9c [patch] config配置修改 ping机制 2025-10-17 15:50:37 +08:00
Gitea Actions Bot
3dd3ceeb3e [skip ci] Update version to v1.5.33 2025-10-17 15:45:17 +08:00
c32d326c69 [patch] 修改适中心跳机制 JD的平台登录抢占 提示弹框优化 并下发连接断开通知 托盘右击提示小标题显示精炼 增加store_name入回调方法做店铺名称定位 2025-10-17 15:21:03 +08:00
Gitea Actions Bot
90bf763bde [skip ci] Update version to v1.5.32 2025-10-17 11:47:44 +08:00
1438e39ffd [patch] 优化心跳检测逻辑 优化与后端交互连接处理逻辑 新增自动登录与手动下发冲突问题解决方法块 2025-10-17 11:29:27 +08:00
Gitea Actions Bot
e2ab13d599 [skip ci] Update version to v1.5.31 2025-10-15 16:52:52 +08:00
737737a6c1 [patch] 优化PDD的js环境补充 优化配置文件心跳环境检测 优化打包环境补充逻辑 2025-10-15 16:26:44 +08:00
Gitea Actions Bot
e15c1db49b [skip ci] Update version to v1.5.30 2025-10-15 10:15:40 +08:00
340670742c [patch] 优化提交检测合并逻辑 2025-10-15 09:55:11 +08:00
10cefc3c11 [patch] PDD售后消息的过滤处理逻辑优化 2025-10-15 09:28:54 +08:00
Gitea Actions Bot
bdd3e125a0 [skip ci] Update version to v1.5.28 2025-10-14 18:38:16 +08:00
9a4e2bba79 [patch] 修改打包逻辑 优化js代码因环境不兼容问题处理方案开发 2025-10-14 18:07:18 +08:00
Gitea Actions Bot
92d69d9166 [skip ci] Update version to v1.5.27 2025-10-14 17:53:39 +08:00
Gitea Actions Bot
7312d3f91a [patch] 修正京东丢失客服列表功能 2025-10-14 17:26:53 +08:00
Gitea Actions Bot
3d20c47eb9 [skip ci] Update version to v1.5.26 2025-10-13 17:32:02 +08:00
177eddc695 [patch] 修改GUI页面布局以自适应横幅位置 与 横幅跳转功能实现 2025-10-13 17:07:22 +08:00
Gitea Actions Bot
5e9e87bbba [skip ci] Update version to v1.5.25 2025-10-13 15:57:13 +08:00
3c57b99064 [patch] 修改打包固定项缓存复用 提升打包效率 2025-10-13 15:31:54 +08:00
9d9677972b [patch] 修改打包固定项缓存复用 提升打包效率 2025-10-13 15:18:39 +08:00
a2fa008cb6 [patch] 修改打包固定项缓存复用 提升打包效率 2025-10-13 15:07:00 +08:00
303c33b44d [patch] 修改打包固定项缓存复用 提升打包效率 2025-10-13 14:58:56 +08:00
Gitea Actions Bot
7d9297616f [skip ci] Update version to v1.5.21 2025-10-13 14:12:30 +08:00
Gitea Actions Bot
f651e0e3f0 [patch] 修正pdd不识别系统消息的问题 2025-10-13 14:04:01 +08:00
Gitea Actions Bot
461d743ea6 [skip ci] Update version to v1.5.20 2025-10-13 13:52:00 +08:00
5896b422f7 [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题 2025-10-13 13:34:23 +08:00
aabd450ea3 [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题 2025-10-13 13:02:24 +08:00
0abac8518c [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题 2025-10-13 12:38:54 +08:00
5247de38df [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题 2025-10-13 12:06:50 +08:00
40846dd7f4 [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 2025-10-13 11:39:38 +08:00
29fdf85c07 [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 2025-10-13 10:53:42 +08:00
f6c55d4185 [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 2025-10-13 10:27:15 +08:00
Gitea Actions Bot
7ef6b36ff5 [skip ci] Update version to v1.5.13 2025-10-11 17:35:40 +08:00
Gitea Actions Bot
d8154025ea [patch] 修正ks3问题且优化pillow依赖安装时间 2025-10-11 17:09:09 +08:00
Gitea Actions Bot
5c73bbf8bc [patch] 修正ks3问题 2025-10-11 16:54:14 +08:00
Gitea Actions Bot
bcb76c0a99 [patch] 修正ks3包名问题 2025-10-11 16:43:59 +08:00
Gitea Actions Bot
99446eca74 [patch] 修正Pillow依赖冲突问题 2025-10-11 16:34:03 +08:00
Gitea Actions Bot
1472646624 [patch] 修正Pillow安装步骤 2025-10-11 16:30:01 +08:00
Gitea Actions Bot
b685e64e71 [patch] 安装Pillow依赖 2025-10-11 16:27:26 +08:00
Gitea Actions Bot
ee7c76ef68 [patch] 测试是否安装成功 2025-10-11 16:24:30 +08:00
Gitea Actions Bot
afcd360603 [patch] 修改nsis安装流程 2025-10-11 16:19:58 +08:00
Gitea Actions Bot
3352a59ff9 [patch] 修改nsis安装流程 2025-10-11 16:15:22 +08:00
Gitea Actions Bot
468a092fd9 [patch] 修改nsis安装流程 2025-10-11 16:11:26 +08:00
Gitea Actions Bot
7c0667cb39 [patch] 解决乱码bug 2025-10-11 16:08:34 +08:00
Gitea Actions Bot
2d05024e82 [patch] 解决乱码bug 2025-10-11 16:04:35 +08:00
Gitea Actions Bot
2dddcc9804 [patch] 添加develop用于测试 2025-10-11 15:56:12 +08:00
Gitea Actions Bot
d879626b56 [patch] 修复CI/CD中的GBK编码问题 2025-10-11 15:54:06 +08:00
Gitea Actions Bot
8ecec1edbe 实现更新版本管理 2025-10-11 15:46:39 +08:00
Gitea Actions Bot
95534f5304 [skip ci] Update version to v1.4.29 2025-10-11 15:30:39 +08:00
160b3f38ed [patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 2025-10-11 15:30:12 +08:00
Gitea Actions Bot
2dd384e8e4 [skip ci] Update version to v1.4.28 2025-10-11 10:17:21 +08:00
c6ae22c7e1 [patch] PDD登录状态GUI回调优化 与 非原生git指令导致check任务失败 2025-10-11 10:17:01 +08:00
a6a9e5861e [patch] PDD登录状态GUI回调优化 2025-10-11 10:16:13 +08:00
Gitea Actions Bot
a584f2a61a [skip ci] Update version to v1.4.27 2025-10-11 10:15:03 +08:00
b80e205680 [patch] PDD登录状态GUI回调优化 2025-10-11 10:14:45 +08:00
Gitea Actions Bot
9a2ea3f2a3 [skip ci] Update version to v1.4.26 2025-10-11 10:13:08 +08:00
80e9bba722 [patch] PDD登录状态GUI回调优化 2025-10-11 10:12:45 +08:00
66dd6051e5 [patch] PDD登录状态GUI回调优化 2025-10-11 10:09:59 +08:00
3ab1c82a37 [patch] PDD登录状态GUI回调优化 2025-10-11 10:08:10 +08:00
d72fe1a781 [patch] PDD登录状态GUI回调优化 2025-10-11 10:03:58 +08:00
b7d9d73de5 [patch] PDD登录状态GUI回调优化 2025-10-11 09:59:26 +08:00
ca65024cd2 [patch] PDD登录状态GUI回调优化 2025-10-11 09:55:21 +08:00
jjz
2ae051f07d [skip ci] Update version to v1.4.24 2025-10-10 16:17:36 +08:00
31ca5d0819 [patch] 优化打包逻辑 控制变量为日志产出 和 安装包整合路径统一 2025-10-10 16:16:36 +08:00
jjz
9ff009712a [skip ci] Update version to v1.4.23 2025-10-10 15:35:24 +08:00
0d9ab498b1 Merge remote-tracking branch 'origin/develop' into develop 2025-10-10 15:34:36 +08:00
12c1b1dfb8 [patch] 优化版本管理显示 与 更新提示(修改表名信息) 修复因线程安全问题导致的崩溃 2025-10-10 15:34:26 +08:00
jjz
fa2b0486d0 [skip ci] Update version to v1.4.22 2025-10-10 14:42:07 +08:00
Gitea Actions
8c150054a0 强制空提交 2025-10-10 14:41:15 +08:00
jjz
a9cb0c6b4f [skip ci] Update version to v1.4.21 2025-10-10 14:39:19 +08:00
Gitea Actions
95acd6e76a 强制空提交 2025-10-10 14:38:28 +08:00
jjz
aab2a6767e [skip ci] Update version to v1.4.19 2025-10-10 14:32:04 +08:00
Gitea Actions
00391fba67 Merge branch 'develop' of http://120.92.90.209:3000/magua/shuidrop_gui into develop 2025-10-10 14:30:54 +08:00
Gitea Actions
f9cc46f879 [patch] 增强Git提交调试日志 2025-10-10 14:29:04 +08:00
jjz
a0c97dc4c2 [skip ci] Update version to v1.4.18 2025-10-10 14:27:28 +08:00
Gitea Actions
e60c99d81e [patch] 增强版本更新日志输出 2025-10-10 14:24:46 +08:00
jjz
734dd8d1fe [skip ci] Update version to v1.4.17 2025-10-10 14:20:35 +08:00
jjz
1b196ac4ca [skip ci] Update version to v1.4.16 2025-10-10 14:18:27 +08:00
Gitea Actions
0bdac1bff8 强制空提交 2025-10-10 14:17:27 +08:00
Gitea Actions
82e31e0ef0 Merge remote-tracking branch 'origin/develop' into develop 2025-10-10 14:16:52 +08:00
Gitea Actions
707ace7ba1 [patch] 修复CI/CD提交消息乱码问题 2025-10-10 14:15:24 +08:00
jjz
bc419e2d22 [skip ci] 馃 鑷姩鏇存柊鐗堟湰鍒?v1.4.15 2025-10-10 14:07:50 +08:00
Gitea Actions
dddf9a7a74 强制空提交 2025-10-10 13:55:04 +08:00
Gitea Actions
d47b9536ee 强制空提交 2025-10-10 13:53:09 +08:00
Gitea Actions
533b1d2b01 Merge remote-tracking branch 'origin/develop' into develop 2025-10-10 13:51:00 +08:00
Gitea Actions
1868d26472 优化ci/cd 2025-10-10 13:49:49 +08:00
jjz
1dcaf847a6 [skip ci] 馃 鑷姩鏇存柊鐗堟湰鍒?v1.4.14 2025-10-10 13:47:32 +08:00
Gitea Actions
8eb17baf35 强制空提交 2025-10-10 13:46:12 +08:00
jjz
e67392e2c0 [skip ci] 馃 鑷姩鏇存柊鐗堟湰鍒?v1.4.13 2025-10-10 12:05:19 +08:00
6ad58b81b0 Merge remote-tracking branch 'origin/develop' into develop 2025-10-10 12:04:29 +08:00
42633da853 [patch] 优化版本管理显示 与 更新提示(修改表名信息) 修复因线程安全问题导致的崩溃 2025-10-10 12:04:17 +08:00
jjz
b4e7d957e4 [skip ci] 馃 鑷姩鏇存柊鐗堟湰鍒?v1.4.12 2025-10-10 11:54:57 +08:00
c819bdaa1c [patch] 优化版本管理显示 与 更新提示(修改表名信息) 2025-10-10 11:53:56 +08:00
bb5ef9ccbf [patch] 优化版本管理显示 与 更新提示(修改表名信息) 2025-10-10 10:53:16 +08:00
982e896916 [patch] 优化版本管理显示 与 更新提示 2025-10-10 10:41:57 +08:00
a21e2693fa [patch] 优化版本管理显示 2025-10-10 10:10:07 +08:00
Gitea Actions
e7a7bd2644 提交ci/cd文件 2025-10-09 17:49:50 +08:00
Gitea Actions
b64f15a483 提交ci/cd文件 2025-10-09 17:49:19 +08:00
26 changed files with 8973 additions and 1132 deletions

View File

@@ -258,6 +258,9 @@ class DatabaseVersionManager:
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:
@@ -298,32 +301,58 @@ class DatabaseVersionManager:
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.warning(f"配置文件不存在: {self.config_file}")
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.]+["\']'
replacement = f'APP_VERSION = "{new_version}"'
match = re.search(pattern, content)
if 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.warning("未找到APP_VERSION配置项")
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}")
logger.error(f"更新config.py失败: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def create_version_record(self) -> dict:
@@ -369,6 +398,11 @@ class DatabaseVersionManager:
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,
@@ -378,6 +412,7 @@ class DatabaseVersionManager:
'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
}
@@ -405,6 +440,7 @@ class DatabaseVersionManager:
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 '❌ 失败'}")

View 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())

View File

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

View File

@@ -2,7 +2,7 @@ name: GUI Version Release
on:
push:
branches: [ master ]
branches: [ master, develop ] # Trigger on master and develop branches
jobs:
gui-version-release:
@@ -13,24 +13,35 @@ jobs:
working-directory: E:\shuidrop_gui
steps:
# Step 1: Clone repository manually
- name: Clone repository
# Step 1: Pull latest code
- name: Pull latest code
shell: powershell
run: |
Write-Host "Cloning repository..."
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 "";
# Change to workspace directory
cd E:\shuidrop_gui
git config core.autocrlf false;
git config core.longpaths true;
Write-Host "Current directory: $(Get-Location)"
Write-Host "Git status:"
git status
Write-Host "Fetching from origin...";
git fetch origin;
Write-Host "Fetching latest changes..."
git fetch origin
git reset --hard origin/master
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 "==========================================";
Write-Host "Repository ready"
# Step 2: Check Python environment
- name: Check Python environment
@@ -67,6 +78,13 @@ jobs:
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
@@ -120,59 +138,546 @@ jobs:
DB_PASSWORD: password_ee2iQ3
DB_PORT: 5400
# Step 5: Commit changes back to repository
# 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 "Committing version changes..."
Write-Host "========================================";
Write-Host "Step 5: Commit version changes";
Write-Host "========================================";
# Read version from config.py
$VERSION = ""
# Read new version number
$VERSION = "";
if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw
$configContent = Get-Content "config.py" -Raw;
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]
Write-Host "New version: $VERSION"
$VERSION = $matches[1];
Write-Host "New version: $VERSION";
}
}
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
# Configure Git
git config user.name "Gitea Actions Bot";
git config user.email "bot@gitea.local";
git add config.py
git add version_history.json
# 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;
$hasChanges = git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "[skip ci] Update version to $VERSION"
git push origin master
Write-Host "Version changes committed and pushed"
} else {
Write-Host "No changes to commit"
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: |
# Read version from config.py
$VERSION = "Unknown"
$VERSION = "Unknown";
if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw
$configContent = Get-Content "config.py" -Raw;
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]
$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 "=========================================="
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 "==========================================";

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -13,6 +13,8 @@ import requests
import json
import time
import threading
import uuid
import os
# 定义持久化数据类
@@ -155,6 +157,14 @@ class FixJdCookie:
self.max_reconnect_delay = 60.0 # 最大重连延迟
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"):
"""内部日志方法"""
if self.log_callback:
@@ -177,6 +187,11 @@ class FixJdCookie:
"""初始化 socket"""
await self.send_heartbeat(ws, aid, pin_zj)
print("开始监听初始化")
# 🔧 修复生成唯一设备ID避免多端互踢
import uuid
unique_device_id = f"shuidrop_gui_{uuid.uuid4().hex[:16]}"
auth = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"aid": aid,
@@ -185,8 +200,9 @@ class FixJdCookie:
"type": "auth",
"body": {"presence": 1, "clientVersion": "2.6.3"},
"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))
@@ -210,6 +226,80 @@ class FixJdCookie:
}
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):
"""异步客服转接 在发送的消息为客服转接的关键词的时候"""
message = {
@@ -233,10 +323,27 @@ class FixJdCookie:
traceback.print_exc()
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:
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",
@@ -264,6 +371,528 @@ class FixJdCookie:
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 = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": pin},
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(time.time() * 1000),
"readFlag": 0,
"body": {
"height": upload_result.get("height", 0),
"width": upload_result.get("width", 0),
"url": upload_result.get("path"),
"translated": "",
"param": {"cusVenderId": vender_id},
"type": "image"
}
}
await ws.send(json.dumps(message))
self._log(f"✅ 图片消息发送成功: {pin}", "SUCCESS")
except Exception as e:
self._log(f"❌ 图片消息发送失败: {e}", "ERROR")
raise
finally:
# 清理临时文件
if temp_file and os.path.exists(temp_file):
try:
os.remove(temp_file)
self._log(f"🗑️ 已清理临时文件: {temp_file}", "DEBUG")
except Exception:
pass
async def send_video_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, video_url: str):
"""发送视频消息
Args:
ws: WebSocket连接
pin: 客户pin
aid: 账号aid
pin_zj: 客服pin
vender_id: 商家ID
video_url: 视频URL外部URL需要下载后上传
"""
temp_video = None
try:
self._log(f"🎥 开始发送视频消息: {video_url}", "INFO")
# 1. 下载视频
temp_video = await self.download_file(video_url)
# 2. 提取并上传封面
thumbnail_result = await self.get_video_thumbnail(temp_video)
# 3. 上传视频
await asyncio.sleep(2) # 等待2秒避免请求过快
video_result = await self.upload_file_to_jd(temp_video, "video")
# 4. 发送视频消息
message = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": pin},
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(time.time() * 1000),
"readFlag": 0,
"body": {
"desc": "",
"duration": 6,
"param": {},
"reupload": "false",
"size": os.path.getsize(temp_video),
"thumbHeight": thumbnail_result.get("height", 0),
"thumbWidth": thumbnail_result.get("width", 0),
"thumbnail": thumbnail_result.get("path"),
"url": video_result.get("path"),
"type": "video"
}
}
await ws.send(json.dumps(message))
self._log(f"✅ 视频消息发送成功: {pin}", "SUCCESS")
except Exception as e:
self._log(f"❌ 视频消息发送失败: {e}", "ERROR")
raise
finally:
# 清理临时文件
if temp_video and os.path.exists(temp_video):
try:
os.remove(temp_video)
self._log(f"🗑️ 已清理临时视频: {temp_video}", "DEBUG")
except Exception:
pass
def get_userinfo(self, response_text):
"""获取用户 pin 并存入 pins 列表"""
pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin")
@@ -475,6 +1104,11 @@ class FixJdCookie:
print("✅ DEBUG 进入message_monitoring方法")
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
store_id = str(store.get('id', '')) or str(vender_id)
self._log(f"🔗 尝试连接后端服务店铺ID: {store_id}", "DEBUG")
@@ -506,6 +1140,13 @@ class FixJdCookie:
# print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}")
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']}"
loop = asyncio.get_running_loop()
@@ -516,7 +1157,8 @@ class FixJdCookie:
aid=aid,
pin_zj=pin_zj,
platform="京东",
loop=loop
loop=loop,
cookies_str=cookies_str # 🔥 传递cookies用于文件上传
)
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
@@ -530,12 +1172,44 @@ class FixJdCookie:
print(f"等待监听消息-{datetime.now()}")
response = await asyncio.wait_for(ws.recv(), timeout=1)
print(f"原始消息类型:{type(response)}, 消息体为: {response}")
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
# 安全解析消息
# 🔧 修复:检测被踢下线消息
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
print(json_resp)
# 检查是否为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)
# print(json_resp)
ver = json_resp.get("ver")
print(f"版本{ver}")
@@ -579,9 +1253,8 @@ class FixJdCookie:
if not await self.handle_reconnect(e):
break
# 关闭后端服务连接
# 关闭后端服务连接JDBackendService没有close方法跳过
if self.backend_connected:
await self.backend_service.close()
self.backend_connected = False
self._log("🛑 消息监听已停止", "INFO")

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,6 +4,8 @@ from typing import Optional, Callable
import threading
import asyncio
from threading import Thread
from PyQt5.QtCore import QObject, pyqtSignal
# 创建新的后端客户端
from WebSocket.BackendClient import BackendClient
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.QianNiu.QianNiuUtils import QianNiuListenerForGUI as QNListenerForGUI_WS
class PlatformConnectionSignals(QObject):
"""平台连接信号(线程安全)"""
platform_connected = pyqtSignal(str, str) # (platform_name, store_id)
_backend_client = None
@@ -36,16 +44,27 @@ class WebSocketManager:
self.gui_update_callback = None
self.platform_listeners = {} # 存储各平台的监听器
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 = {
'log': None,
'success': None,
'error': None,
'platform_connected': None,
'token_error': None,
'disconnect': 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:
self.callbacks['log'] = log
@@ -55,8 +74,14 @@ class WebSocketManager:
self.callbacks['error'] = error
if platform_connected: # ← 新增
self.callbacks['platform_connected'] = platform_connected
if platform_disconnected:
self.callbacks['platform_disconnected'] = platform_disconnected
if 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"):
"""内部日志方法"""
@@ -65,8 +90,16 @@ class WebSocketManager:
else:
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):
"""通知GUI平台连接成功"""
"""通知GUI平台连接成功(仅在主线程中调用)"""
try:
if platform_name not in self.connected_platforms:
self.connected_platforms.append(platform_name)
@@ -77,16 +110,103 @@ class WebSocketManager:
except Exception as e:
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:
"""连接后端WebSocket"""
try:
# 1 保存token到配置
# 🔥 根据配置决定是否保存token
# 生产模式保存token方便用户下次自动加载
# 测试模式:不保存,避免多实例冲突
import config as cfg
if not cfg.is_multi_instance_mode():
try:
from config import set_saved_token
set_saved_token(token)
except Exception:
pass
self._log("生产模式已保存token到配置文件", "INFO")
except Exception as e:
self._log(f"保存token失败: {e}", "WARNING")
else:
self._log("测试模式不保存token支持多实例运行", "INFO")
# 2 获取或创建后端客户端
backend = get_backend_client()
@@ -115,19 +235,36 @@ class WebSocketManager:
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
def _on_backend_login(platform_name: str, store_id: str, cookies: str, store_name: str = ""):
store_display = store_name or store_id
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")
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):
self._log(f"Token验证失败: {error_content}", "ERROR")
if self.callbacks['token_error']:
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,
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:
backend.connect()
@@ -141,11 +278,12 @@ class WebSocketManager:
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(
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")
self._handle_platform_login(platform_name, store_id, cookies)
self._handle_platform_login(platform_name, store_id, cookies, store_name)
def _on_backend_success():
try:
@@ -162,7 +300,24 @@ class WebSocketManager:
if self.callbacks['token_error']:
self.callbacks['token_error'](error_content)
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error)
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()
set_backend_client(backend)
@@ -176,17 +331,106 @@ class WebSocketManager:
self.callbacks['error'](str(e))
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:
# 🔥 检查并清理当前店铺的旧连接
# 🔥 检查并断开当前店铺的旧连接策略B先断开旧连接再建立新连接
store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式
keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)]
if keys_to_remove:
self._log(f"🔄 检测到店铺 {store_id}连,清理 {len(keys_to_remove)} 个旧连接", "INFO")
self._log(f"🔄 检测到店铺 {store_id}复登录,断开 {len(keys_to_remove)} 个旧连接", "INFO")
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)
# 给WebSocket一点时间完全关闭
import time
time.sleep(0.5)
self._log(f"✅ 旧连接已全部断开,准备建立新连接", "INFO")
# 平台名称映射
platform_map = {
"淘宝": "千牛",
@@ -211,16 +455,16 @@ class WebSocketManager:
if cookies == "login_success":
self._log("⚠️ 千牛平台收到空cookies但允许启动监听器", "WARNING")
cookies = "" # 清空cookies千牛不需要真实cookies
self._start_qianniu_listener(store_id, cookies)
self._start_qianniu_listener(store_id, cookies, store_name)
elif normalized_platform == "京东":
self._start_jd_listener(store_id, cookies)
self._start_jd_listener(store_id, cookies, store_name)
elif normalized_platform == "抖音":
self._start_douyin_listener(store_id, cookies)
self._start_douyin_listener(store_id, cookies, store_name)
elif normalized_platform == "拼多多":
self._start_pdd_listener(store_id, cookies)
self._start_pdd_listener(store_id, cookies, store_name)
else:
self._log(f"❌ 不支持的平台: {platform_name}", "ERROR")
@@ -245,13 +489,20 @@ class WebSocketManager:
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
def _on_update_available(self, latest_version, download_url):
"""发现新版本时的处理"""
"""发现新版本时的处理(在子线程中调用)"""
self._log(f"🔔 发现新版本 {latest_version}", "INFO")
# 通知主GUI显示更新提醒
# 通知主GUI显示更新提醒(通过 Qt 信号机制,线程安全)
if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
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:
def _runner():
@@ -269,7 +520,8 @@ class WebSocketManager:
self.platform_listeners[f"京东:{store_id}"] = {
'thread': thread,
'platform': '京东',
'store_id': store_id
'store_id': store_id,
'store_name': store_name # 保存店铺名称
}
# 上报连接状态给后端
@@ -300,7 +552,7 @@ class WebSocketManager:
except Exception as send_e:
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:
def _runner():
@@ -318,14 +570,13 @@ class WebSocketManager:
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
# 🔥 详细的结果分析(与拼多多完全一致
# 详细的结果分析(仅日志记录GUI 已在主线程中通知
if result == "need_verification_code":
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
elif result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
elif result:
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
else:
self._log("❌ [DY] 登录失败", "ERROR")
else:
@@ -350,10 +601,9 @@ class WebSocketManager:
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时也要通知GUI
# Cookie启动成功时记录日志GUI 已在主线程中通知)
if result:
self._log("✅ [DY] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
# 🔥 移除不再在backend_singleton中发送connect_message
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送与拼多多保持一致
@@ -373,13 +623,17 @@ class WebSocketManager:
'thread': thread,
'platform': '抖音',
'store_id': store_id,
'store_name': store_name # 保存店铺名称
}
# 更新监听器状态
if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# ✅ 临时方案:启动后立即通知(因为 DY 监听器也会阻塞)
# DY 内部会处理验证码流程,失败时会向后端上报相应状态
self._log("已启动抖音平台监听", "SUCCESS")
self._notify_platform_connected("抖音") # ← 立即通知
except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
@@ -387,7 +641,7 @@ class WebSocketManager:
# 🔥 移除:确保失败时也不在这里上报状态
# 失败状态应该在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:
# 获取用户token从后端客户端获取
@@ -416,6 +670,7 @@ class WebSocketManager:
'thread': thread,
'platform': '千牛',
'store_id': store_id,
'store_name': store_name, # 保存店铺名称
'exe_token': exe_token
}
@@ -447,49 +702,46 @@ class WebSocketManager:
except Exception as send_e:
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:
# 在创建新实例前,清理旧实例的定时器(避免遗留定时器触发)
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():
try:
self._log("🚀 开始创建拼多多监听器实例", "DEBUG")
listener = PDDListenerForGUI_WS(log_callback=self._log)
self._log("✅ 拼多多监听器实例创建成功", "DEBUG")
# 传递session_timeout_signal用于会话过期重试
listener = PDDListenerForGUI_WS(
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
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))
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:
# 使用Cookie启动兼容旧方式
self._log("🍪 使用Cookie启动拼多多监听器", "INFO")
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时也要通知GUI
if result:
self._log("✅ [PDD] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("拼多多")
# 根据实际登录结果上报状态给后端
if self.backend_client and result not in ["need_verification_code", "verification_code_error",
"login_failure"]:
# 如果是特殊状态,说明通知已经在PddLogin中发送,不需要重复发送
"login_failure", "cookie_expired"]:
# 🔥 如果是特殊状态包括cookie_expired,说明通知已经发送,不需要重复发送
try:
message = {
"type": "connect_message",
@@ -508,6 +760,8 @@ class WebSocketManager:
self._log("验证码错误错误通知已由PddLogin发送等待后端处理", "INFO")
elif result == "login_failure":
self._log("登录失败失败通知已由PddLogin发送等待后端处理", "INFO")
elif result == "cookie_expired":
self._log("🔄 Cookie已过期过期通知已发送等待后端重新下发Cookie", "INFO")
return result
@@ -541,9 +795,13 @@ class WebSocketManager:
'thread': thread,
'platform': '拼多多',
'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:
self._log(f"启动拼多多平台监听失败: {e}", "ERROR")
@@ -563,29 +821,19 @@ class WebSocketManager:
def _is_pdd_login_params(self, data: str) -> bool:
"""判断是否为拼多多登录参数"""
try:
self._log(f"🔍 [DEBUG] 检查是否为登录参数,数据长度: {len(data)}", "DEBUG")
self._log(f"🔍 [DEBUG] 数据前100字符: {data[:100]}", "DEBUG")
import json
parsed_data = json.loads(data)
self._log(f"🔍 [DEBUG] JSON解析成功键: {list(parsed_data.keys())}", "DEBUG")
login_params = parsed_data.get("data", {}).get("login_params", {})
self._log(f"🔍 [DEBUG] login_params存在: {bool(login_params)}", "DEBUG")
if not login_params:
self._log("🔍 [DEBUG] login_params为空返回False", "DEBUG")
return False
# 检查必需的登录参数字段
required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"]
has_all_fields = all(field in login_params for field in required_fields)
self._log(f"🔍 [DEBUG] 包含所有必需字段: {has_all_fields}", "DEBUG")
self._log(f"🔍 [DEBUG] 现有字段: {list(login_params.keys())}", "DEBUG")
return has_all_fields
except Exception as e:
self._log(f"🔍 [DEBUG] 解析失败: {e}", "DEBUG")
except Exception:
return False
def send_message(self, message: dict):
@@ -621,3 +869,4 @@ def get_websocket_manager() -> WebSocketManager:
if _websocket_manager is None:
_websocket_manager = WebSocketManager()
return _websocket_manager

489
auto_updater.py Normal file
View 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("❌ 重启启动器创建失败")

View File

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

View File

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

344
custom_update_dialog.py Normal file
View 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)

136
fix_pyminiracer_dll.py Normal file
View 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()

View File

@@ -21,48 +21,69 @@ class NSISInstaller:
self.assets_dir = self.script_dir / "assets"
self.nsis_script = self.script_dir / "installer.nsi"
# 应用程序信息
self.app_name = "水滴AI客服智能助手"
# 应用程序信息使用英文避免CI/CD环境乱码
self.app_name = "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.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):
"""检查构建前提条件"""
print("🔍 检查构建前提条件...")
print("Checking build prerequisites...")
# 检查NSIS安装
try:
result = subprocess.run(['makensis', '/VERSION'],
capture_output=True, text=True, check=True)
nsis_version = result.stdout.strip()
print(f"NSIS 版本: {nsis_version}")
print(f"NSIS version: {nsis_version}")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ 错误: 未找到NSIS或makensis命令")
print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS")
print(" 确保将NSIS添加到系统PATH环境变量")
print("ERROR: NSIS or makensis command not found")
print(" Please download and install NSIS from https://nsis.sourceforge.io/Download")
print(" Make sure to add NSIS to system PATH")
return False
# 检查dist目录
if not self.dist_dir.exists():
print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}")
print(" 请先运行 build_production.py 构建应用程序")
print(f"ERROR: Build output directory not found: {self.dist_dir}")
print(" Please run build_production.py first")
return False
# 检查主程序文件
exe_path = self.dist_dir / self.exe_name
if not exe_path.exists():
print(f"❌ 错误: 未找到主程序文件 {exe_path}")
print(f"ERROR: Main executable not found: {exe_path}")
return False
print(f"✅ 找到主程序: {exe_path}")
print(f"Found main executable: {exe_path}")
return True
def prepare_assets(self):
"""准备安装包资源文件"""
print("📁 准备资源文件...")
print("Preparing asset files...")
# 创建assets目录
self.assets_dir.mkdir(exist_ok=True)
@@ -86,14 +107,14 @@ class NSISInstaller:
img = Image.open(icon_source)
ico_path = self.assets_dir / "icon.ico"
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"✅ 转换主程序图标: {icon_source.name} -> icon.ico")
print(f"Converted main icon: {icon_source.name} -> icon.ico")
icon_found = True
break
except ImportError:
print("⚠️ 未安装PIL库跳过图标转换")
print("WARNING: PIL library not installed, skipping icon conversion")
break
except Exception as e:
print(f"⚠️ 主程序图标转换失败: {e}")
print(f"WARNING: Main icon conversion failed: {e}")
continue
# 转换卸载程序图标
@@ -104,17 +125,17 @@ class NSISInstaller:
img = Image.open(uninstall_icon_source)
uninstall_ico_path = self.assets_dir / "uninstall_icon.ico"
img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"✅ 转换卸载程序图标: {uninstall_icon_source.name} -> uninstall_icon.ico")
print(f"Converted uninstall icon: {uninstall_icon_source.name} -> uninstall_icon.ico")
uninstall_icon_found = True
except ImportError:
print("⚠️ 未安装PIL库跳过卸载图标转换")
print("WARNING: PIL library not installed, skipping uninstall icon conversion")
except Exception as e:
print(f"⚠️ 卸载程序图标转换失败: {e}")
print(f"WARNING: Uninstall icon conversion failed: {e}")
else:
print("⚠️ 未找到卸载程序专用图标,将使用主程序图标")
print("WARNING: Uninstall icon not found, will use main icon")
if not icon_found:
print("⚠️ 未找到主程序图标文件,将使用默认图标")
print("WARNING: Main icon not found, will use default icon")
return {
'main_icon': icon_found,
@@ -123,16 +144,24 @@ class NSISInstaller:
def generate_nsis_script(self, icon_info):
"""生成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")}
Unicode True
# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题)
RequestExecutionLevel admin
# 定义应用程序信息
!define APP_NAME "{self.app_name}"
!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"
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 "FileDescription" "${{APP_NAME}} 安装程序"
VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}"
@@ -238,7 +267,7 @@ SectionEnd
with open(self.nsis_script, 'w', encoding='utf-8-sig') as f:
f.write(nsis_content)
print(f"NSIS脚本已生成: {self.nsis_script}")
print(f"NSIS script generated: {self.nsis_script}")
return installer_name
def create_license_file(self):
@@ -263,11 +292,11 @@ SectionEnd
with open(license_file, 'w', encoding='utf-8-sig') as f:
f.write(license_content)
print(f"✅ 许可证文件已创建: {license_file}")
print(f"License file created: {license_file}")
def build_installer(self):
"""构建安装包"""
print("🚀 开始构建NSIS安装包...")
print("Building NSIS installer...")
# 创建输出目录
self.output_dir.mkdir(exist_ok=True)
@@ -278,22 +307,22 @@ SectionEnd
result = subprocess.run(cmd, cwd=str(self.script_dir),
capture_output=True, text=True, check=True)
print("NSIS编译成功")
print("NSIS compilation successful")
if result.stdout:
print("NSIS输出:")
print("NSIS output:")
print(result.stdout)
return True
except subprocess.CalledProcessError as e:
print("❌ NSIS编译失败")
print(f"错误信息: {e.stderr}")
print("ERROR: NSIS compilation failed")
print(f"Error message: {e.stderr}")
return False
def run(self):
"""执行完整的构建流程"""
print("=" * 60)
print(f"🔧 {self.app_name} 安装包构建工具")
print(f"Installer Build Tool for {self.app_name}")
print("=" * 60)
try:
@@ -319,18 +348,18 @@ SectionEnd
if installer_path.exists():
installer_size = installer_path.stat().st_size / (1024 * 1024)
print("\n" + "=" * 60)
print("🎉 安装包构建成功!")
print(f"📁 安装包位置: {installer_path}")
print(f"📏 文件大小: {installer_size:.1f} MB")
print("🚀 可以直接分发给用户使用")
print("Installer build completed successfully!")
print(f"Installer location: {installer_path}")
print(f"File size: {installer_size:.1f} MB")
print("Ready to distribute to users")
print("=" * 60)
return True
else:
print("❌ 安装包文件未找到")
print("ERROR: Installer file not found")
return False
except Exception as e:
print(f"❌ 构建过程出错: {e}")
print(f"ERROR: Build process error: {e}")
return False
def main():

1131
main.py

File diff suppressed because it is too large Load Diff

View File

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

BIN
static/hengfu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

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

View File

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

474
static/js/sign.js Normal file
View 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
View 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):
"""
更新下载进度阶段10-50%
Args:
downloaded: 已下载字节数
total: 总字节数
"""
if self.current_stage != self.STAGE_DOWNLOAD:
return # 不在下载阶段,忽略
if total > 0:
# 计算下载百分比0-100
download_percent = int((downloaded / total) * 100)
self.download_progress = download_percent
# 映射到总进度的0-50%
overall_progress = int(download_percent * 0.5)
# 更新圆形进度条
self.circular_progress.setValue(overall_progress)
self.circular_progress.setText(f"{overall_progress}%")
# 转换为MB显示
downloaded_mb = downloaded / (1024 * 1024)
total_mb = total / (1024 * 1024)
# 更新状态文本
self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB")
else:
# 无法获取总大小时显示不确定状态
self.status_label.setText("正在下载,请稍候...")
def show_retry_info(self, current_retry, max_retries):
"""
显示重试信息
Args:
current_retry: 当前重试次数
max_retries: 最大重试次数
"""
self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})")
self.retry_label.show()
# 重置下载进度
self.download_progress = 0
self.circular_progress.setValue(0)
self.circular_progress.setText("0%")
self.status_label.setText("正在重试连接...")
# 更新标题
self.title_label.setText(f"正在重试下载 v{self.version}")
def download_finished(self, file_path):
"""
下载完成进入UAC授权阶段阶段250-60%
Args:
file_path: 下载的文件路径
"""
self.current_stage = self.STAGE_UAC
# 更新进度到50%
self.circular_progress.setValue(50)
self.circular_progress.setText("50%")
# 更新阶段提示
self.stage_label.setText("🔐 等待管理员授权...")
self.title_label.setText(f"✅ 下载完成")
self.status_label.setText("请在UAC窗口点击「允许」以继续...")
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
# 禁用取消按钮
self.cancel_button.setEnabled(False)
self.retry_label.hide()
# 启动UAC等待动画50% → 60%
self.start_uac_waiting_animation()
def start_uac_waiting_animation(self):
"""启动UAC等待动画进度条在50-60%之间缓慢移动)"""
self.uac_timer = QTimer(self)
self.uac_progress = 50
def update_uac_progress():
self.uac_progress += 0.5 # 每次增加0.5%
if self.uac_progress <= 60:
self.circular_progress.setValue(int(self.uac_progress))
self.circular_progress.setText(f"{int(self.uac_progress)}%")
else:
# 超过60%停止(等待用户授权)
self.uac_timer.stop()
self.uac_timer.timeout.connect(update_uac_progress)
self.uac_timer.start(500) # 每500ms更新一次
def uac_authorized(self):
"""
用户已授权UAC进入安装阶段阶段360-90%
"""
# 停止UAC等待动画
if hasattr(self, 'uac_timer'):
self.uac_timer.stop()
self.current_stage = self.STAGE_INSTALL
# 更新进度到60%
self.circular_progress.setValue(60)
self.circular_progress.setText("60%")
# 更新阶段提示
self.stage_label.setText("⚙️ 正在安装新版本...")
self.title_label.setText("正在安装")
self.status_label.setText("正在替换程序文件,请稍候...")
self.status_label.setStyleSheet("color: #007bff; font-size: 11px; font-weight: bold;")
# 启动安装进度动画60% → 90%
self.start_install_animation()
def start_install_animation(self):
"""启动安装进度动画60% → 90%"""
self.install_timer = QTimer(self)
self.install_progress = 60
def update_install_progress():
self.install_progress += 1 # 每次增加1%
if self.install_progress <= 90:
self.circular_progress.setValue(self.install_progress)
self.circular_progress.setText(f"{self.install_progress}%")
else:
# 安装完成,进入重启阶段
self.install_timer.stop()
self.prepare_restart()
self.install_timer.timeout.connect(update_install_progress)
self.install_timer.start(800) # 每800ms增加1%共24秒
def prepare_restart(self):
"""
准备重启阶段490-100%
"""
self.current_stage = self.STAGE_RESTART
# 更新进度到90%
self.circular_progress.setValue(90)
self.circular_progress.setText("90%")
# 更新阶段提示
self.stage_label.setText("🚀 准备启动新版本...")
self.title_label.setText("即将完成")
self.status_label.setText("程序即将重启,请稍候...")
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
# 启动重启倒计时动画90% → 100%
self.start_restart_animation()
def start_restart_animation(self):
"""启动重启倒计时动画90% → 100%"""
self.restart_timer = QTimer(self)
self.restart_progress = 90
def update_restart_progress():
self.restart_progress += 2 # 每次增加2%
if self.restart_progress <= 100:
self.circular_progress.setValue(self.restart_progress)
self.circular_progress.setText(f"{self.restart_progress}%")
if self.restart_progress == 100:
# 🔥 到达100%,更新提示但不自动关闭
self.status_label.setText("✅ 安装完成!等待启动确认...")
self.stage_label.setText("🎉 更新成功!")
self.restart_timer.stop() # 停止定时器
else:
self.restart_timer.stop()
self.restart_timer.timeout.connect(update_restart_progress)
self.restart_timer.start(500) # 每500ms增加2%共2.5秒)
def download_error(self, error_msg):
"""
下载失败
Args:
error_msg: 错误信息
"""
self.title_label.setText(f"❌ 下载失败")
# 错误信息截断(避免太长)
if len(error_msg) > 80:
display_msg = error_msg[:80] + "..."
else:
display_msg = error_msg
self.stage_label.setText("⚠️ 更新失败")
self.status_label.setText(display_msg)
self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;")
self.cancel_button.setText("关闭")
self.cancel_button.setEnabled(True)
self.retry_label.hide()
def cancel_download(self):
"""取消下载"""
if self.current_stage == self.STAGE_DOWNLOAD and self.downloader and self.downloader.isRunning():
# 下载阶段可以取消
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(
self,
"确认取消",
"确定要取消更新吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.downloader.cancel()
self.downloader.wait()
self.status_label.setText("更新已取消")
self.reject() # 会自动清理定时器
else:
# 其他阶段或下载已完成,直接关闭
self.reject() # 会自动清理定时器
def cleanup_timers(self):
"""清理所有定时器"""
if hasattr(self, 'uac_timer'):
self.uac_timer.stop()
if hasattr(self, 'install_timer'):
self.install_timer.stop()
if hasattr(self, 'restart_timer'):
self.restart_timer.stop()
def accept(self):
"""对话框接受时清理资源"""
self.cleanup_timers()
super().accept()
def reject(self):
"""对话框拒绝时清理资源"""
self.cleanup_timers()
super().reject()
if __name__ == "__main__":
# 测试代码
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
# 创建测试对话框
dialog = UpdateProgressDialog("1.5.55")
dialog.show()
# 模拟完整更新流程
def simulate_update():
# 阶段1模拟下载 (0-50%)
def simulate_download():
total_size = 100 * 1024 * 1024 # 100MB
for i in range(0, 101, 5):
downloaded = int(i * total_size / 100)
dialog.update_progress(downloaded, total_size)
QApplication.processEvents()
QTimer.singleShot(100, lambda: None) # 延迟
# 下载完成
QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe"))
# 阶段2模拟UAC授权 (50-60%)
QTimer.singleShot(5000, lambda: dialog.uac_authorized())
simulate_download()
QTimer.singleShot(500, simulate_update)
sys.exit(app.exec_())

216
version_checker.py Normal file
View 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()

View File

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