├── script ├── huaruntong │ ├── 999 │ │ ├── api.py │ │ └── main.py │ ├── __init__.py │ ├── wentiweilaihui │ │ ├── api.py │ │ └── main.py │ ├── ole │ │ ├── api.py │ │ └── main.py │ └── huaruntong_wx │ │ ├── api.py │ │ └── main.py ├── wps │ ├── img.png │ └── README.md ├── sf │ ├── code.js │ ├── api.py │ └── main.py ├── smzdm │ ├── README.md │ ├── api │ │ └── sign_calculator.py │ └── sign_daily_task │ │ └── service.py ├── kanxue │ ├── api.py │ └── sign_in.py ├── enshan │ ├── api.py │ └── sign_in.py ├── erke │ ├── README.md │ ├── api.py │ └── main.py └── shyp │ ├── auto_buy.py │ └── api.py ├── LICENSE ├── config ├── notification.json └── token.json ├── .gitignore └── README.md /script/huaruntong/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/wps/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cat-zaizai/ZaiZaiCat-Checkin/HEAD/script/wps/img.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cat-zaizai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "bark": { 3 | "push": "", 4 | "icon": "", 5 | "sound": "birdsong", 6 | "group": "ZaiZaiCat-Checkin", 7 | "level": "active", 8 | "url": "" 9 | }, 10 | "server": { 11 | "sckey": "", 12 | "sendkey": "" 13 | }, 14 | "coolpush": { 15 | "skey": "", 16 | "qq": true, 17 | "wx": false, 18 | "email": false 19 | }, 20 | "qmsg": { 21 | "key": "", 22 | "type": "private" 23 | }, 24 | "telegram": { 25 | "bot_token": "", 26 | "user_id": "", 27 | "api_host": "", 28 | "proxy": "" 29 | }, 30 | "feishu": { 31 | "key": "" 32 | }, 33 | "dingtalk": { 34 | "access_token": "", 35 | "secret": "" 36 | }, 37 | "qywx": { 38 | "key": "", 39 | "corpid": "", 40 | "agentid": "", 41 | "corpsecret": "", 42 | "touser": "", 43 | "media_id": "", 44 | "origin": "" 45 | }, 46 | "pushplus": { 47 | "token": "", 48 | "topic": "" 49 | }, 50 | "pushdeer": { 51 | "pushkey": "", 52 | "url": "https://api2.pushdeer.com/message/push", 53 | "type": "text" 54 | }, 55 | "gotify": { 56 | "url": "", 57 | "token": "", 58 | "priority": "3" 59 | }, 60 | "ntfy": { 61 | "url": "https://ntfy.sh", 62 | "topic": "", 63 | "priority": "3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /script/sf/code.js: -------------------------------------------------------------------------------- 1 | window = global; 2 | 3 | navigator = {} 4 | navigator.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' 5 | location = { 6 | "ancestorOrigins": {}, 7 | "href": "https://y.qq.com/n/ryqq/toplist/4", 8 | "origin": "https://y.qq.com", 9 | "protocol": "https:", 10 | "host": "y.qq.com", 11 | "hostname": "y.qq.com", 12 | "port": "", 13 | "pathname": "/n/ryqq/toplist/4", 14 | "search": "", 15 | "hash": "" 16 | } 17 | proxy_array = ['window', 'document', 'location', 'navigator', 'history','screen' ] 18 | 19 | 20 | 21 | N = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g; 22 | const B = (t) => Buffer.from(t, "utf8").toString("base64"); 23 | 24 | 25 | V = (t, e=!1) => e ? (t => t.replace(/=/g, "").replace(/[+\/]/g, (t => "+" == t ? "-" : "_")))(B(t)) : B(t); 26 | 27 | var M = { 28 | randomUUID: "undefined" != typeof crypto && crypto.randomUUID && crypto.randomUUID.bind(crypto) 29 | }; 30 | 31 | function O(t, e, r) { 32 | if (M.randomUUID && !e && !t) 33 | return M.randomUUID(); 34 | const n = (t = t || {}).random || (t.rng || C)(); 35 | if (n[6] = 15 & n[6] | 64, 36 | n[8] = 63 & n[8] | 128, 37 | e) { 38 | r = r || 0; 39 | for (let t = 0; t < 16; ++t) 40 | e[r + t] = n[t]; 41 | return e 42 | } 43 | return function(t, e=0) { 44 | return (T[t[e + 0]] + T[t[e + 1]] + T[t[e + 2]] + T[t[e + 3]] + "-" + T[t[e + 4]] + T[t[e + 5]] + "-" + T[t[e + 6]] + T[t[e + 7]] + "-" + T[t[e + 8]] + T[t[e + 9]] + "-" + T[t[e + 10]] + T[t[e + 11]] + T[t[e + 12]] + T[t[e + 13]] + T[t[e + 14]] + T[t[e + 15]]).toLowerCase() 45 | }(n) 46 | } 47 | 48 | function ft(t, e) { 49 | var r = O() 50 | , n = String(V(r)) 51 | , i = String(V(O())) 52 | , o = String(V(t)) 53 | , a = String(V("web")) 54 | , s = String(V((null === location || void 0 === location ? void 0 : location.pathname) || "")) 55 | , c = String(V(e)); 56 | return { 57 | code: "".concat(1, "-").concat(n, "-").concat(i, "-").concat(0, "-").concat(o, "-").concat(a, "-").concat(s, "-").concat(c), 58 | traceId: r 59 | } 60 | } 61 | var key = "fb40817085be4e398e0b6f4b08177746" 62 | function get_sw8(url_path) { 63 | return ft(key, url_path) 64 | } 65 | 66 | console.log(ft("fb40817085be4e398e0b6f4b08177746","/mcs-mimp/commonPost/~memberEs~taskRecord~finishTask")) -------------------------------------------------------------------------------- /script/huaruntong/wentiweilaihui/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 华润通文体未来荟API接口 3 | """ 4 | import requests 5 | import uuid 6 | import time 7 | 8 | 9 | class WenTiWeiLaiHuiAPI: 10 | """文体未来荟API接口类""" 11 | 12 | def __init__(self, token, mobile, user_agent=None): 13 | """ 14 | 初始化API 15 | :param token: 认证token 16 | :param mobile: 手机号(用于显示) 17 | :param user_agent: 用户代理字符串 18 | """ 19 | self.token = token 20 | self.mobile = mobile 21 | self.base_url = "https://wtmp.crland.com.cn" 22 | self.user_agent = user_agent or 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf26405f0) XWEB/13910' 23 | self.headers = { 24 | 'User-Agent': self.user_agent, 25 | 'Content-Type': 'application/json', 26 | 'xweb_xhr': '1', 27 | 'x-hrt-mid-appid': 'API_AUTH_MINI', 28 | 'token': self.token, 29 | 'sec-fetch-site': 'cross-site', 30 | 'sec-fetch-mode': 'cors', 31 | 'sec-fetch-dest': 'empty', 32 | 'referer': 'https://servicewechat.com/wx3c35b1f0737c23ce/11/page-frame.html', 33 | 'accept-language': 'zh-CN,zh;q=0.9', 34 | 'priority': 'u=1, i' 35 | } 36 | 37 | def sign_in(self): 38 | """ 39 | 签到接口 40 | :return: 接口响应数据 41 | """ 42 | url = f"{self.base_url}/promotion/app/sign/signin" 43 | 44 | # 生成签到数据 45 | data = { 46 | "data": { 47 | "outOrderNo": str(uuid.uuid4()), 48 | "mobile": self.mobile, 49 | "timestamp": int(time.time() * 1000), 50 | "projectCode": "df2d2333f94f4c508073e0646610c021", 51 | "deviceChannel": "WECHAT", 52 | "businessChannel": "miniprogram", 53 | "channelCode": "wechat" 54 | } 55 | } 56 | 57 | try: 58 | response = requests.post(url, json=data, headers=self.headers) 59 | response.raise_for_status() 60 | return response.json() 61 | except Exception as e: 62 | return {"success": False, "msg": f"请求失败: {str(e)}"} 63 | 64 | def query_points(self): 65 | """ 66 | 查询万象星积分 67 | :return: 接口响应数据 68 | """ 69 | url = f"{self.base_url}/pointsAccount/app/queryAccount" 70 | 71 | try: 72 | response = requests.post(url, json={}, headers=self.headers) 73 | response.raise_for_status() 74 | return response.json() 75 | except Exception as e: 76 | return {"success": False, "msg": f"请求失败: {str(e)}"} 77 | 78 | -------------------------------------------------------------------------------- /script/smzdm/README.md: -------------------------------------------------------------------------------- 1 | # 什么值得买任务自动化脚本 2 | 3 | 一个功能完整的什么值得买(SMZDM)自动化任务脚本,支持多账号管理,自动完成各类任务并领取奖励。 4 | 5 | ## ✨ 功能特性 6 | 7 | - 🔐 **多账号支持** - 可配置多个账号,自动依次处理 8 | - 📋 **全任务支持** - 支持所有类型的任务自动执行 9 | - 📖 浏览文章任务 10 | - ⭐ 收藏文章任务 11 | - 👍 点赞文章任务 12 | - 🎁 申请众测任务 13 | - 📤 分享众测招募任务 14 | - 💰 **自动领奖** - 任务完成后自动领取奖励 15 | - 📊 **详细日志** - 美化的日志输出,同时记录到文件 16 | - ⚡ **智能签名** - 自动计算请求签名 17 | - 🛡️ **错误处理** - 完善的异常处理机制 18 | 19 | ## 📦 安装依赖 20 | 21 | ```bash 22 | pip install requests 23 | ``` 24 | 25 | ## ⚙️ 配置说明 26 | 27 | 在项目根目录的 `config/token.json` 中配置您的账号信息: 28 | 自行抓包获取相关内容,在headers中找到相关字段(搜索) 29 | ```json 30 | { 31 | "smzdm": { 32 | "accounts": [ 33 | { 34 | "account_name": "账号1", 35 | "cookie": "你的Cookie信息", 36 | "user_agent": "", 37 | "setting": "" 38 | } 39 | ] 40 | } 41 | } 42 | ``` 43 | 44 | ### 配置文件位置 45 | 46 | - **默认路径**: `ZaiZaiCat-Checkin/config/token.json` 47 | - **结构说明**: 配置文件采用统一管理,支持多个平台配置 48 | - **smzdm 节点**: 什么值得买相关配置都在此节点下 49 | 50 | ### Cookie 获取方法 51 | 52 | 1. 打开什么值得买APP/网站 53 | 2. 使用抓包工具(如Charles、Fiddler等) 54 | 3. 找到任意API请求的Cookie字段 55 | 4. 复制完整的Cookie值到配置文件 56 | 57 | ## 🚀 使用方法 58 | 59 | ```bash 60 | python main.py 61 | ``` 62 | 63 | 脚本将自动: 64 | 1. 加载配置的所有账号 65 | 2. 获取每个账号的任务列表 66 | 3. 自动执行所有未完成的任务 67 | 4. 完成后自动领取奖励 68 | 5. 输出详细的执行日志 69 | 70 | ## 📝 日志说明 71 | 72 | 日志同时输出到: 73 | - **控制台** - 实时显示执行进度(简洁格式) 74 | - **smzdm_task.log** - 完整的日志记录(详细格式) 75 | 76 | 日志包含: 77 | - ⏰ 时间戳 78 | - 📊 日志级别(INFO/WARNING/ERROR) 79 | - 📋 详细的操作信息 80 | - 🎨 彩色emoji标识,易于阅读 81 | 82 | ``` 83 | 84 | ## ⚠️ 注意事项 85 | 86 | 1. **Cookie安全** - 请妥善保管您的Cookie,不要泄露给他人 87 | 2. **频率控制** - 脚本已内置任务间隔,避免请求过于频繁 88 | 3. **账号切换** - 多账号之间会自动等待间隔 89 | 4. **错误处理** - 遇到错误会自动跳过并继续执行 90 | 91 | ## 📊 任务状态说明 92 | 93 | - ⚪ 未开始 (0) - 任务尚未开始 94 | - 🔵 进行中 (1) - 任务正在进行 95 | - 🟡 未完成 (2) - 任务未完成 96 | - 🟢 已完成 (3) - 任务已完成,等待领奖 97 | - ✅ 已领取 (4) - 任务已完成并已领取奖励 98 | 99 | ## 🎯 执行流程 100 | 101 | ``` 102 | 启动脚本 103 | ↓ 104 | 加载配置 105 | ↓ 106 | 循环处理每个账号 107 | ├─ 获取用户信息 108 | ├─ 获取任务列表 109 | ├─ 执行未完成任务 110 | ├─ 领取任务奖励 111 | └─ 输出统计信息 112 | ↓ 113 | 完成所有账号处理 114 | ``` 115 | 116 | ## 📈 日志示例 117 | 118 | ``` 119 | 2025-10-20 10:00:00 | INFO | 🎉 什么值得买任务自动化脚本启动 120 | 2025-10-20 10:00:00 | INFO | ⏰ 启动时间: 2025-10-20 10:00:00 121 | 2025-10-20 10:00:01 | INFO | ✅ 成功加载配置文件,共 1 个账号 122 | 2025-10-20 10:00:01 | INFO | 🚀 开始处理账号: 测试账号 123 | 2025-10-20 10:00:02 | INFO | 📊 任务列表 (共 5 个任务): 124 | 2025-10-20 10:00:02 | INFO | 📋 浏览文章: 🔵 进行中 (0/1) 125 | 2025-10-20 10:00:03 | INFO | ✅ 任务 [浏览文章] 执行成功 126 | ``` 127 | 128 | ## 🤝 贡献 129 | 130 | 欢迎提交Issue和Pull Request! 131 | 132 | ## 📄 许可证 133 | 134 | MIT License 135 | 136 | ## 🙏 致谢 137 | 138 | 感谢什么值得买平台提供的优质服务。 139 | 140 | --- 141 | 142 | **免责声明**: 本项目仅供学习交流使用,请勿用于非法用途。使用本脚本产生的一切后果由使用者自行承担。 143 | 144 | -------------------------------------------------------------------------------- /script/huaruntong/ole/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ole 签到接口 3 | """ 4 | import requests 5 | 6 | 7 | class OleAPI: 8 | """Ole 签到相关 API""" 9 | 10 | def __init__(self, session_id: str, device_name: str, unique: str, 11 | ole_wx_open_id: str, shop_code: str = "205368", 12 | city_id: str = "c_region_11122", user_agent: str = None): 13 | """ 14 | 初始化 API 15 | 16 | :param session_id: 会话 ID 17 | :param device_name: 设备名称(手机号) 18 | :param unique: 唯一标识 19 | :param ole_wx_open_id: 微信 openid 20 | :param shop_code: 门店编码 21 | :param city_id: 城市 ID 22 | :param user_agent: 用户代理字符串 23 | """ 24 | self.base_url = "https://ole-app.crvole.com.cn/vgdt_app_api/v1" 25 | self.session_id = session_id 26 | self.device_name = device_name 27 | self.unique = unique 28 | self.ole_wx_open_id = ole_wx_open_id 29 | self.shop_code = shop_code 30 | self.city_id = city_id 31 | self.user_agent = user_agent or 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf26405f0) XWEB/13910' 32 | 33 | def _get_headers(self): 34 | """获取请求头""" 35 | return { 36 | 'User-Agent': self.user_agent, 37 | 'Content-Type': 'application/json', 38 | 'Tenant-Channel': 'OLE', 39 | 'cityId': self.city_id, 40 | 'oleWxOpenId': self.ole_wx_open_id, 41 | 'Tenant': 'VGDT', 42 | 'sessionId': self.session_id, 43 | 'Device-Name': self.device_name, 44 | 'unique': self.unique, 45 | 'channel': 'wxmini', 46 | 'shopCode': self.shop_code, 47 | 'os': 'ios', 48 | 'appVersion': '1.10.17', 49 | 'Referer': 'https://servicewechat.com/wx6c61aaeba1551439/93/page-frame.html', 50 | 'Accept-Language': 'zh-CN,zh;q=0.9' 51 | } 52 | 53 | def sign_in(self, enter_shop_code: str = None): 54 | """ 55 | 签到接口 56 | 57 | :param enter_shop_code: 进入的门店编码,默认使用初始化时的 shop_code 58 | :return: 响应数据 59 | """ 60 | if enter_shop_code is None: 61 | enter_shop_code = self.shop_code 62 | 63 | url = f"{self.base_url}/vgdt-fea-app-member/front_api/member_sign" 64 | payload = { 65 | "enter_shop_code": enter_shop_code 66 | } 67 | 68 | try: 69 | response = requests.post(url, json=payload, headers=self._get_headers()) 70 | response.raise_for_status() 71 | return response.json() 72 | except requests.exceptions.RequestException as e: 73 | return { 74 | "success": False, 75 | "error": str(e) 76 | } 77 | 78 | def query_points(self): 79 | """ 80 | 查询积分(如果有相关接口可以添加) 81 | 注:当前 curl 中未提供查询接口,这里预留接口 82 | 83 | :return: 响应数据 84 | """ 85 | # 这里可以根据实际需求添加查询接口 86 | pass 87 | 88 | -------------------------------------------------------------------------------- /script/huaruntong/999/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 答题接口封装 3 | """ 4 | import requests 5 | 6 | 7 | class QuizAPI: 8 | """答题相关 API""" 9 | 10 | def __init__(self, token: str, mobile: str, user_agent: str = None): 11 | """ 12 | 初始化 API 13 | 14 | :param token: 认证 token 15 | :param mobile: 手机号 16 | :param user_agent: 用户代理字符串 17 | """ 18 | self.base_url = "https://api4.jiankangyouyi.com/base-data/v1/api/gadgets" 19 | self.token = token 20 | self.mobile = mobile 21 | self.user_agent = user_agent or 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.7(0x13080712) UnifiedPCMacWechat(0xf26405f0) XWEB/13910' 22 | 23 | def _get_headers(self): 24 | """获取请求头""" 25 | return { 26 | 'User-Agent': self.user_agent, 27 | 'Accept': 'application/json, text/plain, */*', 28 | 'Content-Type': 'application/json', 29 | 'customdata': f'{{"mobile":"{self.mobile}","point":5,"entrance":"huarun-sj-mryt"}}', 30 | 'token': self.token, 31 | 'origin': 'https://apps.jiankangyouyi.com', 32 | 'referer': 'https://apps.jiankangyouyi.com/', 33 | 'accept-language': 'zh-CN,zh;q=0.9' 34 | } 35 | 36 | def get_question(self): 37 | """ 38 | 获取题目信息 39 | 40 | :return: 题目数据 41 | """ 42 | url = f"{self.base_url}/business-knowledge-challenges?bizType=160107" 43 | payload = { 44 | "userId": self.mobile, 45 | "strategy": "1", 46 | "customerParams": ["huarun-sanjiu-prointsRewardScore"], 47 | "customers": ["050205"], 48 | "configId": "" 49 | } 50 | 51 | try: 52 | response = requests.post(url, json=payload, headers=self._get_headers()) 53 | response.raise_for_status() 54 | return response.json() 55 | except requests.exceptions.RequestException as e: 56 | return { 57 | "resultCode": "-1", 58 | "message": f"请求失败: {str(e)}" 59 | } 60 | 61 | def submit_answer(self, question_id: str, option_codes: list): 62 | """ 63 | 提交答案 64 | 65 | :param question_id: 题目 ID 66 | :param option_codes: 选项代码列表 67 | :return: 提交结果 68 | """ 69 | url = f"{self.base_url}/knowledge-challenges/user-choice?bizType=160107" 70 | payload = { 71 | "userId": self.mobile, 72 | "questionId": question_id, 73 | "userOptionCodes": option_codes, 74 | "customerParams": ["huarun-sanjiu-prointsRewardScore"], 75 | "mobile": self.mobile, 76 | "configId": "" 77 | } 78 | 79 | try: 80 | response = requests.post(url, json=payload, headers=self._get_headers()) 81 | response.raise_for_status() 82 | return response.json() 83 | except requests.exceptions.RequestException as e: 84 | return { 85 | "resultCode": "-1", 86 | "message": f"提交失败: {str(e)}" 87 | } 88 | 89 | -------------------------------------------------------------------------------- /script/smzdm/api/sign_calculator.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import re 3 | from typing import Dict, Any, Union 4 | from urllib.parse import urlparse, parse_qs 5 | 6 | # 公共变量:用于 sign 计算的固定 key 7 | SECRET_KEY = "zok5JtAq3$QixaA%mncn*jGWlEpSL3E1" 8 | 9 | 10 | def calculate_sign(data: Dict[str, Any]) -> str: 11 | """ 12 | 计算 sign 签名(适用于 POST 请求的 data 参数) 13 | 14 | Args: 15 | data: 包含请求参数的字典 16 | 17 | Returns: 18 | 计算出的 MD5 签名(大写) 19 | """ 20 | return _generate_sign_from_dict(data) 21 | 22 | 23 | def calculate_sign_from_url(url: str) -> str: 24 | """ 25 | 从 GET 请求的 URL 中提取参数并计算 sign 签名 26 | 27 | Args: 28 | url: 包含查询参数的完整 URL 29 | 30 | Returns: 31 | 计算出的 MD5 签名(大写) 32 | """ 33 | # 解析 URL 获取查询参数 34 | parsed_url = urlparse(url) 35 | query_params = parse_qs(parsed_url.query) 36 | 37 | # 将查询参数转换为字典格式(取第一个值,因为 parse_qs 返回列表) 38 | params_dict = {} 39 | for key, values in query_params.items(): 40 | if values: # 确保值不为空 41 | params_dict[key] = values[0] 42 | 43 | return _generate_sign_from_dict(params_dict) 44 | 45 | 46 | def calculate_sign_from_params(params: Union[Dict[str, Any], str]) -> str: 47 | """ 48 | 通用的 sign 计算函数,支持字典参数或 URL 字符串 49 | 50 | Args: 51 | params: 可以是字典格式的参数,或者包含查询参数的 URL 字符串 52 | 53 | Returns: 54 | 计算出的 MD5 签名(大写) 55 | """ 56 | if isinstance(params, str): 57 | # 如果是字符串,判断是否为完整 URL 还是查询字符串 58 | if params.startswith('http'): 59 | return calculate_sign_from_url(params) 60 | else: 61 | # 处理纯查询字符串,如 "a=1&b=2" 62 | query_params = parse_qs(params) 63 | params_dict = {} 64 | for key, values in query_params.items(): 65 | if values: 66 | params_dict[key] = values[0] 67 | return _generate_sign_from_dict(params_dict) 68 | elif isinstance(params, dict): 69 | return calculate_sign(params) 70 | else: 71 | raise ValueError("params 必须是字典或字符串类型") 72 | 73 | 74 | def _generate_sign_from_dict(data: Dict[str, Any]) -> str: 75 | """ 76 | 从字典参数生成 sign 签名的内部函数 77 | 78 | Args: 79 | data: 包含请求参数的字典 80 | 81 | Returns: 82 | 计算出的 MD5 签名(大写) 83 | """ 84 | # 1. 获取所有 key 并按字母顺序排序 85 | sorted_keys = sorted(data.keys()) 86 | 87 | # 2. 构建 key=value 对,并用 & 连接(跳过空值) 88 | params = [] 89 | for key in sorted_keys: 90 | value = data[key] 91 | # 跳过空值(None、空字符串、空列表等) 92 | if value is not None and value != "" and value != []: 93 | # 转换为字符串并去除空格和换行符 94 | value_str = re.sub(r'[^\S\r\n]+', '', str(value)) 95 | # value_str = str(value).strip().replace('\n', '').replace('\r', '').replace(' ', '') 96 | # 再次检查处理后的值是否为空 97 | if value_str: 98 | params.append(f"{key}={value_str}") 99 | 100 | # 3. 用 & 连接所有参数 101 | query_string = "&".join(params) 102 | # print(params) 103 | 104 | # 4. 在最后拼接固定的 key 105 | query_string += f"&key={SECRET_KEY}" 106 | # print(query_string) 107 | # 5. 计算 MD5 108 | md5_hash = hashlib.md5(query_string.encode('utf-8')).hexdigest() 109 | 110 | # 6. 返回大写的 MD5 111 | return md5_hash.upper() 112 | 113 | -------------------------------------------------------------------------------- /script/kanxue/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 看雪论坛API模块 5 | 6 | 提供看雪论坛签到相关的API接口 7 | """ 8 | 9 | import requests 10 | import json 11 | import logging 12 | from typing import Dict, Optional 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class KanxueAPI: 18 | """看雪论坛API类""" 19 | 20 | def __init__(self, cookie: str, csrf_token: str, user_agent: Optional[str] = None): 21 | """ 22 | 初始化API类 23 | 24 | Args: 25 | cookie: 用户的Cookie字符串 26 | csrf_token: CSRF token,用于验证请求 27 | user_agent: 用户代理字符串,可选 28 | """ 29 | self.cookie = cookie 30 | self.csrf_token = csrf_token 31 | self.user_agent = user_agent or ( 32 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' 33 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 34 | 'Chrome/141.0.0.0 Safari/537.36' 35 | ) 36 | self.sign_url = 'https://bbs.kanxue.com/user-signin.htm' 37 | self.base_url = 'https://bbs.kanxue.com' 38 | 39 | def get_headers(self) -> Dict[str, str]: 40 | """ 41 | 获取请求头 42 | 43 | Returns: 44 | Dict[str, str]: 请求头字典 45 | """ 46 | return { 47 | 'User-Agent': self.user_agent, 48 | 'Accept': 'text/plain, */*; q=0.01', 49 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 50 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 51 | 'sec-ch-ua-platform': '"macOS"', 52 | 'X-Requested-With': 'XMLHttpRequest', 53 | 'sec-ch-ua': '"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"', 54 | 'sec-ch-ua-mobile': '?0', 55 | 'Sec-GPC': '1', 56 | 'Accept-Language': 'zh-CN,zh;q=0.6', 57 | 'Origin': 'https://bbs.kanxue.com', 58 | 'Sec-Fetch-Site': 'same-origin', 59 | 'Sec-Fetch-Mode': 'cors', 60 | 'Sec-Fetch-Dest': 'empty', 61 | 'Referer': 'https://bbs.kanxue.com/', 62 | 'Cookie': self.cookie 63 | } 64 | 65 | def sign_in(self) -> Dict: 66 | """ 67 | 执行签到 68 | 69 | Returns: 70 | Dict: 签到结果 71 | { 72 | 'success': bool, # 是否成功 73 | 'result': dict, # 成功时的结果数据 74 | 'error': str # 失败时的错误信息 75 | } 76 | """ 77 | logger.info("开始执行看雪论坛签到...") 78 | headers = self.get_headers() 79 | data = { 80 | 'csrf_token': self.csrf_token 81 | } 82 | 83 | try: 84 | response = requests.post( 85 | self.sign_url, 86 | headers=headers, 87 | data=data, 88 | timeout=30 89 | ) 90 | 91 | # 检查响应状态 92 | response.raise_for_status() 93 | 94 | # 尝试解析JSON响应 95 | try: 96 | result = response.json() 97 | except json.JSONDecodeError: 98 | result = { 99 | 'status': 'success', 100 | 'message': response.text 101 | } 102 | 103 | logger.info(f"看雪论坛签到成功: {result}") 104 | return { 105 | 'success': True, 106 | 'result': result 107 | } 108 | 109 | except requests.RequestException as e: 110 | error_msg = f"签到失败: {str(e)}" 111 | logger.error(error_msg) 112 | return { 113 | 'success': False, 114 | 'error': error_msg 115 | } 116 | 117 | def get_user_info(self) -> Dict: 118 | """ 119 | 获取用户信息(可选功能) 120 | 121 | Returns: 122 | Dict: 用户信息 123 | """ 124 | return {} 125 | 126 | -------------------------------------------------------------------------------- /script/enshan/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 恩山论坛API模块 5 | 6 | 提供恩山论坛签到相关的API接口 7 | """ 8 | 9 | import requests 10 | import json 11 | import logging 12 | from typing import Dict, Optional 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class EnshanAPI: 18 | """恩山论坛API类""" 19 | 20 | def __init__(self, cookies: str, formhash: str, user_agent: Optional[str] = None): 21 | """ 22 | 初始化API类 23 | 24 | Args: 25 | cookies: 用户的Cookie字符串 26 | formhash: 表单hash值,用于验证请求 27 | user_agent: 用户代理字符串,可选 28 | """ 29 | self.cookies = cookies 30 | self.formhash = formhash 31 | self.user_agent = user_agent or ( 32 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' 33 | 'AppleWebKit/537.36 (KHTML, like Gecko) ' 34 | 'Chrome/141.0.0.0 Safari/537.36' 35 | ) 36 | self.sign_url = 'https://www.right.com.cn/forum/plugin.php?id=erling_qd:action&action=sign' 37 | self.base_url = 'https://www.right.com.cn/forum' 38 | 39 | def get_headers(self) -> Dict[str, str]: 40 | """ 41 | 获取请求头 42 | 43 | Returns: 44 | Dict[str, str]: 请求头字典 45 | """ 46 | return { 47 | 'User-Agent': self.user_agent, 48 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 49 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 50 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 51 | 'sec-ch-ua-platform': '"macOS"', 52 | 'X-Requested-With': 'XMLHttpRequest', 53 | 'sec-ch-ua': '"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"', 54 | 'sec-ch-ua-mobile': '?0', 55 | 'Sec-GPC': '1', 56 | 'Accept-Language': 'zh-CN,zh;q=0.9', 57 | 'Origin': 'https://www.right.com.cn', 58 | 'Sec-Fetch-Site': 'same-origin', 59 | 'Sec-Fetch-Mode': 'cors', 60 | 'Sec-Fetch-Dest': 'empty', 61 | 'Referer': 'https://www.right.com.cn/forum/erling_qd-sign_in.html', 62 | 'Cookie': self.cookies 63 | } 64 | 65 | def sign_in(self) -> Dict: 66 | """ 67 | 执行签到 68 | 69 | Returns: 70 | Dict: 签到结果 71 | { 72 | 'success': bool, # 是否成功 73 | 'result': dict, # 成功时的结果数据 74 | 'error': str # 失败时的错误信息 75 | } 76 | """ 77 | logger.info("开始执行恩山论坛签到...") 78 | headers = self.get_headers() 79 | data = { 80 | 'formhash': self.formhash 81 | } 82 | 83 | try: 84 | response = requests.post( 85 | self.sign_url, 86 | headers=headers, 87 | data=data, 88 | timeout=30 89 | ) 90 | 91 | # 检查响应状态 92 | response.raise_for_status() 93 | 94 | # 尝试解析JSON响应 95 | try: 96 | result = response.json() 97 | except json.JSONDecodeError: 98 | result = { 99 | 'status': 'success', 100 | 'message': response.text 101 | } 102 | 103 | logger.info(f"恩山论坛签到成功: {result}") 104 | return { 105 | 'success': True, 106 | 'result': result 107 | } 108 | 109 | except requests.RequestException as e: 110 | error_msg = f"签到失败: {str(e)}" 111 | logger.error(error_msg) 112 | return { 113 | 'success': False, 114 | 'error': error_msg 115 | } 116 | 117 | def get_user_info(self) -> Dict: 118 | """ 119 | 获取用户信息(可选功能) 120 | 121 | Returns: 122 | Dict: 用户信息 123 | """ 124 | # 这里可以添加获取用户信息的逻辑 125 | # 目前返回空字典 126 | return {} 127 | 128 | -------------------------------------------------------------------------------- /script/erke/README.md: -------------------------------------------------------------------------------- 1 | # 鸿星尔克小程序签到脚本 2 | 3 | 鸿星尔克小程序的自动签到脚本,支持查询积分明细和自动签到功能。 4 | 5 | ## 功能特性 6 | 7 | - ✅ 查询积分明细 8 | - ✅ 自动签到 9 | - ✅ 多账号支持 10 | - ✅ 推送通知 11 | - ✅ 详细日志输出 12 | 13 | ## 文件说明 14 | 15 | ``` 16 | erke/ 17 | ├── __init__.py # 模块初始化文件 18 | ├── api.py # API接口文件(包含签名计算) 19 | ├── main.py # 主程序文件 20 | └── README.md # 说明文档 21 | ``` 22 | 23 | ## 配置说明 24 | 25 | 编辑项目根目录下的 `/config/token.json` 文件,在 `erke` 节点下添加账号信息: 26 | 27 | ```json 28 | { 29 | "erke": { 30 | "accounts": [ 31 | { 32 | "account_name": "账号1", 33 | "member_id": "你的会员ID", 34 | "enterprise_id": "你的企业ID", 35 | "unionid": "你的unionid", 36 | "openid": "你的openid", 37 | "wx_openid": "你的微信openid", 38 | "user_agent": "自定义UA(可选)" 39 | } 40 | ] 41 | } 42 | } 43 | ``` 44 | 45 | ### 参数说明 46 | 随便找到一个post请求,查看data请求数据即可 47 | 48 | | 参数 | 说明 | 必填 | 示例 | 49 | |------|------|------|------| 50 | | account_name | 账号备注名 | 是 | 账号1 | 51 | | member_id | 会员ID | 是 | 8a80a18b9ac5a7cb019ac87bda88530f | 52 | | enterprise_id | 企业ID | 是 | ff8080817d9fbda8017dc20674f47fb6 | 53 | | unionid | 微信unionid | 是 | o36lGwKt-pV1vjXvzb0eWFV-Mfn4 | 54 | | openid | 小程序openid | 是 | oXpsg5YUJ_MJzH3orkR8zKEF2udw | 55 | | wx_openid | 微信openid | 是 | oUIO7jktQKedebu0grkjVdpyLvTI | 56 | | user_agent | 用户代理 | 否 | 自定义UA | 57 | 58 | ## 使用方法 59 | 60 | ### 前置条件 61 | 62 | 确保已在项目根目录的 `/config/token.json` 中配置好账号信息。 63 | 64 | ### 直接运行 65 | 66 | ```bash 67 | cd /Users/cat/Projects/python/PrivateProjects/ZaiZaiCat-Checkin/script/erke 68 | python3 main.py 69 | ``` 70 | 71 | ### 定时任务 72 | 73 | 可以使用 crontab 设置定时任务: 74 | 75 | ```bash 76 | # 每天早上8点执行 77 | 0 8 * * * cd /Users/cat/Projects/python/PrivateProjects/ZaiZaiCat-Checkin/script/erke && python3 main.py 78 | ``` 79 | 80 | ## 执行流程 81 | 82 | 1. 读取配置文件中的账号信息 83 | 2. 遍历每个账号: 84 | - 查询积分明细 85 | - 执行签到操作 86 | 3. 输出执行结果统计 87 | 4. 发送推送通知 88 | 89 | ## 输出示例 90 | 91 | ``` 92 | ================================================== 93 | 开始处理账号: 账号1 94 | ================================================== 95 | [账号1] 查询积分明细... 96 | [账号1] 积分明细查询成功 97 | [账号1] 当前积分: 100 98 | [账号1] 执行签到... 99 | [账号1] 签到成功 100 | [账号1] 签到信息: 签到成功,获得10积分 101 | 102 | ============================================================ 103 | 执行结果统计 104 | ============================================================ 105 | 总账号数: 1 106 | 成功: 1 107 | 失败: 0 108 | ``` 109 | 110 | ## API 说明 111 | 112 | ### ErkeAPI 类 113 | 114 | #### 初始化参数 115 | 116 | ```python 117 | api = ErkeAPI( 118 | member_id="会员ID", 119 | enterprise_id="企业ID", 120 | unionid="unionid", 121 | openid="openid", 122 | wx_openid="微信openid", 123 | appid="wxa1f1fa3785a47c7d", # 可选,默认值 124 | user_agent="用户代理" # 可选 125 | ) 126 | ``` 127 | 128 | #### 方法 129 | 130 | ##### get_integral_record() 131 | 132 | 获取积分明细 133 | 134 | ```python 135 | result = api.get_integral_record( 136 | current_page=1, # 当前页码 137 | page_size=20 # 每页大小 138 | ) 139 | ``` 140 | 141 | 返回格式: 142 | ```python 143 | { 144 | 'success': bool, # 是否成功 145 | 'result': dict, # 成功时的结果数据 146 | 'error': str # 失败时的错误信息 147 | } 148 | ``` 149 | 150 | ##### member_sign() 151 | 152 | 会员签到 153 | 154 | ```python 155 | result = api.member_sign() 156 | ``` 157 | 158 | 返回格式: 159 | ```python 160 | { 161 | 'success': bool, # 是否成功 162 | 'result': dict, # 成功时的结果数据 163 | 'error': str # 失败时的错误信息 164 | } 165 | ``` 166 | 167 | ## 注意事项 168 | 169 | 1. 请确保配置文件中的信息准确无误 170 | 2. 建议不要频繁调用接口,避免被限制 171 | 3. 签名算法已内置在 api.py 中,无需单独配置 172 | 4. 支持多账号,可在配置文件中添加多个账号 173 | 174 | ## 常见问题 175 | 176 | ### Q: 如何获取账号信息? 177 | A: 可以通过抓包小程序请求获取相关参数。 178 | 179 | ### Q: 签到失败怎么办? 180 | A: 检查 `/config/token.json` 中 erke 节点下的参数是否正确,查看日志中的错误信息。 181 | 182 | ### Q: 可以添加更多功能吗? 183 | A: 可以,在 api.py 中添加新的接口方法即可。 184 | 185 | ### Q: 配置文件在哪里? 186 | A: 统一使用项目根目录下的 `/config/token.json` 文件,在 `erke` 节点下配置账号信息。 187 | 188 | ## 更新日志 189 | 190 | ### v1.0.0 (2025-11-28) 191 | - 初始版本 192 | - 支持积分明细查询 193 | - 支持自动签到 194 | - 支持多账号 195 | - 支持推送通知 196 | 197 | -------------------------------------------------------------------------------- /config/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "sf": { 3 | "accounts": [ 4 | { 5 | "account_name": "大号", 6 | "cookies": "COOKIE_MASKED", 7 | "user_id": "MASKED", 8 | "user_agent": "UA_MASKED", 9 | "channel": "weixin", 10 | "device_id": "DEVICE_MASKED" 11 | }, 12 | { 13 | "account_name": "小号", 14 | "cookies": "COOKIE_MASKED", 15 | "user_id": "MASKED", 16 | "user_agent": "UA_MASKED", 17 | "channel": "weixin", 18 | "device_id": "DEVICE_MASKED" 19 | } 20 | ] 21 | }, 22 | "shyp": { 23 | "accounts": [ 24 | { 25 | "account_name": "账号1", 26 | "token": "TOKEN_MASKED", 27 | "device_id": "DEVICE_MASKED", 28 | "site_id": "MASKED", 29 | "user_agent": "UA_MASKED" 30 | } 31 | ] 32 | }, 33 | "enshan": { 34 | "accounts": [ 35 | { 36 | "account_name": "默认账号", 37 | "cookies": "COOKIE_MASKED", 38 | "formhash": "MASKED", 39 | "user_agent": "UA_MASKED" 40 | } 41 | ] 42 | }, 43 | "kanxue": { 44 | "kanxue": { 45 | "accounts": [ 46 | { 47 | "account_name": "默认账号", 48 | "cookie": "COOKIE_MASKED", 49 | "csrf_token": "MASKED", 50 | "user_agent": "UA_MASKED" 51 | } 52 | ] 53 | } 54 | }, 55 | "huaruntong": { 56 | "999": { 57 | "accounts": [ 58 | { 59 | "account_name": "大号", 60 | "token": "TOKEN_MASKED", 61 | "mobile": "MOBILE_MASKED", 62 | "user_agent": "UA_MASKED" 63 | }, 64 | { 65 | "account_name": "小号", 66 | "token": "TOKEN_MASKED", 67 | "mobile": "MOBILE_MASKED", 68 | "user_agent": "UA_MASKED" 69 | } 70 | ] 71 | }, 72 | "huaruntong_wx": { 73 | "accounts": [ 74 | { 75 | "account_name": "大号", 76 | "answerResult": 1, 77 | "channelId": "APP", 78 | "merchantCode": "MASKED", 79 | "storeCode": "MASKED", 80 | "sysId": "MASKED", 81 | "transactionUuid": "MASKED", 82 | "inviteCode": "MASKED", 83 | "token": "TOKEN_MASKED", 84 | "user_agent": "UA_MASKED" 85 | }, 86 | { 87 | "account_name": "小号", 88 | "answerResult": 1, 89 | "channelId": "APP", 90 | "merchantCode": "MASKED", 91 | "storeCode": "MASKED", 92 | "sysId": "MASKED", 93 | "transactionUuid": "MASKED", 94 | "inviteCode": "MASKED", 95 | "token": "TOKEN_MASKED", 96 | "user_agent": "UA_MASKED" 97 | } 98 | ] 99 | }, 100 | "ole": { 101 | "accounts": [ 102 | { 103 | "account_name": "大号", 104 | "session_id": "SESSION_MASKED", 105 | "device_name": "MOBILE_MASKED", 106 | "unique": "MASKED", 107 | "ole_wx_open_id": "MASKED", 108 | "shop_code": "MASKED", 109 | "city_id": "MASKED", 110 | "user_agent": "UA_MASKED" 111 | }, 112 | { 113 | "account_name": "小号", 114 | "session_id": "SESSION_MASKED", 115 | "device_name": "MOBILE_MASKED", 116 | "unique": "MASKED", 117 | "ole_wx_open_id": "MASKED", 118 | "shop_code": "MASKED", 119 | "city_id": "MASKED", 120 | "user_agent": "UA_MASKED" 121 | } 122 | ] 123 | }, 124 | "wentiweilaihui": { 125 | "accounts": [ 126 | { 127 | "account_name": "大号", 128 | "token": "TOKEN_MASKED", 129 | "mobile": "MOBILE_MASKED", 130 | "user_agent": "UA_MASKED" 131 | }, 132 | { 133 | "account_name": "小号", 134 | "token": "TOKEN_MASKED", 135 | "mobile": "MOBILE_MASKED", 136 | "user_agent": "UA_MASKED" 137 | } 138 | ] 139 | } 140 | }, 141 | "smzdm": { 142 | "accounts": [ 143 | { 144 | "name": "111", 145 | "cookie": "COOKIE_MASKED", 146 | "user_agent": "UA_MASKED", 147 | "setting": "MASKED" 148 | } 149 | ] 150 | }, 151 | "erke": { 152 | "accounts": [ 153 | { 154 | "account_name": "", 155 | "member_id": "", 156 | "enterprise_id": "", 157 | "unionid": "", 158 | "openid": "", 159 | "wx_openid": "", 160 | "user_agent": "" 161 | }, 162 | { 163 | "account_name": "", 164 | "member_id": "", 165 | "enterprise_id": "", 166 | "unionid": "", 167 | "openid": "", 168 | "wx_openid": "", 169 | "user_agent": "" 170 | } 171 | ] 172 | }, 173 | "wps": { 174 | "accounts": [ 175 | { 176 | "account_name": "大号", 177 | "user_id": 123456789, 178 | "cookies": "COOKIE_MASKED", 179 | "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" 180 | } 181 | ] 182 | } 183 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | #poetry.toml 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 115 | #pdm.lock 116 | #pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # pixi 121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 122 | #pixi.lock 123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 124 | # in the .venv directory. It is recommended not to include this directory in version control. 125 | .pixi 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .envrc 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | # PyCharm 172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 174 | # and can be added to the global gitignore or merged into this file. For a more nuclear 175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 176 | #.idea/ 177 | 178 | # Abstra 179 | # Abstra is an AI-powered process automation framework. 180 | # Ignore directories containing user credentials, local state, and settings. 181 | # Learn more at https://abstra.io/docs 182 | .abstra/ 183 | 184 | # Visual Studio Code 185 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 186 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 187 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 188 | # you could uncomment the following to ignore the entire vscode folder 189 | # .vscode/ 190 | 191 | # Ruff stuff: 192 | .ruff_cache/ 193 | 194 | # PyPI configuration file 195 | .pypirc 196 | 197 | # Cursor 198 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 199 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 200 | # refer to https://docs.cursor.com/context/ignore-files 201 | .cursorignore 202 | .cursorindexingignore 203 | 204 | # Marimo 205 | marimo/_static/ 206 | marimo/_lsp/ 207 | __marimo__/ 208 | -------------------------------------------------------------------------------- /script/huaruntong/huaruntong_wx/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 华润通签到API接口 3 | """ 4 | import json 5 | import time 6 | import hmac 7 | import hashlib 8 | import base64 9 | import os 10 | import requests 11 | from urllib.parse import quote 12 | from Crypto.Cipher import AES 13 | from Crypto.PublicKey import RSA 14 | from Crypto.Cipher import PKCS1_OAEP 15 | 16 | 17 | class HuaRunTongAPI: 18 | """华润通签到API接口类""" 19 | 20 | # 配置常量 21 | SECRET = "c274fc67-19f9-47ba-bb84-585a2e3a1f6a" 22 | PUB_KEY = """-----BEGIN PUBLIC KEY----- 23 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDuAiqDmvn9Rf15o21qkDxN0rUf 24 | ZsX6rVBrtfgY6tamN2Yn+1D3eHZJuKNlucyqeBr6nmfN2srYAX+oyCXr5vWwFclj 25 | PuWh8aSASqyk7MfbAv5Q4VqYS7lsYUQRdw4plZG0NASDeBvHWi3lsHjGfNb7iUvg 26 | rk312EDfBHtRgDvB0QIDAQAB 27 | -----END PUBLIC KEY-----""" 28 | APP_ID = "API_AUTH_WEB" 29 | HOST = "https://mid.huaruntong.cn" 30 | 31 | def __init__(self, token: str, answer_result: int = 1, channel_id: str = "APP", 32 | merchant_code: str = "1641000001532", store_code: str = "qiandaosonjifen", 33 | sys_id: str = "T0000001", transaction_uuid: str = "", 34 | invite_code: str = "", user_agent: str = None): 35 | """ 36 | 初始化华润通API 37 | 38 | :param token: 认证token 39 | :param answer_result: 答题结果 40 | :param channel_id: 渠道ID 41 | :param merchant_code: 商户编码 42 | :param store_code: 门店编码 43 | :param sys_id: 系统ID 44 | :param transaction_uuid: 交易UUID 45 | :param invite_code: 邀请码 46 | :param user_agent: 用户代理字符串 47 | """ 48 | self.token = token 49 | self.answer_result = answer_result 50 | self.channel_id = channel_id 51 | self.merchant_code = merchant_code 52 | self.store_code = store_code 53 | self.sys_id = sys_id 54 | self.transaction_uuid = transaction_uuid 55 | self.invite_code = invite_code 56 | self.user_agent = user_agent or "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1" 57 | 58 | def _generate_aes_key(self): 59 | """生成16字节随机AES密钥""" 60 | return os.urandom(16) 61 | 62 | def _pad_pkcs7(self, data: bytes, block_size: int = 16) -> bytes: 63 | """PKCS7填充""" 64 | padding_len = block_size - (len(data) % block_size) 65 | return data + bytes([padding_len] * padding_len) 66 | 67 | def _crypto_data(self, params: dict, api_path: str) -> dict: 68 | """加密请求数据""" 69 | # 添加必要字段 70 | params["apiPath"] = quote(api_path, safe='') 71 | params["appId"] = self.APP_ID 72 | params["timestamp"] = int(time.time() * 1000) 73 | 74 | # 生成签名 (HMAC-MD5) 75 | parts = [] 76 | for key, value in params.items(): 77 | if value is not None: 78 | if isinstance(value, (dict, list)): 79 | value = json.dumps(value, separators=(',', ':')) 80 | parts.append(f"{key}={value}") 81 | 82 | sign_str = "&".join(sorted(parts)) 83 | signature = hmac.new( 84 | self.SECRET.encode('utf-8'), 85 | sign_str.encode('utf-8'), 86 | hashlib.md5 87 | ).hexdigest() 88 | params["signature"] = signature 89 | 90 | # AES-CBC 加密 (IV为空) 91 | aes_key = self._generate_aes_key() 92 | iv = b'\x00' * 16 93 | cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv) 94 | 95 | plaintext = json.dumps(params, separators=(',', ':')).encode('utf-8') 96 | padded = self._pad_pkcs7(plaintext) 97 | encrypted_data = cipher_aes.encrypt(padded) 98 | data_b64 = base64.b64encode(encrypted_data).decode('utf-8') 99 | 100 | # RSA-OAEP 加密 AES 密钥 101 | rsa_key = RSA.import_key(self.PUB_KEY) 102 | cipher_rsa = PKCS1_OAEP.new(rsa_key) 103 | encrypted_key = cipher_rsa.encrypt(aes_key) 104 | key_b64 = base64.b64encode(encrypted_key).decode('utf-8') 105 | 106 | return {"key": key_b64, "data": data_b64} 107 | 108 | def _get_headers(self): 109 | """获取请求头""" 110 | return { 111 | "User-Agent": self.user_agent, 112 | "Accept": "application/json, text/plain, */*", 113 | "Content-Type": "application/json;charset=UTF-8", 114 | "X-HRT-MID-NEWRISK": "newRisk", 115 | "X-Hrt-Mid-Appid": self.APP_ID, 116 | "Origin": "https://cloud.huaruntong.cn", 117 | "Referer": "https://cloud.huaruntong.cn/", 118 | } 119 | 120 | def _send_request(self, api_path: str, payload: dict) -> dict: 121 | """发送加密请求""" 122 | url = f"{self.HOST}{api_path}" 123 | encrypted = self._crypto_data(payload.copy(), api_path) 124 | 125 | try: 126 | resp = requests.post(url, json=encrypted, headers=self._get_headers()) 127 | resp.raise_for_status() 128 | return resp.json() 129 | except requests.exceptions.RequestException as e: 130 | return { 131 | "success": False, 132 | "error": str(e) 133 | } 134 | 135 | def sign_in(self) -> dict: 136 | """ 137 | 签到接口 138 | 139 | :return: 响应数据 140 | """ 141 | payload = { 142 | "answerResult": self.answer_result, 143 | "channelId": self.channel_id, 144 | "merchantCode": self.merchant_code, 145 | "storeCode": self.store_code, 146 | "sysId": self.sys_id, 147 | "transactionUuid": self.transaction_uuid, 148 | "inviteCode": self.invite_code, 149 | "token": self.token 150 | } 151 | 152 | api_path = "/api/points/saveQuestionSignin" 153 | return self._send_request(api_path, payload) 154 | 155 | -------------------------------------------------------------------------------- /script/wps/README.md: -------------------------------------------------------------------------------- 1 | # WPS自动签到脚本 2 | 3 | ## ⚠️ 重要提示 4 | 5 | **获取Cookie和User ID的正确方法:** 6 | 7 | 1. 🔑 **Cookie获取建议**:请抓取页面请求接口的Cookie,而不是浏览器地址栏的Cookie 8 | - 原因:接口请求的Cookie通常包含完整的认证信息,包括 `uid` 字段 9 | - 这样可以确保Cookie中包含 `user_id`,便于配置 10 | 11 | 2. 📝 **配置步骤**: 12 | - 先抓取签到接口的Cookie(包含 `uid=数字` 字段) 13 | - 从Cookie中提取 `uid` 的值作为 `user_id` 14 | - 将完整的Cookie和提取的 `user_id` 一起填写到配置文件中 15 | 16 | 3. ✅ **配置检查**: 17 | - 确保Cookie中包含 `uid` 字段 18 | - 确保配置文件中的 `user_id` 与Cookie中的 `uid` 值一致 19 | 20 | 21 | ## 功能说明 22 | 23 | 本脚本用于自动执行WPS的签到和抽奖任务,支持多账号管理和推送通知功能。 24 | 25 | ### 主要功能 26 | 27 | - ✅ 多账号管理 28 | - ✅ 自动获取RSA加密公钥 29 | - ✅ 自动生成加密参数 30 | - ✅ 自动签到操作 31 | - ✅ 自动抽奖功能(可自定义抽奖次数) 32 | - ✅ 账号间随机延迟(5-10秒) 33 | - ✅ 详细的签到奖励显示 34 | - ✅ 每次抽奖结果展示 35 | - ✅ 推送执行结果(支持Bark推送) 36 | 37 | ## 目录结构 38 | 39 | ``` 40 | wps/ 41 | ├── api.py # API接口和加密模块 42 | ├── main.py # 主程序入口 43 | └── README.md # 本文档 44 | ``` 45 | 46 | ## 配置说明 47 | 48 | ### 1. 账号配置 49 | 50 | 在项目根目录的 `config/token.json` 文件中配置WPS账号信息: 51 | 52 | ```json 53 | { 54 | "wps": { 55 | "accounts": [ 56 | { 57 | "account_name": "默认账号", 58 | "user_id": 123456789, 59 | "cookies": "YOUR_WPS_COOKIES_HERE", 60 | "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", 61 | "max_lottery_limit": 5 62 | }, 63 | { 64 | "account_name": "账号2", 65 | "user_id": 987654321, 66 | "cookies": "ANOTHER_WPS_COOKIES", 67 | "user_agent": "Mozilla/5.0 ..." 68 | } 69 | ] 70 | } 71 | } 72 | ``` 73 | 74 | **配置参数说明:** 75 | 76 | - `account_name`: 账号名称,用于日志和通知中识别账号 77 | - `user_id`: **【必需】** WPS用户ID,从Cookie中的 `uid` 字段获取 78 | - `cookies`: **【必需】** WPS登录Cookie 79 | - `user_agent`: 【可选】用户代理字符串,不填则使用默认值 80 | - `max_lottery_limit`: 【可选】最大抽奖次数限制,默认为2次。设置后将限制每次最多抽奖的次数,即使账户有更多抽奖机会 81 | 82 | **⚠️ 重要提示:** 83 | - `user_id` 是必需参数,如果配置中没有 `user_id`,该账号将被跳过,不执行签到 84 | - `user_id` 可以从Cookie中的 `uid` 字段获取 85 | - `max_lottery_limit` 用于控制抽奖次数,避免过度消耗抽奖机会。例如设置为5,则每次最多抽奖5次,即使账户有10次机会 86 | 87 | ### 2. 获取Cookies和User ID 88 | 89 | #### 方法一:从用户中心页面获取 user_id(推荐) 90 | 91 | **最简单的获取方式:** 92 | 93 | 1. 打开浏览器,访问 [WPS用户中心](https://personal-act.wps.cn/center_page/user_index) 94 | 2. 登录你的WPS账号 95 | ![img.png](img.png) 96 | 3. 会直接显示用户id 97 | 98 | 99 | **或者:** 100 | - 打开开发者工具 (F12) 101 | - 切换到 **Application (应用)** 标签 102 | - 左侧选择 **Cookies** → `https://personal-act.wps.cn` 103 | - 在右侧Cookie列表中找到 `uid`,其值就是你的 `user_id` 104 | 105 | #### 方法二:从接口请求获取(推荐用于获取完整Cookie) 106 | 107 | **获取完整Cookie和user_id:** 108 | 109 | 1. 打开浏览器,访问 [WPS签到页面](https://personal-act.wps.cn/) 110 | 2. 登录你的WPS账号 111 | 3. 打开浏览器开发者工具 (F12) 112 | 4. 切换到 **Network (网络)** 标签 113 | 5. 在页面上执行一次签到操作 114 | 6. 在网络请求列表中找到签到相关的接口请求(如 `sign_in`、`encrypt/key` 等) 115 | 7. 点击该请求,查看 **Request Headers(请求头)** 116 | 8. 复制 `Cookie` 字段的完整内容 117 | 118 | **提取 user_id:** 119 | 120 | 从复制的Cookie字符串中找到 `uid=数字` 部分,例如: 121 | 122 | ``` 123 | uid=655953169; wps_sid=V02S...; csrf=... 124 | ↑ 125 | 这就是 user_id 126 | ``` 127 | 128 | 在这个例子中,`user_id` 就是 `655953169` 129 | 130 | **为什么要从接口请求获取Cookie?** 131 | - ✅ 接口请求的Cookie包含完整的认证信息(包括 `uid` 字段) 132 | - ✅ 确保Cookie是有效且最新的 133 | - ✅ 避免Cookie不完整导致签到失败 134 | - ⚠️ 不要使用浏览器地址栏复制Cookie,可能缺少必要字段 135 | 136 | **配置示例:** 137 | 138 | 假设从接口获取的Cookie为: 139 | ``` 140 | uid=655953169; wps_sid=V02Syi5I...; csrf=SCXAn6r5... 141 | ``` 142 | 143 | 则在配置文件中填写: 144 | ```json 145 | { 146 | "account_name": "我的账号", 147 | "user_id": 655953169, // 从Cookie中的uid字段提取,或从用户中心页面获取 148 | "cookies": "uid=655953169; wps_sid=V02Syi5I...; csrf=SCXAn6r5...", // 完整Cookie 149 | "user_agent": "Mozilla/5.0 ..." 150 | } 151 | ``` 152 | 153 | **说明:** 154 | - `user_id`: 从Cookie中的uid字段提取(655953169),或从用户中心页面获取 155 | - `cookies`: 从接口请求中复制的完整Cookie字符串 156 | 157 | ### 3. 推送配置(可选) 158 | 159 | 如需启用推送通知,需要在 `config/notification.json` 中配置Bark推送参数,或在环境变量中配置: 160 | 161 | ```bash 162 | export BARK_PUSH="YOUR_BARK_KEY" # Bark推送密钥(必需) 163 | export BARK_SOUND="birdsong" # 推送声音(可选) 164 | export BARK_GROUP="WPS签到" # 推送分组(可选) 165 | export BARK_LEVEL="active" # 推送级别(可选) 166 | ``` 167 | 168 | **推送通知内容包括:** 169 | - 📊 总体统计(总账号数、成功数、失败数) 170 | - 📋 每个账号的详细结果 171 | - ✅ 签到状态和消息 172 | - 🎁 签到奖励(仅首次签到成功时显示) 173 | - 🎲 抽奖结果(显示每次抽奖获得的奖品) 174 | - 📊 账户信息(抽奖次数、积分、即将过期积分) 175 | 176 | ## 使用方法 177 | 178 | ### 直接运行 179 | 180 | ```bash 181 | cd /Users/cat/Projects/python/PrivateProjects/ZaiZaiCat-Checkin/script/wps 182 | python main.py 183 | ``` 184 | 185 | ### 定时任务 186 | 187 | 可以使用cron或青龙面板等工具设置定时执行: 188 | 189 | ```bash 190 | # 每天早上9点执行 191 | 0 9 * * * cd /path/to/ZaiZaiCat-Checkin/script/wps && python main.py 192 | ``` 193 | 194 | ## 依赖库 195 | 196 | ```bash 197 | pip install requests pycryptodome 198 | ``` 199 | 200 | ## 注意事项 201 | 202 | 1. **Cookie安全** 203 | - 请妥善保管你的Cookie信息 204 | - 不要分享给他人 205 | - 定期更新Cookie 206 | 207 | 2. **签到频率** 208 | - 建议每天签到一次 209 | - 避免频繁请求 210 | 211 | 3. **抽奖次数控制** 212 | - 可通过 `max_lottery_limit` 参数控制每次最多抽奖次数 213 | - 未设置时默认为2次 214 | - 避免过度消耗抽奖机会 215 | 216 | 4. **账号间延迟** 217 | - 多账号时自动在账号间添加5-10秒随机延迟 218 | - 提高安全性,避免被检测为异常操作 219 | 220 | 5. **错误处理** 221 | - 脚本会自动处理网络错误和签到失败 222 | - Token过期时会提示重新登录 223 | - 如持续失败,请检查Cookie是否过期 224 | 225 | 6. **公钥更新** 226 | - 公钥会定期从服务器获取 227 | - 无需手动更新 228 | 229 | ## 更新日志 230 | 231 | ### v1.1 (2025-12-18) 232 | - ✅ 添加自动抽奖功能 233 | - ✅ 支持自定义最大抽奖次数限制 234 | - ✅ 添加签到奖励详细显示功能 235 | - ✅ 添加每次抽奖结果展示 236 | - ✅ 优化推送通知内容 237 | - ✅ 改进错误处理机制 238 | 239 | ### v1.0 (2025-12-01) 240 | - ✅ 重构代码结构,分离API和主逻辑 241 | - ✅ 支持统一配置文件管理 242 | - ✅ 支持多账号管理 243 | - ✅ 自动获取RSA公钥 244 | - ✅ 集成推送通知功能 245 | - ✅ 完善日志输出 246 | - ✅ 符合开发规范 247 | 248 | ## 问题反馈 249 | 250 | 如遇到问题,请检查: 251 | 1. Cookie是否有效 252 | 2. User ID是否正确配置 253 | 3. 网络连接是否正常 254 | 4. 依赖库是否正确安装 255 | 5. 配置文件格式是否正确 256 | 6. max_lottery_limit参数是否合理设置 257 | 258 | ## 免责声明 259 | 260 | 本脚本仅供学习交流使用,请勿用于商业用途。使用本脚本产生的任何问题,由使用者自行承担。 261 | 262 | -------------------------------------------------------------------------------- /script/huaruntong/ole/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ole 签到主程序 3 | """ 4 | import json 5 | import os 6 | import sys 7 | from datetime import datetime 8 | from pathlib import Path 9 | from api import OleAPI 10 | 11 | # 添加项目根目录到Python路径以导入notification模块 12 | current_dir = Path(__file__).parent 13 | project_root = current_dir.parent.parent.parent 14 | sys.path.insert(0, str(project_root)) 15 | 16 | from notification import send_notification, NotificationSound 17 | 18 | 19 | def load_config(): 20 | """加载统一配置文件""" 21 | # 使用统一的 token.json 配置文件 22 | # 从当前文件位置向上三级到达项目根目录,然后进入 config 目录 23 | current_dir = os.path.dirname(os.path.abspath(__file__)) 24 | config_path = os.path.join(current_dir, '..', '..', '..', 'config', 'token.json') 25 | with open(config_path, 'r', encoding='utf-8') as f: 26 | return json.load(f) 27 | 28 | 29 | def process_account(account_config): 30 | """处理单个账号的签到""" 31 | account_name = account_config.get('account_name', '未知账号') 32 | 33 | # 初始化结果 34 | result_info = { 35 | 'account_name': account_name, 36 | 'device_name': account_config["device_name"], 37 | 'success': False, 38 | 'error': None, 39 | 'message': None 40 | } 41 | 42 | # 初始化 API 43 | api = OleAPI( 44 | session_id=account_config["session_id"], 45 | device_name=account_config["device_name"], 46 | unique=account_config["unique"], 47 | ole_wx_open_id=account_config["ole_wx_open_id"], 48 | shop_code=account_config["shop_code"], 49 | city_id=account_config["city_id"], 50 | user_agent=account_config.get("user_agent") 51 | ) 52 | 53 | print("=" * 50) 54 | print(f"账号: {account_name} ({account_config['device_name']})") 55 | print("=" * 50) 56 | 57 | # 执行签到 58 | print("\n开始签到...") 59 | result = api.sign_in() 60 | 61 | # 处理签到结果 62 | if "error" in result: 63 | error_msg = result['error'] 64 | print(f"❌ 签到失败: {error_msg}") 65 | result_info['error'] = error_msg 66 | else: 67 | print(f"✅ 签到成功!") 68 | print(f"响应数据: {result}") 69 | result_info['success'] = True 70 | result_info['message'] = "签到成功" 71 | 72 | print("\n" + "=" * 50) 73 | return result_info 74 | 75 | 76 | def send_notification_summary(all_results, start_time, end_time): 77 | """ 78 | 发送任务执行结果的推送通知 79 | 80 | Args: 81 | all_results: 所有账号的执行结果列表 82 | start_time: 任务开始时间 83 | end_time: 任务结束时间 84 | """ 85 | try: 86 | duration = (end_time - start_time).total_seconds() 87 | 88 | # 统计结果 89 | total_count = len(all_results) 90 | success_count = sum(1 for r in all_results if r.get('success')) 91 | failed_count = total_count - success_count 92 | 93 | # 构建通知标题 94 | if failed_count == 0: 95 | title = "Ole签到成功 ✅" 96 | sound = NotificationSound.BIRDSONG 97 | elif success_count == 0: 98 | title = "Ole签到失败 ❌" 99 | sound = NotificationSound.ALARM 100 | else: 101 | title = "Ole签到部分成功 ⚠️" 102 | sound = NotificationSound.BELL 103 | 104 | # 构建通知内容 105 | content_parts = [ 106 | "📊 执行统计:", 107 | f"✅ 成功: {success_count} 个账号", 108 | f"❌ 失败: {failed_count} 个账号", 109 | f"📈 总计: {total_count} 个账号", 110 | "", 111 | "📝 详情:" 112 | ] 113 | 114 | for result in all_results: 115 | account_name = result.get('account_name', '未知账号') 116 | if result.get('success'): 117 | content_parts.append(f" ✅ [{account_name}] 签到成功") 118 | else: 119 | error = result.get('error', '未知错误') 120 | if len(error) > 30: 121 | error = error[:30] + "..." 122 | content_parts.append(f" ❌ [{account_name}] {error}") 123 | 124 | content_parts.append("") 125 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 126 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 127 | 128 | content = "\n".join(content_parts) 129 | 130 | # 发送通知 131 | send_notification( 132 | title=title, 133 | content=content, 134 | sound=sound, 135 | group="Ole精品超市" 136 | ) 137 | print("✅ 推送通知发送成功") 138 | 139 | except Exception as e: 140 | print(f"❌ 推送通知失败: {str(e)}") 141 | 142 | 143 | def main(): 144 | """主函数""" 145 | # 记录开始时间 146 | start_time = datetime.now() 147 | print(f"\n{'='*60}") 148 | print(f"## Ole签到任务开始") 149 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 150 | print(f"{'='*60}\n") 151 | 152 | # 加载配置 153 | config = load_config() 154 | accounts = config.get('huaruntong', {}).get('ole', {}).get('accounts', []) 155 | 156 | if not accounts: 157 | print("❌ 配置文件中没有找到账号信息") 158 | return 159 | 160 | # 收集所有账号的结果 161 | all_results = [] 162 | 163 | # 遍历所有账号 164 | for account in accounts: 165 | if not account.get('session_id'): 166 | print(f"⚠️ 跳过账号 {account.get('account_name', '未知')}: session_id 为空") 167 | print("=" * 50) 168 | all_results.append({ 169 | 'account_name': account.get('account_name', '未知'), 170 | 'success': False, 171 | 'error': 'session_id为空' 172 | }) 173 | continue 174 | 175 | result = process_account(account) 176 | all_results.append(result) 177 | print("\n") 178 | 179 | # 记录结束时间 180 | end_time = datetime.now() 181 | duration = (end_time - start_time).total_seconds() 182 | 183 | print(f"\n{'='*60}") 184 | print(f"## Ole签到任务完成") 185 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 186 | print(f"## 执行耗时: {int(duration)} 秒") 187 | print(f"{'='*60}\n") 188 | 189 | # 发送推送通知 190 | send_notification_summary(all_results, start_time, end_time) 191 | 192 | 193 | if __name__ == "__main__": 194 | main() 195 | -------------------------------------------------------------------------------- /script/huaruntong/huaruntong_wx/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 华润通签到主程序 3 | """ 4 | import json 5 | import os 6 | import sys 7 | from datetime import datetime 8 | from pathlib import Path 9 | from api import HuaRunTongAPI 10 | 11 | # 添加项目根目录到Python路径以导入notification模块 12 | current_dir = Path(__file__).parent 13 | project_root = current_dir.parent.parent.parent 14 | sys.path.insert(0, str(project_root)) 15 | 16 | from notification import send_notification, NotificationSound 17 | 18 | 19 | def load_config(): 20 | """加载统一配置文件""" 21 | # 使用统一的 token.json 配置文件 22 | # 从当前文件位置向上三级到达项目根目录,然后进入 config 目录 23 | current_dir = os.path.dirname(os.path.abspath(__file__)) 24 | config_path = os.path.join(current_dir, '..', '..', '..', 'config', 'token.json') 25 | with open(config_path, 'r', encoding='utf-8') as f: 26 | return json.load(f) 27 | 28 | 29 | 30 | def process_account(account_config): 31 | """处理单个账号的签到""" 32 | account_name = account_config.get('account_name', '未知账号') 33 | 34 | # 初始化结果 35 | result_info = { 36 | 'account_name': account_name, 37 | 'success': False, 38 | 'error': None, 39 | 'message': None, 40 | 'response': None 41 | } 42 | 43 | print("=" * 50) 44 | print(f"账号: {account_name}") 45 | print("=" * 50) 46 | 47 | # 初始化API 48 | api = HuaRunTongAPI( 49 | token=account_config.get("token"), 50 | answer_result=account_config.get("answerResult", 1), 51 | channel_id=account_config.get("channelId", "APP"), 52 | merchant_code=account_config.get("merchantCode", "1641000001532"), 53 | store_code=account_config.get("storeCode", "qiandaosonjifen"), 54 | sys_id=account_config.get("sysId", "T0000001"), 55 | transaction_uuid=account_config.get("transactionUuid", ""), 56 | invite_code=account_config.get("inviteCode", ""), 57 | user_agent=account_config.get("user_agent") 58 | ) 59 | 60 | # 发送请求 61 | print("\n发送签到请求...") 62 | result = api.sign_in() 63 | 64 | # 解析结果 65 | if result.get('code') == "S0A00000": 66 | result_info['success'] = True 67 | result_info['message'] = result.get('message', '签到成功') 68 | result_info['response'] = result 69 | print("✅ 签到成功") 70 | else: 71 | result_info['error'] = result.get('msg', '签到失败') 72 | result_info['response'] = result.get('msg') 73 | print(f"❌ 签到失败: {result_info['error']}") 74 | 75 | print("响应:") 76 | print(json.dumps(result, indent=2, ensure_ascii=False)) 77 | print("\n" + "=" * 50) 78 | 79 | return result_info 80 | 81 | 82 | def send_notification_summary(all_results, start_time, end_time): 83 | """ 84 | 发送任务执行结果的推送通知 85 | 86 | Args: 87 | all_results: 所有账号的执行结果列表 88 | start_time: 任务开始时间 89 | end_time: 任务结束时间 90 | """ 91 | try: 92 | duration = (end_time - start_time).total_seconds() 93 | 94 | # 统计结果 95 | total_count = len(all_results) 96 | success_count = sum(1 for r in all_results if r.get('success')) 97 | failed_count = total_count - success_count 98 | 99 | # 构建通知标题 100 | if failed_count == 0: 101 | title = "华润通签到成功 ✅" 102 | sound = NotificationSound.BIRDSONG 103 | elif success_count == 0: 104 | title = "华润通签到失败 ❌" 105 | sound = NotificationSound.ALARM 106 | else: 107 | title = "华润通签到部分成功 ⚠️" 108 | sound = NotificationSound.BELL 109 | 110 | # 构建通知内容 111 | content_parts = [ 112 | "📊 执行统计:", 113 | f"✅ 成功: {success_count} 个账号", 114 | f"❌ 失败: {failed_count} 个账号", 115 | f"📈 总计: {total_count} 个账号", 116 | "", 117 | "📝 详情:" 118 | ] 119 | 120 | for result in all_results: 121 | account_name = result.get('account_name', '未知账号') 122 | if result.get('success'): 123 | message = result.get('message', '签到成功') 124 | content_parts.append(f" ✅ [{account_name}] {message}") 125 | else: 126 | error = result.get('error', '未知错误') 127 | if len(error) > 30: 128 | error = error[:30] + "..." 129 | content_parts.append(f" ❌ [{account_name}] {error}") 130 | 131 | content_parts.append("") 132 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 133 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 134 | 135 | content = "\n".join(content_parts) 136 | 137 | # 发送通知 138 | send_notification( 139 | title=title, 140 | content=content, 141 | sound=sound 142 | ) 143 | print("✅ 推送通知发送成功") 144 | 145 | except Exception as e: 146 | print(f"❌ 推送通知失败: {str(e)}") 147 | 148 | 149 | def main(): 150 | """主函数""" 151 | # 记录开始时间 152 | start_time = datetime.now() 153 | print(f"\n{'='*60}") 154 | print(f"## 华润通签到任务开始") 155 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 156 | print(f"{'='*60}\n") 157 | 158 | # 加载配置 159 | config = load_config() 160 | accounts = config.get('huaruntong', {}).get('huaruntong_wx', {}).get('accounts', []) 161 | 162 | if not accounts: 163 | print("❌ 配置文件中没有找到账号信息") 164 | return 165 | 166 | # 收集所有账号的结果 167 | all_results = [] 168 | 169 | # 遍历所有账号 170 | for account in accounts: 171 | if not account.get('token'): 172 | print(f"⚠️ 跳过账号 {account.get('account_name', '未知')}: token 为空") 173 | print("=" * 50) 174 | all_results.append({ 175 | 'account_name': account.get('account_name', '未知'), 176 | 'success': False, 177 | 'error': 'token为空' 178 | }) 179 | continue 180 | 181 | result = process_account(account) 182 | all_results.append(result) 183 | print("\n") 184 | 185 | # 记录结束时间 186 | end_time = datetime.now() 187 | duration = (end_time - start_time).total_seconds() 188 | 189 | print(f"\n{'='*60}") 190 | print(f"## 华润通签到任务完成") 191 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 192 | print(f"## 执行耗时: {int(duration)} 秒") 193 | print(f"{'='*60}\n") 194 | 195 | # 发送推送通知 196 | send_notification_summary(all_results, start_time, end_time) 197 | 198 | 199 | if __name__ == "__main__": 200 | main() 201 | -------------------------------------------------------------------------------- /script/huaruntong/wentiweilaihui/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文体未来荟签到脚本 3 | """ 4 | import json 5 | import os 6 | import sys 7 | from datetime import datetime 8 | from pathlib import Path 9 | from api import WenTiWeiLaiHuiAPI 10 | 11 | # 添加项目根目录到Python路径以导入notification模块 12 | current_dir = Path(__file__).parent 13 | project_root = current_dir.parent.parent.parent 14 | sys.path.insert(0, str(project_root)) 15 | 16 | from notification import send_notification, NotificationSound 17 | 18 | 19 | def load_config(): 20 | """加载统一配置文件""" 21 | # 使用统一的 token.json 配置文件 22 | # 从当前文件位置向上三级到达项目根目录,然后进入 config 目录 23 | current_dir = os.path.dirname(os.path.abspath(__file__)) 24 | config_path = os.path.join(current_dir, '..', '..', '..', 'config', 'token.json') 25 | with open(config_path, 'r', encoding='utf-8') as f: 26 | return json.load(f) 27 | 28 | 29 | def process_account(account_config): 30 | """处理单个账号的签到""" 31 | account_name = account_config.get('account_name', '未知账号') 32 | token = account_config.get('token') 33 | mobile = account_config.get('mobile') 34 | 35 | # 初始化结果 36 | result_info = { 37 | 'account_name': account_name, 38 | 'mobile': mobile, 39 | 'success': False, 40 | 'error': None, 41 | 'sign_message': None, 42 | 'points': None, 43 | 'available_points': None 44 | } 45 | 46 | print("=" * 50) 47 | print(f"账号: {account_name} ({mobile})") 48 | print("=" * 50) 49 | 50 | # 创建API实例 51 | api = WenTiWeiLaiHuiAPI(token, mobile, account_config.get('user_agent')) 52 | 53 | # 执行签到 54 | print("\n开始签到...") 55 | sign_result = api.sign_in() 56 | 57 | if sign_result.get("success"): 58 | msg = sign_result.get('msg', '签到成功') 59 | print(f"✓ 签到成功: {msg}") 60 | result_info['sign_message'] = msg 61 | else: 62 | msg = sign_result.get('msg', '签到失败') 63 | print(f"✗ 签到失败: {msg}") 64 | result_info['error'] = msg 65 | 66 | # 查询积分 67 | print("\n查询万象星积分...") 68 | points_result = api.query_points() 69 | 70 | if points_result.get("success"): 71 | data = points_result.get("data", {}) 72 | points = data.get("points", 0) 73 | available_points = data.get("availablePoints", 0) 74 | hold_points = data.get("holdPoints", 0) 75 | 76 | print(f"✓ 查询成功") 77 | print(f" 总积分: {points}") 78 | print(f" 可用积分: {available_points}") 79 | print(f" 冻结积分: {hold_points}") 80 | 81 | result_info['points'] = points 82 | result_info['available_points'] = available_points 83 | result_info['success'] = True 84 | else: 85 | msg = points_result.get('msg', '查询失败') 86 | print(f"✗ 查询失败: {msg}") 87 | if not result_info['error']: 88 | result_info['error'] = msg 89 | 90 | print("\n" + "=" * 50) 91 | return result_info 92 | 93 | 94 | def send_notification_summary(all_results, start_time, end_time): 95 | """ 96 | 发送任务执行结果的推送通知 97 | 98 | Args: 99 | all_results: 所有账号的执行结果列表 100 | start_time: 任务开始时间 101 | end_time: 任务结束时间 102 | """ 103 | try: 104 | duration = (end_time - start_time).total_seconds() 105 | 106 | # 统计结果 107 | total_count = len(all_results) 108 | success_count = sum(1 for r in all_results if r.get('success')) 109 | failed_count = total_count - success_count 110 | 111 | # 构建通知标题 112 | if failed_count == 0: 113 | title = "文体未来荟签到成功 ✅" 114 | sound = NotificationSound.BIRDSONG 115 | elif success_count == 0: 116 | title = "文体未来荟签到失败 ❌" 117 | sound = NotificationSound.ALARM 118 | else: 119 | title = "文体未来荟签到部分成功 ⚠️" 120 | sound = NotificationSound.BELL 121 | 122 | # 构建通知内容 123 | content_parts = [ 124 | "📊 执行统计:", 125 | f"✅ 成功: {success_count} 个账号", 126 | f"❌ 失败: {failed_count} 个账号", 127 | f"📈 总计: {total_count} 个账号", 128 | "", 129 | "📝 详情:" 130 | ] 131 | 132 | for result in all_results: 133 | account_name = result.get('account_name', '未知账号') 134 | if result.get('success'): 135 | points = result.get('points', 0) 136 | available = result.get('available_points', 0) 137 | content_parts.append(f" ✅ [{account_name}]") 138 | content_parts.append(f" 总积分: {points} | 可用: {available}") 139 | else: 140 | error = result.get('error', '未知错误') 141 | if len(error) > 30: 142 | error = error[:30] + "..." 143 | content_parts.append(f" ❌ [{account_name}] {error}") 144 | 145 | content_parts.append("") 146 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 147 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 148 | 149 | content = "\n".join(content_parts) 150 | 151 | # 发送通知 152 | send_notification( 153 | title=title, 154 | content=content, 155 | sound=sound 156 | ) 157 | print("✅ 推送通知发送成功") 158 | 159 | except Exception as e: 160 | print(f"❌ 推送通知失败: {str(e)}") 161 | 162 | 163 | def main(): 164 | """主函数""" 165 | # 记录开始时间 166 | start_time = datetime.now() 167 | print("=" * 50) 168 | print("文体未来荟签到脚本") 169 | print(f"开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 170 | print("=" * 50) 171 | print() 172 | 173 | # 加载配置 174 | config = load_config() 175 | accounts = config.get('huaruntong', {}).get('wentiweilaihui', {}).get('accounts', []) 176 | 177 | if not accounts: 178 | print("❌ 配置文件中没有找到账号信息") 179 | return 180 | 181 | # 收集所有账号的结果 182 | all_results = [] 183 | 184 | # 遍历所有账号 185 | for account in accounts: 186 | if not account.get('token'): 187 | print(f"⚠️ 跳过账号 {account.get('account_name', '未知')}: token 为空") 188 | print("=" * 50) 189 | all_results.append({ 190 | 'account_name': account.get('account_name', '未知'), 191 | 'success': False, 192 | 'error': 'token为空' 193 | }) 194 | continue 195 | 196 | result = process_account(account) 197 | all_results.append(result) 198 | print("\n") 199 | 200 | # 记录结束时间 201 | end_time = datetime.now() 202 | duration = (end_time - start_time).total_seconds() 203 | 204 | print("=" * 50) 205 | print("执行完成") 206 | print(f"结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 207 | print(f"执行耗时: {int(duration)} 秒") 208 | print("=" * 50) 209 | 210 | # 发送推送通知 211 | send_notification_summary(all_results, start_time, end_time) 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /script/huaruntong/999/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 答题主程序 3 | """ 4 | import json 5 | import os 6 | import sys 7 | from datetime import datetime 8 | from pathlib import Path 9 | from api import QuizAPI 10 | 11 | # 添加项目根目录到Python路径以导入notification模块 12 | current_dir = Path(__file__).parent 13 | project_root = current_dir.parent.parent.parent 14 | sys.path.insert(0, str(project_root)) 15 | 16 | from notification import send_notification, NotificationSound 17 | 18 | 19 | def load_config(): 20 | """加载统一配置文件""" 21 | # 使用统一的 token.json 配置文件 22 | # 从当前文件位置向上三级到达项目根目录,然后进入 config 目录 23 | current_dir = os.path.dirname(os.path.abspath(__file__)) 24 | config_path = os.path.join(current_dir, '..', '..', '..', 'config', 'token.json') 25 | with open(config_path, 'r', encoding='utf-8') as f: 26 | return json.load(f) 27 | 28 | 29 | def find_correct_answer(question_data): 30 | """ 31 | 从题目数据中找出正确答案 32 | 33 | :param question_data: 题目数据 34 | :return: 正确答案的选项代码列表 35 | """ 36 | options = question_data.get('question', {}).get('options', []) 37 | correct_options = [] 38 | 39 | for option in options: 40 | if option.get('right'): 41 | correct_options.append(option.get('optionCode')) 42 | 43 | return correct_options 44 | 45 | 46 | def process_account(account_config): 47 | """处理单个账号的答题""" 48 | account_name = account_config.get('account_name', '未知账号') 49 | 50 | # 初始化结果 51 | result_info = { 52 | 'account_name': account_name, 53 | 'mobile': account_config['mobile'], 54 | 'success': False, 55 | 'error': None, 56 | 'question': None, 57 | 'answer': None 58 | } 59 | 60 | # 初始化 API 61 | api = QuizAPI( 62 | token=account_config["token"], 63 | mobile=account_config["mobile"], 64 | user_agent=account_config.get("user_agent") 65 | ) 66 | 67 | print("=" * 50) 68 | print(f"账号: {account_name} ({account_config['mobile']})") 69 | print("开始答题...") 70 | print("=" * 50) 71 | 72 | # 获取题目 73 | print("\n📝 正在获取题目...") 74 | result = api.get_question() 75 | 76 | if result.get('resultCode') != '0': 77 | error_msg = result.get('message', '未知错误') 78 | print(f"❌ 获取题目失败: {error_msg}") 79 | result_info['error'] = f"获取题目失败: {error_msg}" 80 | return result_info 81 | 82 | # 解析题目 83 | question_data = result.get('data', {}).get('knowledgeQuestionData') 84 | if not question_data: 85 | print("❌ 题目数据为空") 86 | result_info['error'] = '题目数据为空' 87 | return result_info 88 | 89 | question_id = question_data.get('questionId') 90 | question_text = question_data.get('question', {}).get('questionContents', [''])[0] 91 | options = question_data.get('question', {}).get('options', []) 92 | 93 | # 限制题目长度用于通知 94 | result_info['question'] = question_text[:30] + '...' if len(question_text) > 30 else question_text 95 | 96 | print(f"\n题目: {question_text}") 97 | print("\n选项:") 98 | for option in options: 99 | option_code = option.get('optionCode') 100 | option_text = option.get('optionContents', [''])[0] 101 | is_right = "✅" if option.get('right') else "" 102 | print(f" {option_code}. {option_text} {is_right}") 103 | 104 | # 找出正确答案 105 | correct_options = find_correct_answer(question_data) 106 | if not correct_options: 107 | print("\n❌ 未找到正确答案") 108 | result_info['error'] = '未找到正确答案' 109 | return result_info 110 | 111 | result_info['answer'] = ', '.join(correct_options) 112 | print(f"\n💡 正确答案: {result_info['answer']}") 113 | 114 | # 提交答案 115 | print("\n📤 正在提交答案...") 116 | submit_result = api.submit_answer(question_id, correct_options) 117 | 118 | if submit_result.get('resultCode') == '0': 119 | print("✅ 答题成功!") 120 | print(f"响应数据: {submit_result}") 121 | result_info['success'] = True 122 | else: 123 | error_msg = submit_result.get('message', '未知错误') 124 | print(f"❌ 答题失败: {error_msg}") 125 | result_info['error'] = f"答题失败: {error_msg}" 126 | 127 | print("\n" + "=" * 50) 128 | return result_info 129 | 130 | 131 | def send_notification_summary(all_results, start_time, end_time): 132 | """ 133 | 发送任务执行结果的推送通知 134 | 135 | Args: 136 | all_results: 所有账号的执行结果列表 137 | start_time: 任务开始时间 138 | end_time: 任务结束时间 139 | """ 140 | try: 141 | duration = (end_time - start_time).total_seconds() 142 | 143 | # 统计结果 144 | total_count = len(all_results) 145 | success_count = sum(1 for r in all_results if r.get('success')) 146 | failed_count = total_count - success_count 147 | 148 | # 构建通知标题 149 | if failed_count == 0: 150 | title = "华润通999答题成功 ✅" 151 | sound = NotificationSound.BIRDSONG 152 | elif success_count == 0: 153 | title = "华润通999答题失败 ❌" 154 | sound = NotificationSound.ALARM 155 | else: 156 | title = "华润通999答题部分成功 ⚠️" 157 | sound = NotificationSound.BELL 158 | 159 | # 构建通知内容 160 | content_parts = [ 161 | "📊 执行统计:", 162 | f"✅ 成功: {success_count} 个账号", 163 | f"❌ 失败: {failed_count} 个账号", 164 | f"📈 总计: {total_count} 个账号", 165 | "", 166 | "📝 详情:" 167 | ] 168 | 169 | for result in all_results: 170 | account_name = result.get('account_name', '未知账号') 171 | if result.get('success'): 172 | content_parts.append(f" ✅ [{account_name}] 答题成功") 173 | else: 174 | error = result.get('error', '未知错误') 175 | if len(error) > 30: 176 | error = error[:30] + "..." 177 | content_parts.append(f" ❌ [{account_name}] {error}") 178 | 179 | content_parts.append("") 180 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 181 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 182 | 183 | content = "\n".join(content_parts) 184 | 185 | # 发送通知 186 | send_notification( 187 | title=title, 188 | content=content, 189 | sound=sound 190 | ) 191 | print("✅ 推送通知发送成功") 192 | 193 | except Exception as e: 194 | print(f"❌ 推送通知失败: {str(e)}") 195 | 196 | 197 | def main(): 198 | """主函数""" 199 | # 记录开始时间 200 | start_time = datetime.now() 201 | print(f"\n{'='*60}") 202 | print(f"## 华润通999答题任务开始") 203 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 204 | print(f"{'='*60}\n") 205 | 206 | # 加载配置 207 | config = load_config() 208 | accounts = config.get('huaruntong', {}).get('999', {}).get('accounts', []) 209 | 210 | if not accounts: 211 | print("❌ 配置文件中没有找到账号信息") 212 | return 213 | 214 | # 收集所有账号的结果 215 | all_results = [] 216 | 217 | # 遍历所有账号 218 | for account in accounts: 219 | if not account.get('token'): 220 | print(f"⚠️ 跳过账号 {account.get('account_name', '未知')}: token 为空") 221 | print("=" * 50) 222 | all_results.append({ 223 | 'account_name': account.get('account_name', '未知'), 224 | 'success': False, 225 | 'error': 'token为空' 226 | }) 227 | continue 228 | 229 | result = process_account(account) 230 | all_results.append(result) 231 | print("\n") 232 | 233 | # 记录结束时间 234 | end_time = datetime.now() 235 | duration = (end_time - start_time).total_seconds() 236 | 237 | print(f"\n{'='*60}") 238 | print(f"## 华润通999答题任务完成") 239 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 240 | print(f"## 执行耗时: {int(duration)} 秒") 241 | print(f"{'='*60}\n") 242 | 243 | # 发送推送通知 244 | send_notification_summary(all_results, start_time, end_time) 245 | 246 | 247 | if __name__ == "__main__": 248 | main() 249 | 250 | -------------------------------------------------------------------------------- /script/erke/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 鸿星尔克API模块 5 | 6 | 提供鸿星尔克小程序相关的API接口 7 | """ 8 | 9 | import requests 10 | import logging 11 | import hashlib 12 | import random 13 | from typing import Dict, Optional 14 | from datetime import datetime, timedelta, timezone 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def get_gmt8_time() -> str: 20 | """ 21 | 获取GMT+8时间并格式化为字符串 22 | 23 | Returns: 24 | str: 格式化的时间字符串,如 "2025-11-28 11:36:14" 25 | """ 26 | tz = timezone(timedelta(hours=8)) 27 | now = datetime.now(tz) 28 | return now.strftime('%Y-%m-%d %H:%M:%S') 29 | 30 | 31 | def calculate_sign(appid: str, member_id: str, timestamp: str = None) -> dict: 32 | """ 33 | 计算请求签名 34 | 35 | 根据JS代码逻辑: 36 | timestamp=时间戳 37 | transId=appid+时间戳 38 | secret=damogic8888 39 | random=随机数 40 | memberId=会员ID 41 | 42 | Args: 43 | appid: 小程序appid 44 | member_id: 会员ID 45 | timestamp: 时间戳,不传则自动生成 46 | 47 | Returns: 48 | dict: 包含sign和相关参数的字典 49 | """ 50 | if timestamp is None: 51 | timestamp = get_gmt8_time() 52 | 53 | random_num = random.randint(0, 9999999) 54 | trans_id = appid + timestamp 55 | 56 | sign_str = f"timestamp={timestamp}" 57 | sign_str += f"transId={trans_id}" 58 | sign_str += "secret=damogic8888" 59 | sign_str += f"random={random_num}" 60 | sign_str += f"memberId={member_id}" 61 | 62 | sign = hashlib.md5(sign_str.encode()).hexdigest() 63 | 64 | return { 65 | 'sign': sign, 66 | 'timestamp': timestamp, 67 | 'transId': trans_id, 68 | 'random': random_num, 69 | 'appid': appid, 70 | 'memberId': member_id 71 | } 72 | 73 | 74 | class ErkeAPI: 75 | """鸿星尔克API类""" 76 | 77 | def __init__( 78 | self, 79 | member_id: str, 80 | enterprise_id: str, 81 | unionid: str, 82 | openid: str, 83 | wx_openid: str, 84 | appid: str = "wxa1f1fa3785a47c7d", 85 | user_agent: Optional[str] = None 86 | ): 87 | """ 88 | 初始化API类 89 | 90 | Args: 91 | member_id: 会员ID 92 | enterprise_id: 企业ID 93 | unionid: 微信unionid 94 | openid: 小程序openid 95 | wx_openid: 微信wxOpenid 96 | appid: 小程序appid,默认为wxa1f1fa3785a47c7d 97 | user_agent: 用户代理字符串,可选 98 | """ 99 | self.member_id = member_id 100 | self.enterprise_id = enterprise_id 101 | self.unionid = unionid 102 | self.openid = openid 103 | self.wx_openid = wx_openid 104 | self.appid = appid 105 | self.user_agent = user_agent or ( 106 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' 107 | '(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 ' 108 | 'MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI ' 109 | 'MiniProgramEnv/Windows WindowsWechat/WMPF ' 110 | 'WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf2541510) XWEB/17071' 111 | ) 112 | self.base_url = 'https://hope.demogic.com/gic-wx-app' 113 | 114 | def get_headers(self, sign: str) -> Dict[str, str]: 115 | """ 116 | 获取请求头 117 | 118 | Args: 119 | sign: 签名参数 120 | 121 | Returns: 122 | Dict[str, str]: 请求头字典 123 | """ 124 | return { 125 | 'User-Agent': self.user_agent, 126 | 'Content-Type': 'application/x-www-form-urlencoded', 127 | 'Pragma': 'no-cache', 128 | 'Cache-Control': 'no-cache', 129 | 'sign': sign, 130 | 'channelEntrance': 'wx_app', 131 | 'xweb_xhr': '1', 132 | 'Sec-Fetch-Site': 'cross-site', 133 | 'Sec-Fetch-Mode': 'cors', 134 | 'Sec-Fetch-Dest': 'empty', 135 | 'Referer': 'https://servicewechat.com/wxa1f1fa3785a47c7d/85/page-frame.html', 136 | 'Accept-Language': 'zh-CN,zh;q=0.9' 137 | } 138 | 139 | def get_integral_record( 140 | self, 141 | current_page: int = 1, 142 | page_size: int = 20 143 | ) -> Dict: 144 | """ 145 | 获取积分明细 146 | 147 | Args: 148 | current_page: 当前页码,默认为1 149 | page_size: 每页大小,默认为20 150 | 151 | Returns: 152 | Dict: 接口返回结果 153 | { 154 | 'success': bool, # 是否成功 155 | 'result': dict, # 成功时的结果数据 156 | 'error': str # 失败时的错误信息 157 | } 158 | """ 159 | logger.info("开始获取积分明细...") 160 | 161 | # 计算签名 162 | sign_data = calculate_sign(self.appid, self.member_id) 163 | 164 | # 构建请求数据 165 | data = { 166 | 'currentPage': current_page, 167 | 'pageSize': page_size, 168 | 'memberId': self.member_id, 169 | 'cliqueId': '-1', 170 | 'cliqueMemberId': '-1', 171 | 'useClique': '0', 172 | 'enterpriseId': self.enterprise_id, 173 | 'unionid': self.unionid, 174 | 'openid': self.openid, 175 | 'wxOpenid': self.wx_openid, 176 | 'random': sign_data['random'], 177 | 'appid': sign_data['appid'], 178 | 'transId': sign_data['transId'], 179 | 'sign': sign_data['sign'], 180 | 'timestamp': sign_data['timestamp'], 181 | 'gicWxaVersion': '3.9.56', 182 | 'launchOptions': '{"path":"pages/authorize/authorize","query":{},"scene":1101,"referrerInfo":{},"apiCategory":"default"}' 183 | } 184 | 185 | # 获取请求头(这里的sign用于header) 186 | headers = self.get_headers(self.enterprise_id) 187 | 188 | try: 189 | url = f"{self.base_url}/integral_record.json" 190 | response = requests.post( 191 | url, 192 | headers=headers, 193 | data=data, 194 | timeout=30 195 | ) 196 | 197 | # 检查响应状态 198 | response.raise_for_status() 199 | 200 | # 解析响应 201 | result = response.json() 202 | 203 | logger.info(f"积分明细获取成功") 204 | return { 205 | 'success': True, 206 | 'result': result, 207 | 'error': None 208 | } 209 | 210 | except requests.exceptions.RequestException as e: 211 | error_msg = f"请求失败: {str(e)}" 212 | logger.error(error_msg) 213 | return { 214 | 'success': False, 215 | 'result': None, 216 | 'error': error_msg 217 | } 218 | except Exception as e: 219 | error_msg = f"未知错误: {str(e)}" 220 | logger.error(error_msg) 221 | return { 222 | 'success': False, 223 | 'result': None, 224 | 'error': error_msg 225 | } 226 | 227 | def member_sign(self) -> Dict: 228 | """ 229 | 会员签到 230 | 231 | Returns: 232 | Dict: 接口返回结果 233 | { 234 | 'success': bool, # 是否成功 235 | 'result': dict, # 成功时的结果数据 236 | 'error': str # 失败时的错误信息 237 | } 238 | """ 239 | logger.info("开始执行签到...") 240 | 241 | # 计算签名 242 | sign_data = calculate_sign(self.appid, self.member_id) 243 | 244 | # 构建请求数据(JSON格式) 245 | data = { 246 | 'source': 'wxapp', 247 | 'memberId': self.member_id, 248 | 'cliqueId': '-1', 249 | 'cliqueMemberId': '-1', 250 | 'useClique': 0, 251 | 'enterpriseId': self.enterprise_id, 252 | 'unionid': self.unionid, 253 | 'openid': self.openid, 254 | 'wxOpenid': self.wx_openid, 255 | 'sign': sign_data['sign'], 256 | 'random': sign_data['random'], 257 | 'appid': sign_data['appid'], 258 | 'transId': sign_data['transId'], 259 | 'timestamp': sign_data['timestamp'], 260 | 'gicWxaVersion': '3.9.56', 261 | 'launchOptions': '{"path":"pages/authorize/authorize","query":{},"scene":1101,"referrerInfo":{},"apiCategory":"default"}' 262 | } 263 | 264 | # 获取请求头 265 | headers = self.get_headers(self.enterprise_id) 266 | headers['Content-Type'] = 'application/json;charset=UTF-8' 267 | 268 | try: 269 | url = f"{self.base_url}/sign/member_sign.json" 270 | response = requests.post( 271 | url, 272 | headers=headers, 273 | json=data, 274 | timeout=30 275 | ) 276 | 277 | # 检查响应状态 278 | response.raise_for_status() 279 | 280 | # 解析响应 281 | result = response.json() 282 | 283 | logger.info(f"签到成功") 284 | return { 285 | 'success': True, 286 | 'result': result, 287 | 'error': None 288 | } 289 | 290 | except requests.exceptions.RequestException as e: 291 | error_msg = f"请求失败: {str(e)}" 292 | logger.error(error_msg) 293 | return { 294 | 'success': False, 295 | 'result': None, 296 | 'error': error_msg 297 | } 298 | except Exception as e: 299 | error_msg = f"未知错误: {str(e)}" 300 | logger.error(error_msg) 301 | return { 302 | 'success': False, 303 | 'result': None, 304 | 'error': error_msg 305 | } 306 | -------------------------------------------------------------------------------- /script/shyp/auto_buy.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定时抢购脚本 3 | 用于在指定时间自动抢购商品 4 | """ 5 | 6 | import time 7 | import requests 8 | from datetime import datetime 9 | import logging 10 | 11 | # 配置日志 12 | logging.basicConfig( 13 | level=logging.INFO, 14 | format='%(asctime)s - %(levelname)s - %(message)s' 15 | ) 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class AutoBuy: 20 | """自动抢购类""" 21 | 22 | def __init__(self): 23 | """初始化抢购配置""" 24 | # API 配置 25 | self.api_url = "https://mall-api.shmedia.tech/trade-service/trade/carts/buy" 26 | 27 | # 请求参数 28 | self.params = { 29 | "sku_id": "1539881186344108034", 30 | "num": "1", 31 | "activity_id": "1706607500810358785", 32 | "promotion_type": "EXCHANGE" 33 | } 34 | 35 | # 请求头 36 | self.headers = { 37 | "User-Agent": "Mozilla/5.0 (Linux; Android 10; MI 8 SE Build/QKQ1.190828.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.101 Mobile Safari/537.36Rmt/YangPu; Version/3.0.2", 38 | "Accept": "application/json, text/plain, */*", 39 | "Accept-Encoding": "gzip, deflate", 40 | "Content-Length": "0", 41 | "Authorization": "eyJhbGciOiJIUzUxMiJ9.eyJ1aWQiOjE5ODYzMTQ5MjY1MTEwOTk5MDUsInN1YiI6InVzZXIiLCJzaXRlIjoiMzEwMTEwIiwiYXJlYVByZWZpeCI6InlwIiwicm9sZXMiOlsiQlVZRVIiXSwibW9iaWxlIjoiMTc2MzM4ODI2MjEiLCJzaG9wSWQiOiIzMTAxMTAwMSIsImxpdmVNZXNzYWdlIjpudWxsLCJleHAiOjE3NjUwNzE2NjIsInV1aWQiOiJmYzI5MDAwOC00M2Q5LTRkYWYtYTMwMC1iMzM4Mjk0ZWRiYTQiLCJ1c2VybmFtZSI6Im1lZGlhX2JhNTgzOGZkIiwidGFyZ2V0IjoibWVkaWEifQ.t_xeRDJeCCj3uqaJjdyzZrbYS4FjP85-YMAFbjp_O6KPaYqCYrkzoWxkZ78YsJ4acYZXvAyl8eiOMQNIHxLs4A", 42 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", 43 | "Origin": "https://mall-mobile.shmedia.tech", 44 | "X-Requested-With": "com.wdit.shrmtyp", 45 | "Sec-Fetch-Site": "same-site", 46 | "Sec-Fetch-Mode": "cors", 47 | "Sec-Fetch-Dest": "empty", 48 | "Referer": "https://mall-mobile.shmedia.tech/real.html?shop_id=31011001&site_id=310110&target=media&access_id=182&version=2025091601", 49 | "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", 50 | "Cookie": "acw_tc=ac11000117624798203052297e48cbaf45af00a4165176e6e59bf1b5159fcc" 51 | } 52 | 53 | def update_config(self, sku_id=None, num=None, activity_id=None, 54 | promotion_type=None, authorization=None): 55 | """ 56 | 更新抢购配置 57 | 58 | Args: 59 | sku_id: 商品SKU ID 60 | num: 购买数量 61 | activity_id: 活动ID 62 | promotion_type: 促销类型 63 | authorization: 授权token 64 | """ 65 | if sku_id: 66 | self.params["sku_id"] = sku_id 67 | if num: 68 | self.params["num"] = str(num) 69 | if activity_id: 70 | self.params["activity_id"] = activity_id 71 | if promotion_type: 72 | self.params["promotion_type"] = promotion_type 73 | if authorization: 74 | self.headers["Authorization"] = authorization 75 | 76 | logger.info("配置已更新") 77 | 78 | def buy(self): 79 | """ 80 | 执行抢购请求 81 | 82 | Returns: 83 | tuple: (是否成功, 响应数据) 84 | """ 85 | try: 86 | logger.info(f"开始抢购: {self.api_url}") 87 | logger.info(f"参数: {self.params}") 88 | 89 | response = requests.post( 90 | self.api_url, 91 | params=self.params, 92 | headers=self.headers, 93 | timeout=10 94 | ) 95 | 96 | logger.info(f"响应状态码: {response.status_code}") 97 | logger.info(f"响应内容长度: {len(response.text)} 字节") 98 | logger.info(f"响应内容: {response.text if response.text else '[空响应]'}") 99 | logger.info(f"响应头 Content-Type: {response.headers.get('Content-Type', 'unknown')}") 100 | 101 | # 处理响应 102 | if response.status_code == 200: 103 | # 检查响应内容是否为空 104 | if not response.text or response.text.strip() == '': 105 | logger.warning("响应内容为空,可能表示抢购成功") 106 | return True, {"code": "200", "message": "响应为空,可能成功", "raw_response": ""} 107 | 108 | # 尝试解析JSON 109 | try: 110 | result = response.json() 111 | logger.info(f"解析后的JSON: {result}") 112 | return True, result 113 | except ValueError as json_error: 114 | logger.error(f"JSON解析失败: {str(json_error)}") 115 | logger.error(f"原始响应内容(repr): {repr(response.text)}") 116 | return False, { 117 | "error": "JSON解析失败", 118 | "json_error": str(json_error), 119 | "raw_response": response.text 120 | } 121 | else: 122 | # 非200状态码,尝试解析错误信息 123 | error_msg = f"请求失败,状态码: {response.status_code}" 124 | try: 125 | error_data = response.json() 126 | logger.error(f"错误详情: {error_data}") 127 | return False, {"error": error_msg, "details": error_data} 128 | except: 129 | return False, {"error": error_msg, "raw_response": response.text} 130 | 131 | except requests.exceptions.Timeout: 132 | logger.error("请求超时") 133 | return False, {"error": "请求超时"} 134 | except requests.exceptions.RequestException as e: 135 | logger.error(f"请求异常: {str(e)}") 136 | return False, {"error": str(e)} 137 | except Exception as e: 138 | logger.error(f"未知错误: {str(e)}") 139 | import traceback 140 | logger.error(f"错误堆栈: {traceback.format_exc()}") 141 | return False, {"error": str(e)} 142 | 143 | def wait_until(self, target_time_str): 144 | """ 145 | 等待到指定时间 146 | 147 | Args: 148 | target_time_str: 目标时间字符串,格式: "YYYY-MM-DD HH:MM:SS" 或 "HH:MM:SS" 149 | """ 150 | try: 151 | # 尝试解析完整日期时间 152 | if len(target_time_str.split()) == 2: 153 | target_time = datetime.strptime(target_time_str, "%Y-%m-%d %H:%M:%S") 154 | else: 155 | # 只有时间,使用今天的日期 156 | today = datetime.now().strftime("%Y-%m-%d") 157 | target_time = datetime.strptime(f"{today} {target_time_str}", "%Y-%m-%d %H:%M:%S") 158 | 159 | # 如果目标时间已经过去,则设置为明天 160 | if target_time < datetime.now(): 161 | logger.warning("目标时间已过,将在明天的相同时间执行") 162 | from datetime import timedelta 163 | target_time = target_time + timedelta(days=1) 164 | 165 | logger.info(f"目标抢购时间: {target_time}") 166 | 167 | while True: 168 | now = datetime.now() 169 | diff = (target_time - now).total_seconds() 170 | 171 | if diff <= 0: 172 | logger.info("到达抢购时间!") 173 | break 174 | 175 | if diff > 60: 176 | logger.info(f"距离抢购还有 {int(diff)} 秒 ({int(diff/60)} 分钟)") 177 | time.sleep(30) # 超过1分钟时,每30秒检查一次 178 | elif diff > 10: 179 | logger.info(f"距离抢购还有 {int(diff)} 秒") 180 | time.sleep(1) # 10秒到1分钟之间,每秒检查一次 181 | elif diff > 1: 182 | logger.info(f"距离抢购还有 {diff:.3f} 秒") 183 | time.sleep(0.1) # 最后10秒,每0.1秒检查一次 184 | else: 185 | # 最后1秒,精确等待 186 | time.sleep(diff) 187 | break 188 | 189 | except ValueError as e: 190 | logger.error(f"时间格式错误: {str(e)}") 191 | logger.error("请使用格式: 'YYYY-MM-DD HH:MM:SS' 或 'HH:MM:SS'") 192 | raise 193 | 194 | def timed_buy(self, target_time_str, retry_times=3, retry_interval=0.1): 195 | """ 196 | 定时抢购 197 | 198 | Args: 199 | target_time_str: 目标时间字符串 200 | retry_times: 失败后重试次数 201 | retry_interval: 重试间隔(秒) 202 | """ 203 | logger.info("=" * 50) 204 | logger.info("定时抢购脚本启动") 205 | logger.info("=" * 50) 206 | 207 | # 等待到指定时间 208 | self.wait_until(target_time_str) 209 | 210 | # 开始抢购 211 | for i in range(retry_times): 212 | logger.info(f"第 {i + 1} 次尝试抢购") 213 | success, result = self.buy() 214 | 215 | if success: 216 | logger.info("=" * 50) 217 | logger.info("抢购成功!") 218 | logger.info(f"结果: {result}") 219 | logger.info("=" * 50) 220 | return True, result 221 | else: 222 | logger.warning(f"第 {i + 1} 次抢购失败: {result}") 223 | if i < retry_times - 1: 224 | logger.info(f"等待 {retry_interval} 秒后重试...") 225 | time.sleep(retry_interval) 226 | 227 | logger.error("=" * 50) 228 | logger.error("抢购失败,已达到最大重试次数") 229 | logger.error("=" * 50) 230 | return False, None 231 | 232 | 233 | def main(): 234 | """主函数 - 示例用法""" 235 | # 创建抢购实例 236 | buyer = AutoBuy() 237 | 238 | # ====== 配置示例 ====== 239 | # 如需修改配置,可以调用 update_config 方法 240 | # buyer.update_config( 241 | # sku_id="1539881186344108034", 242 | # num=1, 243 | # activity_id="1706607500810358785", 244 | # promotion_type="EXCHANGE", 245 | # authorization="your_new_token_here" 246 | # ) 247 | 248 | # ====== 使用方式 1: 设置具体日期时间 ====== 249 | # target_time = "2025-11-08 10:00:00" # 指定完整的日期和时间 250 | 251 | # ====== 使用方式 2: 只设置时间(使用今天日期) ====== 252 | target_time = "10:00:00" # 只指定时间,将使用今天的日期 253 | 254 | # ====== 使用方式 3: 立即执行(测试用) ====== 255 | # now = datetime.now() 256 | # target_time = (now + timedelta(seconds=5)).strftime("%H:%M:%S") # 5秒后执行 257 | 258 | # 执行定时抢购 259 | # 参数说明: 260 | # - target_time: 抢购时间 261 | # - retry_times: 失败后重试次数(默认3次) 262 | # - retry_interval: 重试间隔秒数(默认0.1秒) 263 | buyer.timed_buy( 264 | target_time_str=target_time, 265 | retry_times=10, # 重试5次 266 | retry_interval=0.3 # 每次间隔0.05秒 267 | ) 268 | 269 | 270 | if __name__ == "__main__": 271 | main() 272 | 273 | -------------------------------------------------------------------------------- /script/sf/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 顺丰快递API模块 5 | 6 | 提供顺丰快递积分任务相关的API接口 7 | """ 8 | 9 | import requests 10 | import time 11 | import hashlib 12 | import execjs 13 | import os 14 | import logging 15 | from typing import Dict, Any 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class SFExpressAPI: 21 | """顺丰速运API接口类""" 22 | 23 | def __init__(self, cookies: str = None, user_id: str = None, user_agent: str = None, channel: str = None, device_id: str = None): 24 | """ 25 | 初始化SF Express API 26 | 27 | Args: 28 | cookies: Cookie字符串 29 | user_id: 用户ID 30 | user_agent: 用户代理 31 | channel: 渠道 32 | device_id: 设备ID 33 | """ 34 | self.js_file_path = os.path.join(os.path.dirname(__file__), 'code.js') 35 | self.base_url = "https://mcs-mimp-web.sf-express.com" 36 | self.session = requests.Session() 37 | self.cookies = cookies 38 | self.user_id = user_id 39 | self.user_agent = user_agent or ( 40 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) ' 41 | 'AppleWebKit/605.1.15 (KHTML, like Gecko) ' 42 | 'Version/18.5 Mobile/15E148 Safari/604.1' 43 | ) 44 | self.channel = channel 45 | self.device_id = device_id 46 | self._init_js() 47 | 48 | self.default_headers = { 49 | "User-Agent": self.user_agent, 50 | "pragma": "no-cache", 51 | "cache-control": "no-cache", 52 | "timestamp": "", 53 | "signature": "", 54 | "channel": self.channel, 55 | "syscode": "MCS-MIMP-CORE", 56 | "sw8": "", 57 | "platform": "SFAPP", 58 | "sec-gpc": "1", 59 | "accept-language": "zh-CN,zh;q=0.9", 60 | "origin": "https://mcs-mimp-web.sf-express.com", 61 | "sec-fetch-site": "same-origin", 62 | "sec-fetch-mode": "cors", 63 | "sec-fetch-dest": "empty", 64 | "referer": "https://mcs-mimp-web.sf-express.com/superWelfare?citycode=&cityname=&tab=0", 65 | "cookie": self.cookies, 66 | "priority": "u=1, i" 67 | } 68 | 69 | def _init_js(self): 70 | """初始化JavaScript环境""" 71 | try: 72 | with open(self.js_file_path, 'r', encoding='utf-8') as f: 73 | js_code = f.read() 74 | self.js_context = execjs.compile(js_code) 75 | except Exception as e: 76 | logger.error(f"初始化JavaScript环境失败: {e}") 77 | self.js_context = None 78 | 79 | def get_sw8(self, url_path): 80 | """调用JavaScript中的get_sw8函数""" 81 | if self.js_context is None: 82 | raise RuntimeError("JavaScript context not initialized") 83 | 84 | try: 85 | result = self.js_context.call('get_sw8', url_path) 86 | return result 87 | except Exception as e: 88 | logger.error(f"调用get_sw8函数时出错: {e}") 89 | return None 90 | 91 | def generate_signature(self, timestamp: str, sys_code: str = None) -> str: 92 | """生成签名""" 93 | sign_str = f"wwesldfs29aniversaryvdld29×tamp={timestamp}&sysCode={sys_code}" 94 | return hashlib.md5(sign_str.encode()).hexdigest() 95 | 96 | def query_point_task_and_sign(self, channel_type: str = "1", device_id: str = None) -> Dict[str, Any]: 97 | """ 98 | 查询积分任务和签到信息 99 | 100 | Args: 101 | channel_type: 渠道类型,默认为"1" 102 | device_id: 设备ID,如果不提供则使用初始化时的device_id 103 | 104 | Returns: 105 | Dict: API响应结果 106 | """ 107 | url_path = "/mcs-mimp/commonPost/~memberNonactivity~integralTaskStrategyService~queryPointTaskAndSignFromES" 108 | url = f"{self.base_url}{url_path}" 109 | 110 | timestamp = str(int(time.time() * 1000)) 111 | 112 | data = { 113 | "channelType": channel_type, 114 | "deviceId": device_id or self.device_id 115 | } 116 | 117 | headers = self.default_headers.copy() 118 | sys_code = 'MCS-MIMP-CORE' 119 | headers.update({ 120 | 'timestamp': timestamp, 121 | 'signature': self.generate_signature(timestamp, sys_code), 122 | 'sw8': self.get_sw8(url_path).get('code') 123 | }) 124 | 125 | try: 126 | response = self.session.post( 127 | url, 128 | headers=headers, 129 | json=data, 130 | timeout=30 131 | ) 132 | response.raise_for_status() 133 | return response.json() 134 | except requests.exceptions.RequestException as e: 135 | return { 136 | "success": False, 137 | "error": str(e), 138 | "message": "请求失败" 139 | } 140 | 141 | def finish_task(self, task_code: str) -> Dict[str, Any]: 142 | """ 143 | 完成任务接口 144 | 145 | Args: 146 | task_code: 任务代码 147 | 148 | Returns: 149 | Dict: API响应结果 150 | """ 151 | url_path = "/mcs-mimp/commonPost/~memberEs~taskRecord~finishTask" 152 | url = f"{self.base_url}{url_path}" 153 | 154 | timestamp = str(int(time.time() * 1000)) 155 | 156 | data = { 157 | "taskCode": task_code 158 | } 159 | 160 | headers = self.default_headers.copy() 161 | sys_code = 'MCS-MIMP-CORE' 162 | headers.update({ 163 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 164 | 'Content-Type': 'application/json', 165 | 'timestamp': timestamp, 166 | 'signature': self.generate_signature(timestamp, sys_code), 167 | 'sw8': self.get_sw8(url_path).get('code') if self.get_sw8(url_path) else '', 168 | 'referer': 'https://mcs-mimp-web.sf-express.com/home?from=qqjrwzx515&WC_AC_ID=111&WC_REPORT=111' 169 | }) 170 | 171 | try: 172 | response = self.session.post( 173 | url, 174 | headers=headers, 175 | json=data, 176 | timeout=30 177 | ) 178 | response.raise_for_status() 179 | return response.json() 180 | except requests.exceptions.RequestException as e: 181 | return { 182 | "success": False, 183 | "error": str(e), 184 | "message": "完成任务请求失败" 185 | } 186 | 187 | def fetch_tasks_reward(self, channel_type: str = "1", device_id: str = None) -> Dict[str, Any]: 188 | """ 189 | 获取任务奖励接口 190 | 191 | Args: 192 | channel_type: 渠道类型,默认为"1" 193 | device_id: 设备ID,如果不提供则使用初始化时的device_id 194 | 195 | Returns: 196 | Dict: API响应结果 197 | """ 198 | url_path = "/mcs-mimp/commonNoLoginPost/~memberNonactivity~integralTaskStrategyService~fetchTasksReward" 199 | url = f"{self.base_url}{url_path}" 200 | 201 | timestamp = str(int(time.time() * 1000)) 202 | 203 | data = { 204 | "channelType": channel_type, 205 | "deviceId": device_id or self.device_id 206 | } 207 | 208 | headers = self.default_headers.copy() 209 | sys_code = 'MCS-MIMP-CORE' 210 | headers.update({ 211 | 'User-Agent': self.user_agent, 212 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 213 | 'Content-Type': 'application/json', 214 | 'timestamp': timestamp, 215 | 'signature': self.generate_signature(timestamp, sys_code), 216 | 'sw8': self.get_sw8(url_path).get('code') if self.get_sw8(url_path) else '', 217 | 'referer': 'https://mcs-mimp-web.sf-express.com/superWelfare?citycode=&cityname=&tab=0', 218 | }) 219 | 220 | try: 221 | response = self.session.post( 222 | url, 223 | headers=headers, 224 | json=data, 225 | timeout=30 226 | ) 227 | response.raise_for_status() 228 | return response.json() 229 | except requests.exceptions.RequestException as e: 230 | return { 231 | "success": False, 232 | "error": str(e), 233 | "message": "获取任务奖励请求失败" 234 | } 235 | 236 | def automatic_sign_fetch_package(self, come_from: str = "vioin", channel_from: str = "SFAPP") -> Dict[str, Any]: 237 | """ 238 | 自动签到获取礼包接口 239 | 240 | Args: 241 | come_from: 来源,默认为"vioin" 242 | channel_from: 渠道来源,默认为"SFAPP" 243 | 244 | Returns: 245 | Dict: API响应结果 246 | """ 247 | url_path = "/mcs-mimp/commonPost/~memberNonactivity~integralTaskSignPlusService~automaticSignFetchPackage" 248 | url = f"{self.base_url}{url_path}" 249 | 250 | timestamp = str(int(time.time() * 1000)) 251 | 252 | data = { 253 | "comeFrom": come_from, 254 | "channelFrom": channel_from 255 | } 256 | 257 | headers = self.default_headers.copy() 258 | sys_code = 'MCS-MIMP-CORE' 259 | headers.update({ 260 | 'User-Agent': self.user_agent, 261 | 'Accept': 'application/json, text/plain, */*', 262 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 263 | 'Content-Type': 'application/json', 264 | 'timestamp': timestamp, 265 | 'signature': self.generate_signature(timestamp, sys_code), 266 | 'sw8': self.get_sw8(url_path).get('code') if self.get_sw8(url_path) else '', 267 | 'deviceid': self.device_id, 268 | 'accept-language': 'zh-CN,zh-Hans;q=0.9', 269 | 'priority': 'u=3, i', 270 | 'referer': f'https://mcs-mimp-web.sf-express.com/superWelfare?mobile=176****2621&userId={self.user_id}&path=/superWelfare&supportShare=YES&from=appIndex&tab=1' 271 | }) 272 | 273 | try: 274 | response = self.session.post( 275 | url, 276 | headers=headers, 277 | json=data, 278 | timeout=30 279 | ) 280 | response.raise_for_status() 281 | return response.json() 282 | except requests.exceptions.RequestException as e: 283 | return { 284 | "success": False, 285 | "error": str(e), 286 | "message": "自动签到获取礼包请求失败" 287 | } 288 | 289 | -------------------------------------------------------------------------------- /script/erke/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 鸿星尔克签到脚本 5 | 6 | 该脚本用于自动执行鸿星尔克小程序的签到任务,包括: 7 | - 读取账号配置信息 8 | - 查询积分明细 9 | - 执行签到操作 10 | - 输出执行结果统计 11 | 12 | Author: Assistant 13 | Date: 2025-11-28 14 | """ 15 | 16 | import json 17 | import logging 18 | import sys 19 | from typing import List, Dict, Any 20 | from pathlib import Path 21 | 22 | from api import ErkeAPI 23 | 24 | # 获取项目根目录 25 | project_root = Path(__file__).resolve().parent.parent.parent 26 | sys.path.insert(0, str(project_root)) 27 | 28 | # 导入需要的模块 29 | from notification import send_notification, NotificationSound 30 | 31 | 32 | 33 | class ErkeTasks: 34 | """鸿星尔克签到任务自动化执行类""" 35 | 36 | def __init__(self, config_path: str = None): 37 | """ 38 | 初始化任务执行器 39 | 40 | Args: 41 | config_path (str): 配置文件的完整路径,如果为None则使用项目根目录下的config/token.json 42 | """ 43 | # 设置配置文件路径 44 | if config_path is None: 45 | self.config_path = project_root / "config" / "token.json" 46 | else: 47 | self.config_path = Path(config_path) 48 | 49 | self.accounts: List[Dict[str, Any]] = [] 50 | self.logger = self._setup_logger() 51 | self._init_accounts() 52 | self.account_results: List[Dict[str, Any]] = [] 53 | 54 | def _setup_logger(self) -> logging.Logger: 55 | """ 56 | 设置日志记录器 57 | 58 | Returns: 59 | logging.Logger: 配置好的日志记录器 60 | """ 61 | logger = logging.getLogger(__name__) 62 | logger.setLevel(logging.INFO) 63 | 64 | # 创建控制台处理器 65 | console_handler = logging.StreamHandler() 66 | console_handler.setLevel(logging.INFO) 67 | 68 | # 设置日志格式 69 | formatter = logging.Formatter( 70 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 71 | datefmt='%Y-%m-%d %H:%M:%S' 72 | ) 73 | console_handler.setFormatter(formatter) 74 | 75 | # 避免重复添加处理器 76 | if not logger.handlers: 77 | logger.addHandler(console_handler) 78 | 79 | return logger 80 | 81 | def _init_accounts(self): 82 | """从配置文件中读取账号信息""" 83 | if not self.config_path.exists(): 84 | self.logger.error(f"配置文件不存在: {self.config_path}") 85 | raise FileNotFoundError(f"配置文件不存在: {self.config_path}") 86 | 87 | try: 88 | with open(self.config_path, 'r', encoding='utf-8') as f: 89 | config_data = json.load(f) 90 | # 从统一配置文件的 erke 节点读取 91 | erke_config = config_data.get('erke', {}) 92 | self.accounts = erke_config.get('accounts', []) 93 | 94 | if not self.accounts: 95 | self.logger.warning("配置文件中没有找到 erke 账号信息") 96 | else: 97 | self.logger.info(f"成功加载 {len(self.accounts)} 个账号配置") 98 | 99 | except json.JSONDecodeError as e: 100 | self.logger.error(f"配置文件JSON解析失败: {e}") 101 | raise 102 | except Exception as e: 103 | self.logger.error(f"读取配置文件失败: {e}") 104 | raise 105 | 106 | def process_account(self, account: Dict[str, Any]) -> Dict[str, Any]: 107 | """ 108 | 处理单个账号的任务 109 | 110 | Args: 111 | account (Dict[str, Any]): 账号信息字典 112 | 113 | Returns: 114 | Dict[str, Any]: 账号处理结果 115 | """ 116 | account_name = account.get('account_name', '未命名账号') 117 | self.logger.info(f"\n{'='*50}") 118 | self.logger.info(f"开始处理账号: {account_name}") 119 | self.logger.info(f"{'='*50}") 120 | 121 | result = { 122 | 'account_name': account_name, 123 | 'success': False, 124 | 'integral_info': None, 125 | 'sign_info': None, 126 | 'error': None 127 | } 128 | 129 | try: 130 | # 初始化API 131 | api = ErkeAPI( 132 | member_id=account.get('member_id', ''), 133 | enterprise_id=account.get('enterprise_id', ''), 134 | unionid=account.get('unionid', ''), 135 | openid=account.get('openid', ''), 136 | wx_openid=account.get('wx_openid', ''), 137 | user_agent=account.get('user_agent') 138 | ) 139 | 140 | # 1. 查询积分明细 141 | self.logger.info(f"[{account_name}] 查询积分明细...") 142 | integral_result = api.get_integral_record(current_page=1, page_size=5) 143 | 144 | if integral_result['success']: 145 | result['integral_info'] = integral_result['result'] 146 | self.logger.info(f"[{account_name}] 积分明细查询成功") 147 | 148 | # 解析积分信息 149 | if integral_result['result'] and isinstance(integral_result['result'], dict): 150 | response = integral_result['result'].get('response', {}) 151 | if isinstance(response, dict): 152 | # 获取累计积分和冻结积分 153 | accumulate_points = response.get('accumulatPoints', 0) 154 | frozen_points = response.get('frozenPoints', 0) 155 | available_points = accumulate_points - frozen_points 156 | 157 | self.logger.info(f"[{account_name}] 累计积分: {accumulate_points}") 158 | self.logger.info(f"[{account_name}] 冻结积分: {frozen_points}") 159 | self.logger.info(f"[{account_name}] 可用积分: {available_points}") 160 | 161 | # 获取积分明细列表 162 | page_data = response.get('page', {}) 163 | if page_data: 164 | total_count = page_data.get('totalCount', 0) 165 | self.logger.info(f"[{account_name}] 积分记录数: {total_count}") 166 | else: 167 | self.logger.warning(f"[{account_name}] 积分明细查询失败: {integral_result['error']}") 168 | 169 | # 2. 执行签到 170 | self.logger.info(f"[{account_name}] 执行签到...") 171 | sign_result = api.member_sign() 172 | 173 | if sign_result['success']: 174 | result['sign_info'] = sign_result['result'] 175 | self.logger.info(f"[{account_name}] 签到结果: {sign_result['result']}") 176 | 177 | # 解析签到返回的信息 178 | if sign_result['result'] and isinstance(sign_result['result'], dict): 179 | code = str(sign_result['result'].get('code', '') or '').strip() 180 | message = sign_result['result'].get('message', '') or '' 181 | 182 | success_codes = {'0000', '1001', '0', '200'} 183 | message_indicates_success = any(keyword in message for keyword in ['成功', '已签到']) 184 | 185 | if code in success_codes or message_indicates_success: 186 | result['success'] = True 187 | log_msg = message or '签到成功' 188 | self.logger.info(f"[{account_name}] {log_msg}") 189 | else: 190 | result['success'] = False 191 | result['error'] = message or f'未知返回码: {code}' 192 | self.logger.warning(f"[{account_name}] 签到返回: {message or code}") 193 | else: 194 | result['success'] = True 195 | self.logger.info(f"[{account_name}] 签到完成") 196 | else: 197 | result['error'] = sign_result['error'] 198 | self.logger.error(f"[{account_name}] 签到失败: {sign_result['error']}") 199 | 200 | except Exception as e: 201 | error_msg = f"处理账号时发生异常: {str(e)}" 202 | self.logger.error(f"[{account_name}] {error_msg}") 203 | result['error'] = error_msg 204 | 205 | return result 206 | 207 | def run(self): 208 | """执行所有账号的签到任务""" 209 | self.logger.info("="*60) 210 | self.logger.info("鸿星尔克签到任务开始执行") 211 | self.logger.info("="*60) 212 | 213 | if not self.accounts: 214 | self.logger.error("没有可处理的账号") 215 | return 216 | 217 | # 处理每个账号 218 | for account in self.accounts: 219 | result = self.process_account(account) 220 | self.account_results.append(result) 221 | 222 | 223 | # 输出统计信息 224 | self._print_summary() 225 | 226 | # 发送通知 227 | self._send_notification() 228 | 229 | def _print_summary(self): 230 | """输出执行结果统计""" 231 | self.logger.info("\n" + "="*60) 232 | self.logger.info("执行结果统计") 233 | self.logger.info("="*60) 234 | 235 | success_count = sum(1 for r in self.account_results if r['success']) 236 | fail_count = len(self.account_results) - success_count 237 | 238 | self.logger.info(f"总账号数: {len(self.account_results)}") 239 | self.logger.info(f"成功: {success_count}") 240 | self.logger.info(f"失败: {fail_count}") 241 | 242 | if fail_count > 0: 243 | self.logger.info("\n失败账号详情:") 244 | for result in self.account_results: 245 | if not result['success']: 246 | self.logger.info(f" - {result['account_name']}: {result['error']}") 247 | 248 | def _send_notification(self): 249 | """发送执行结果通知""" 250 | try: 251 | success_count = sum(1 for r in self.account_results if r['success']) 252 | total_count = len(self.account_results) 253 | 254 | # 构建通知标题 255 | title = "鸿星尔克签到任务完成" 256 | 257 | # 构建通知内容 258 | content_lines = [ 259 | f"📊 执行统计:", 260 | f" - 总账号数: {total_count}", 261 | f" - 成功: {success_count}", 262 | f" - 失败: {total_count - success_count}", 263 | ] 264 | 265 | # 添加每个账号的详细信息 266 | content_lines.append("\n📋 账号详情:") 267 | for result in self.account_results: 268 | status = "✅" if result['success'] else "❌" 269 | content_lines.append(f" {status} {result['account_name']}") 270 | 271 | if result['success'] and result['sign_info']: 272 | if isinstance(result['sign_info'], dict): 273 | message = result['sign_info'].get('message', '') 274 | if message: 275 | content_lines.append(f" └─ {message}") 276 | 277 | content = "\n".join(content_lines) 278 | 279 | # 发送通知 280 | send_notification( 281 | title=title, 282 | content=content, 283 | sound=NotificationSound.BIRDSONG 284 | ) 285 | self.logger.info("通知发送成功") 286 | 287 | except Exception as e: 288 | self.logger.error(f"发送通知失败: {str(e)}") 289 | 290 | 291 | def main(): 292 | """主函数""" 293 | try: 294 | tasks = ErkeTasks() 295 | tasks.run() 296 | except Exception as e: 297 | logging.error(f"程序执行失败: {str(e)}") 298 | sys.exit(1) 299 | 300 | 301 | if __name__ == "__main__": 302 | main() 303 | 304 | -------------------------------------------------------------------------------- /script/enshan/sign_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 恩山论坛自动签到脚本 5 | 6 | 功能: 7 | 1. 从token.json配置文件读取账号信息 8 | 2. 支持多账号管理 9 | 3. 自动执行签到并推送通知 10 | 11 | Author: ZaiZaiCat 12 | Date: 2025-01-20 13 | """ 14 | 15 | import json 16 | import logging 17 | import sys 18 | from typing import Dict, Any, List 19 | from datetime import datetime 20 | from pathlib import Path 21 | 22 | # 添加项目根目录到Python路径以导入notification模块 23 | project_root = Path(__file__).parent.parent.parent 24 | sys.path.insert(0, str(project_root)) 25 | 26 | from notification import send_notification, NotificationSound 27 | 28 | # 导入API模块(当前目录) 29 | from api import EnshanAPI 30 | 31 | # 配置日志 32 | logging.basicConfig( 33 | level=logging.INFO, 34 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 35 | ) 36 | logger = logging.getLogger(__name__) 37 | 38 | 39 | class EnshanSignInManager: 40 | """恩山论坛签到管理器""" 41 | 42 | def __init__(self, config_path: str = None): 43 | """ 44 | 初始化签到管理器 45 | 46 | Args: 47 | config_path: 配置文件路径,默认为项目根目录下的config/token.json 48 | """ 49 | if config_path is None: 50 | config_path = project_root / "config" / "token.json" 51 | else: 52 | config_path = Path(config_path) 53 | 54 | self.config_path = config_path 55 | self.site_name = "恩山论坛" 56 | self.accounts = [] 57 | self.load_config() 58 | 59 | def load_config(self) -> None: 60 | """加载配置文件""" 61 | try: 62 | logger.info(f"正在读取配置文件: {self.config_path}") 63 | with open(self.config_path, 'r', encoding='utf-8') as f: 64 | config = json.load(f) 65 | 66 | # 获取恩山论坛的配置 67 | enshan_config = config.get('enshan', {}) 68 | self.accounts = enshan_config.get('accounts', []) 69 | 70 | if not self.accounts: 71 | logger.warning("配置文件中没有找到恩山论坛账号信息") 72 | else: 73 | logger.info(f"成功加载 {len(self.accounts)} 个账号配置") 74 | 75 | except FileNotFoundError: 76 | logger.error(f"配置文件不存在: {self.config_path}") 77 | raise 78 | except json.JSONDecodeError as e: 79 | logger.error(f"配置文件JSON格式错误: {e}") 80 | raise 81 | except Exception as e: 82 | logger.error(f"加载配置文件失败: {e}") 83 | raise 84 | 85 | def sign_in_single_account(self, account: Dict[str, Any]) -> Dict[str, Any]: 86 | """ 87 | 单个账号签到 88 | 89 | Args: 90 | account: 账号配置信息 91 | 92 | Returns: 93 | Dict: 签到结果 94 | """ 95 | account_name = account.get('account_name', '未命名账号') 96 | cookies = account.get('cookies', '') 97 | formhash = account.get('formhash', '') 98 | user_agent = account.get('user_agent') 99 | 100 | logger.info(f"开始执行账号 [{account_name}] 的签到...") 101 | 102 | if not cookies or not formhash: 103 | error_msg = "cookies或formhash为空" 104 | logger.error(f"账号 [{account_name}] {error_msg}") 105 | return { 106 | 'account_name': account_name, 107 | 'success': False, 108 | 'error': error_msg 109 | } 110 | 111 | try: 112 | # 创建API实例并执行签到 113 | api = EnshanAPI(cookies, formhash, user_agent) 114 | result = api.sign_in() 115 | 116 | # 添加账号名称到结果中 117 | result['account_name'] = account_name 118 | 119 | if result.get('success'): 120 | logger.info(f"账号 [{account_name}] 签到成功") 121 | else: 122 | logger.error(f"账号 [{account_name}] 签到失败: {result.get('error', '未知错误')}") 123 | 124 | return result 125 | 126 | except Exception as e: 127 | error_msg = f"签到异常: {str(e)}" 128 | logger.error(f"账号 [{account_name}] {error_msg}", exc_info=True) 129 | return { 130 | 'account_name': account_name, 131 | 'success': False, 132 | 'error': error_msg 133 | } 134 | 135 | def sign_in_all_accounts(self) -> List[Dict[str, Any]]: 136 | """ 137 | 所有账号签到 138 | 139 | Returns: 140 | List[Dict]: 所有账号的签到结果列表 141 | """ 142 | if not self.accounts: 143 | logger.warning("没有可签到的账号") 144 | return [] 145 | 146 | results = [] 147 | for i, account in enumerate(self.accounts, 1): 148 | logger.info(f"\n{'='*60}") 149 | logger.info(f"正在处理第 {i}/{len(self.accounts)} 个账号") 150 | logger.info(f"{'='*60}") 151 | 152 | result = self.sign_in_single_account(account) 153 | results.append(result) 154 | 155 | return results 156 | 157 | def send_notification(self, results: List[Dict[str, Any]], start_time: datetime, end_time: datetime) -> None: 158 | """ 159 | 发送签到结果通知 160 | 161 | Args: 162 | results: 签到结果列表 163 | start_time: 任务开始时间 164 | end_time: 任务结束时间 165 | """ 166 | try: 167 | duration = (end_time - start_time).total_seconds() 168 | 169 | # 统计结果 170 | total_count = len(results) 171 | success_count = sum(1 for r in results if r.get('success')) 172 | failed_count = total_count - success_count 173 | 174 | # 构建通知标题 175 | if failed_count == 0: 176 | title = f"{self.site_name}签到成功 ✅" 177 | sound = NotificationSound.BIRDSONG 178 | elif success_count == 0: 179 | title = f"{self.site_name}签到失败 ❌" 180 | sound = NotificationSound.ALARM 181 | else: 182 | title = f"{self.site_name}签到部分成功 ⚠️" 183 | sound = NotificationSound.BELL 184 | 185 | # 构建通知内容 186 | content_parts = [f"📊 执行统计:"] 187 | 188 | if success_count > 0: 189 | content_parts.append(f"✅ 成功: {success_count} 个账号") 190 | if failed_count > 0: 191 | content_parts.append(f"❌ 失败: {failed_count} 个账号") 192 | 193 | content_parts.append(f"📈 总计: {total_count} 个账号") 194 | content_parts.append("") # 空行 195 | 196 | # 添加详细信息 197 | content_parts.append("📝 详情:") 198 | for result in results: 199 | account_name = result.get('account_name', '未知账号') 200 | if result.get('success'): 201 | api_result = result.get('result', {}) 202 | 203 | # 解析签到返回的数据 204 | if 'credit' in api_result: 205 | content_parts.append(f" ✅ [{account_name}] 获得积分: {api_result.get('credit')}") 206 | elif 'message' in api_result: 207 | message = api_result.get('message', '签到成功') 208 | # 限制消息长度 209 | if len(message) > 50: 210 | message = message[:50] + "..." 211 | content_parts.append(f" ✅ [{account_name}] {message}") 212 | else: 213 | content_parts.append(f" ✅ [{account_name}] 签到成功") 214 | else: 215 | error = result.get('error', '未知错误') 216 | # 限制错误消息长度 217 | if len(error) > 50: 218 | error = error[:50] + "..." 219 | content_parts.append(f" ❌ [{account_name}] {error}") 220 | 221 | # 添加执行信息 222 | content_parts.append("") # 空行 223 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 224 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 225 | 226 | content = "\n".join(content_parts) 227 | 228 | # 发送通知 229 | send_notification( 230 | title=title, 231 | content=content, 232 | sound=sound 233 | ) 234 | logger.info(f"✅ {self.site_name}签到推送发送成功") 235 | 236 | except Exception as e: 237 | logger.error(f"❌ {self.site_name}推送通知失败: {str(e)}", exc_info=True) 238 | 239 | 240 | def main(): 241 | """主函数""" 242 | # 记录开始时间 243 | start_time = datetime.now() 244 | print(f"\n{'='*60}") 245 | print(f"## 恩山论坛签到任务开始") 246 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 247 | print(f"{'='*60}\n") 248 | 249 | logger.info("="*60) 250 | logger.info(f"恩山论坛签到任务开始执行 - {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 251 | logger.info("="*60) 252 | 253 | try: 254 | # 创建签到管理器 255 | manager = EnshanSignInManager() 256 | 257 | # 执行所有账号签到 258 | results = manager.sign_in_all_accounts() 259 | 260 | # 记录结束时间 261 | end_time = datetime.now() 262 | duration = (end_time - start_time).total_seconds() 263 | 264 | print(f"\n{'='*60}") 265 | print(f"## 恩山论坛签到任务完成") 266 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 267 | print(f"## 执行耗时: {int(duration)} 秒") 268 | print(f"{'='*60}\n") 269 | 270 | logger.info("="*60) 271 | logger.info(f"恩山论坛签到任务执行完成 - {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 272 | logger.info(f"执行耗时: {int(duration)} 秒") 273 | logger.info("="*60) 274 | 275 | # 发送推送通知 276 | if results: 277 | manager.send_notification(results, start_time, end_time) 278 | 279 | # 统计结果 280 | total_count = len(results) 281 | success_count = sum(1 for r in results if r.get('success')) 282 | failed_count = total_count - success_count 283 | 284 | # 打印总结 285 | print(f"📊 签到总结:") 286 | print(f" ✅ 成功: {success_count} 个账号") 287 | print(f" ❌ 失败: {failed_count} 个账号") 288 | print(f" 📈 总计: {total_count} 个账号\n") 289 | 290 | # 根据结果返回退出码 291 | if failed_count > 0: 292 | return 1 if success_count == 0 else 2 # 1=全部失败, 2=部分失败 293 | return 0 # 全部成功 294 | 295 | except Exception as e: 296 | end_time = datetime.now() 297 | duration = (end_time - start_time).total_seconds() 298 | 299 | logger.error(f"签到任务执行异常: {str(e)}", exc_info=True) 300 | 301 | print(f"\n{'='*60}") 302 | print(f"## ❌ 签到任务执行异常") 303 | print(f"## 错误信息: {str(e)}") 304 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 305 | print(f"## 执行耗时: {int(duration)} 秒") 306 | print(f"{'='*60}\n") 307 | 308 | # 发送错误通知 309 | try: 310 | send_notification( 311 | title=f"恩山论坛签到任务异常 ❌", 312 | content=( 313 | f"❌ 任务执行异常\n" 314 | f"💬 错误信息: {str(e)}\n" 315 | f"⏱️ 执行耗时: {int(duration)}秒\n" 316 | f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}" 317 | ), 318 | sound=NotificationSound.ALARM 319 | ) 320 | except: 321 | pass 322 | 323 | return 1 324 | 325 | 326 | if __name__ == '__main__': 327 | exit_code = main() 328 | sys.exit(exit_code) 329 | 330 | -------------------------------------------------------------------------------- /script/kanxue/sign_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 看雪论坛自动签到脚本 5 | 6 | 功能: 7 | 1. 从token.json配置文件读取账号信息 8 | 2. 支持多账号管理 9 | 3. 自动执行签到并推送通知 10 | 11 | Author: ZaiZaiCat 12 | Date: 2025-01-20 13 | """ 14 | 15 | import json 16 | import logging 17 | import sys 18 | from typing import Dict, Any, List 19 | from datetime import datetime 20 | from pathlib import Path 21 | 22 | # 添加项目根目录到Python路径 23 | project_root = Path(__file__).parent.parent.parent 24 | sys.path.insert(0, str(project_root)) 25 | 26 | from notification import send_notification, NotificationSound 27 | from api import KanxueAPI 28 | 29 | # 配置日志 30 | logging.basicConfig( 31 | level=logging.INFO, 32 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 33 | ) 34 | logger = logging.getLogger(__name__) 35 | 36 | 37 | class KanxueSignInManager: 38 | """看雪论坛签到管理器""" 39 | 40 | def __init__(self, config_path: str = None): 41 | """ 42 | 初始化签到管理器 43 | 44 | Args: 45 | config_path: 配置文件路径,默认为项目根目录下的config/token.json 46 | """ 47 | if config_path is None: 48 | config_path = project_root / "config" / "token.json" 49 | else: 50 | config_path = Path(config_path) 51 | 52 | self.config_path = config_path 53 | self.site_name = "看雪论坛" 54 | self.accounts = [] 55 | self.load_config() 56 | 57 | def load_config(self) -> None: 58 | """加载配置文件""" 59 | try: 60 | logger.info(f"正在读取配置文件: {self.config_path}") 61 | with open(self.config_path, 'r', encoding='utf-8') as f: 62 | config = json.load(f) 63 | 64 | # 获取看雪论坛的配置 65 | kanxue_config = config.get('kanxue', {}) 66 | # 兼容嵌套结构 67 | if 'kanxue' in kanxue_config: 68 | kanxue_config = kanxue_config.get('kanxue', {}) 69 | self.accounts = kanxue_config.get('accounts', []) 70 | 71 | if not self.accounts: 72 | logger.warning("配置文件中没有找到看雪论坛账号信息") 73 | else: 74 | logger.info(f"成功加载 {len(self.accounts)} 个账号配置") 75 | 76 | except FileNotFoundError: 77 | logger.error(f"配置文件不存在: {self.config_path}") 78 | raise 79 | except json.JSONDecodeError as e: 80 | logger.error(f"配置文件JSON格式错误: {e}") 81 | raise 82 | except Exception as e: 83 | logger.error(f"加载配置文件失败: {e}") 84 | raise 85 | 86 | def sign_in_single_account(self, account: Dict[str, Any]) -> Dict[str, Any]: 87 | """ 88 | 单个账号签到 89 | 90 | Args: 91 | account: 账号配置信息 92 | 93 | Returns: 94 | Dict: 签到结果 95 | """ 96 | account_name = account.get('account_name', '未命名账号') 97 | cookie = account.get('cookie', '') 98 | csrf_token = account.get('csrf_token', '') 99 | user_agent = account.get('user_agent') 100 | 101 | logger.info(f"开始执行账号 [{account_name}] 的签到...") 102 | 103 | if not cookie or not csrf_token: 104 | error_msg = "cookie或csrf_token为空" 105 | logger.error(f"账号 [{account_name}] {error_msg}") 106 | return { 107 | 'account_name': account_name, 108 | 'success': False, 109 | 'error': error_msg 110 | } 111 | 112 | try: 113 | # 创建API实例并执行签到 114 | api = KanxueAPI(cookie, csrf_token, user_agent) 115 | result = api.sign_in() 116 | 117 | # 添加账号名称到结果中 118 | result['account_name'] = account_name 119 | 120 | if result.get('success'): 121 | logger.info(f"账号 [{account_name}] 签到成功") 122 | else: 123 | logger.error(f"账号 [{account_name}] 签到失败: {result.get('error', '未知错误')}") 124 | 125 | return result 126 | 127 | except Exception as e: 128 | error_msg = f"签到异常: {str(e)}" 129 | logger.error(f"账号 [{account_name}] {error_msg}", exc_info=True) 130 | return { 131 | 'account_name': account_name, 132 | 'success': False, 133 | 'error': error_msg 134 | } 135 | 136 | def sign_in_all_accounts(self) -> List[Dict[str, Any]]: 137 | """ 138 | 所有账号签到 139 | 140 | Returns: 141 | List[Dict]: 所有账号的签到结果列表 142 | """ 143 | if not self.accounts: 144 | logger.warning("没有可签到的账号") 145 | return [] 146 | 147 | results = [] 148 | for i, account in enumerate(self.accounts, 1): 149 | logger.info(f"\n{'='*60}") 150 | logger.info(f"正在处理第 {i}/{len(self.accounts)} 个账号") 151 | logger.info(f"{'='*60}") 152 | 153 | result = self.sign_in_single_account(account) 154 | results.append(result) 155 | 156 | return results 157 | 158 | def send_notification(self, results: List[Dict[str, Any]], start_time: datetime, end_time: datetime) -> None: 159 | """ 160 | 发送签到结果通知 161 | 162 | Args: 163 | results: 签到结果列表 164 | start_time: 任务开始时间 165 | end_time: 任务结束时间 166 | """ 167 | try: 168 | duration = (end_time - start_time).total_seconds() 169 | 170 | # 统计结果 171 | total_count = len(results) 172 | success_count = sum(1 for r in results if r.get('success')) 173 | failed_count = total_count - success_count 174 | 175 | # 构建通知标题 176 | if failed_count == 0: 177 | title = f"{self.site_name}签到成功 ✅" 178 | sound = NotificationSound.BIRDSONG 179 | elif success_count == 0: 180 | title = f"{self.site_name}签到失败 ❌" 181 | sound = NotificationSound.ALARM 182 | else: 183 | title = f"{self.site_name}签到部分成功 ⚠️" 184 | sound = NotificationSound.BELL 185 | 186 | # 构建通知内容 187 | content_parts = [f"📊 执行统计:"] 188 | 189 | if success_count > 0: 190 | content_parts.append(f"✅ 成功: {success_count} 个账号") 191 | if failed_count > 0: 192 | content_parts.append(f"❌ 失败: {failed_count} 个账号") 193 | 194 | content_parts.append(f"📈 总计: {total_count} 个账号") 195 | content_parts.append("") 196 | 197 | # 添加详细信息 198 | content_parts.append("📝 详情:") 199 | for result in results: 200 | account_name = result.get('account_name', '未知账号') 201 | if result.get('success'): 202 | api_result = result.get('result', {}) 203 | 204 | # 处理看雪论坛的返回格式 205 | if 'code' in api_result: 206 | if api_result.get('code') == '0': 207 | message = api_result.get('message', '') 208 | content_parts.append(f" ✅ [{account_name}] 获得积分: {message}") 209 | else: 210 | message = api_result.get('message', '签到完成') 211 | if len(message) > 50: 212 | message = message[:50] + "..." 213 | content_parts.append(f" ✅ [{account_name}] {message}") 214 | elif 'message' in api_result: 215 | message = api_result.get('message', '签到成功') 216 | if len(message) > 50: 217 | message = message[:50] + "..." 218 | content_parts.append(f" ✅ [{account_name}] {message}") 219 | else: 220 | content_parts.append(f" ✅ [{account_name}] 签到成功") 221 | else: 222 | error = result.get('error', '未知错误') 223 | if len(error) > 50: 224 | error = error[:50] + "..." 225 | content_parts.append(f" ❌ [{account_name}] {error}") 226 | 227 | # 添加执行信息 228 | content_parts.append("") 229 | content_parts.append(f"⏱️ 执行耗时: {int(duration)}秒") 230 | content_parts.append(f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 231 | 232 | content = "\n".join(content_parts) 233 | 234 | # 发送通知 235 | send_notification( 236 | title=title, 237 | content=content, 238 | sound=sound 239 | ) 240 | logger.info(f"✅ {self.site_name}签到推送发送成功") 241 | 242 | except Exception as e: 243 | logger.error(f"❌ {self.site_name}推送通知失败: {str(e)}", exc_info=True) 244 | 245 | 246 | def main(): 247 | """主函数""" 248 | # 记录开始时间 249 | start_time = datetime.now() 250 | print(f"\n{'='*60}") 251 | print(f"## 看雪论坛签到任务开始") 252 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 253 | print(f"{'='*60}\n") 254 | 255 | logger.info("="*60) 256 | logger.info(f"看雪论坛签到任务开始执行 - {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 257 | logger.info("="*60) 258 | 259 | try: 260 | # 创建签到管理器 261 | manager = KanxueSignInManager() 262 | 263 | # 执行所有账号签到 264 | results = manager.sign_in_all_accounts() 265 | 266 | # 记录结束时间 267 | end_time = datetime.now() 268 | duration = (end_time - start_time).total_seconds() 269 | 270 | print(f"\n{'='*60}") 271 | print(f"## 看雪论坛签到任务完成") 272 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 273 | print(f"## 执行耗时: {int(duration)} 秒") 274 | print(f"{'='*60}\n") 275 | 276 | logger.info("="*60) 277 | logger.info(f"看雪论坛签到任务执行完成 - {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 278 | logger.info(f"执行耗时: {int(duration)} 秒") 279 | logger.info("="*60) 280 | 281 | # 发送推送通知 282 | if results: 283 | manager.send_notification(results, start_time, end_time) 284 | 285 | # 统计结果 286 | total_count = len(results) 287 | success_count = sum(1 for r in results if r.get('success')) 288 | failed_count = total_count - success_count 289 | 290 | # 打印总结 291 | print(f"📊 签到总结:") 292 | print(f" ✅ 成功: {success_count} 个账号") 293 | print(f" ❌ 失败: {failed_count} 个账号") 294 | print(f" 📈 总计: {total_count} 个账号\n") 295 | 296 | # 根据结果返回退出码 297 | if failed_count > 0: 298 | return 1 if success_count == 0 else 2 299 | return 0 300 | 301 | except Exception as e: 302 | end_time = datetime.now() 303 | duration = (end_time - start_time).total_seconds() 304 | 305 | logger.error(f"签到任务执行异常: {str(e)}", exc_info=True) 306 | 307 | print(f"\n{'='*60}") 308 | print(f"## ❌ 签到任务执行异常") 309 | print(f"## 错误信息: {str(e)}") 310 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 311 | print(f"## 执行耗时: {int(duration)} 秒") 312 | print(f"{'='*60}\n") 313 | 314 | # 发送错误通知 315 | try: 316 | send_notification( 317 | title=f"看雪论坛签到任务异常 ❌", 318 | content=( 319 | f"❌ 任务执行异常\n" 320 | f"💬 错误信息: {str(e)}\n" 321 | f"⏱️ 执行耗时: {int(duration)}秒\n" 322 | f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}" 323 | ), 324 | sound=NotificationSound.ALARM 325 | ) 326 | except: 327 | pass 328 | 329 | return 1 330 | 331 | 332 | if __name__ == '__main__': 333 | exit_code = main() 334 | sys.exit(exit_code) 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZaiZaiCat-Checkin 🐱 2 | 3 | 自用每日签到脚本,青龙签到脚本 4 | 5 | > **⚠️ 免责声明** 6 | > 本项目中的大量代码由 AI 辅助编写生成,代码规范和格式可能存在不足之处,敬请见谅。 7 | > 本项目仅供学习交流使用,请勿用于商业用途。使用本项目所造成的一切后果由使用者自行承担。 8 | 9 | ## 📋 目录 10 | 11 | - [功能特性](#功能特性) 12 | - [支持平台](#支持平台) 13 | - [项目结构](#项目结构) 14 | - [环境要求](#环境要求) 15 | - [安装部署](#安装部署) 16 | - [配置说明](#配置说明) 17 | - [使用方法](#使用方法) 18 | - [通知推送](#通知推送) 19 | - [常见问题](#常见问题) 20 | - [更新日志](#更新日志) 21 | - [开源协议](#开源协议) 22 | 23 | ## ✨ 功能特性 24 | 25 | - 🚀 **多平台支持** - 集成多个平台的自动签到和任务执行功能 26 | - 👥 **多账号管理** - 每个平台支持配置多个账号,自动循环执行 27 | - 📱 **通知推送** - 集成 多平台 推送,实时获取执行结果 28 | - 🔄 **智能延迟** - 模拟人工操作,避免被检测 29 | - 📝 **详细日志** - 完整的执行日志记录,方便问题排查 30 | - 🎯 **任务管理** - 自动获取并完成各平台的日常任务 31 | - ⚙️ **灵活配置** - 统一的配置文件管理,易于维护 32 | 33 | ## ✅ 脚本可用性状态 34 | 35 | 以下是各平台脚本的当前可用性状态: 36 | 37 | | 平台 | 脚本路径 | 状态 | 说明 | 38 | |--------------|---------|------|---------------------| 39 | | 🚚 顺丰速运 | `script/sf/main.py` | ✅ 可用 | 支持签到和积分任务 | 40 | | 📱 恩山论坛 | `script/enshan/sign_in.py` | ✅ 可用 | 支持每日签到 | 41 | | 🔐 看雪论坛 | `script/kanxue/sign_in.py` | ✅ 可用 | 支持每日签到 | 42 | | 📺 上海杨浦 | `script/shyp/main.py` | ✅ 可用 | 支持任务列表和积分任务 | 43 | | 🏢 华润通-万象星 | `script/huaruntong/999/main.py` | ✅ 可用 | 支持答题签到 | 44 | | 💳 华润通-微信版 | `script/huaruntong/huaruntong_wx/main.py` | ✅ 可用 | 支持签到送积分 | 45 | | 🛒 华润通-Ole' | `script/huaruntong/ole/main.py` | ❌ 不可用 | 需要动态获取微信code换取token | 46 | | 🎯 华润通-文体未来荟 | `script/huaruntong/wentiweilaihui/main.py` | ✅ 可用 | 支持签到和积分查询 | 47 | | 👟 鸿星尔克 | `script/erke/main.py` | ✅ 可用 | 支持签到和积分明细查询 | 48 | | 📝 WPS Office | `script/wps/main.py` | ✅ 可用 | 支持自动签到和抽奖 | 49 | | 💰 什么值得买 | `script/smzdm/sign_daily_task/main.py` | ✅ 可用 | 支持每日签到和众测任务 | 50 | 51 | ### 状态说明 52 | 53 | - ✅ **可用**: 脚本完整且功能正常,可以直接使用 54 | - ⚠️ **部分可用**: 脚本基本可用,但可能存在某些功能限制 55 | - 🚧 **开发中**: 脚本正在开发或测试阶段 56 | - ❌ **不可用**: 脚本存在问题或已废弃 57 | 58 | ### 使用建议 59 | 60 | 1. **推荐使用**: 所有标记为"✅ 可用"的脚本都已经过测试,可以放心使用 61 | 2. **配置要求**: 使用前请确保在 `config/token.json` 中正确配置了相应平台的账号信息 62 | 3. **Cookie 有效期**: 建议定期更新 Cookie 等认证信息,以保证脚本正常运行 63 | 4. **测试建议**: 使用青龙命令拉取脚本可能会出现混乱的问题,建议直接clone整个项目到青龙的 scripts 目录下 64 | 65 | ## 📁 项目结构 66 | 67 | ``` 68 | ZaiZaiCat-Checkin/ 69 | ├── config/ # 配置文件目录 70 | │ └── token.json # 统一的账号配置文件 71 | │ └── notification.json # 统一的推送配置文件 72 | ├── script/ # 脚本目录 73 | │ ├── enshan/ # 恩山论坛 74 | │ │ ├── api.py # API 接口封装 75 | │ │ └── sign_in.py # 签到脚本 76 | │ ├── kanxue/ # 看雪论坛 77 | │ │ ├── api.py 78 | │ │ └── sign_in.py 79 | │ ├── sf/ # 顺丰速运 80 | │ │ ├── api.py 81 | │ │ └── main.py 82 | │ ├── shyp/ # 上海杨浦 83 | │ │ ├── api.py 84 | │ │ ├── main.py 85 | │ │ └── auto_buy.py # 自动抢购脚本 86 | │ ├── huaruntong/ # 华润通 87 | │ ├── 999/ # 万象星 88 | │ ├── huaruntong_wx/ # 微信小程序 89 | │ ├── ole/ # Ole'精品超市 90 | │ └── wentiweilaihui/ # 文体未来荟 91 | │ ├── erke/ # 鸿星尔克 92 | │ ├── api.py 93 | │ └── main.py 94 | │ └── wps/ # WPS Office 95 | │ ├── api.py # API接口和加密模块 96 | │ ├── main.py # 主程序入口 97 | │ ├── README.md # WPS脚本说明文档 98 | │ ├── QUICK_START.md # 快速配置指南 99 | │ ├── CHANGES.md # 修改说明 100 | │ └── test_config.py # 配置测试脚本 101 | ├── notification.py # 通知推送模块 102 | ├── LICENSE # MIT 开源协议 103 | └── README.md # 项目说明文档 104 | ``` 105 | 106 | ## 🔧 环境要求 107 | 108 | - **Python**: 3.7+ (推荐 3.9+) 109 | - **依赖库**: 110 | - `requests` - HTTP 请求库 111 | - `pycryptodome` - 加密库(WPS 签到需要) 112 | - `logging` - 日志记录 113 | - 其他标准库 114 | 115 | ## 📦 安装部署 116 | 117 | ### 1. 克隆项目 118 | 119 | ```bash 120 | git clone https://github.com/Cat-zaizai/ZaiZaiCat-Checkin.git 121 | cd ZaiZaiCat-Checkin 122 | ``` 123 | 124 | ### 2. 安装依赖 125 | 126 | ```bash 127 | # 基础依赖 128 | pip install requests 129 | 130 | # WPS 签到需要的加密库 131 | pip install pycryptodome 132 | ``` 133 | 134 | ### 3. 配置账号信息 135 | 136 | 编辑 `config/token.json` 文件,按照平台添加账号信息。 137 | 138 | ### 4. 青龙面板部署(推荐) 139 | 140 | 1. 将整个项目上传到青龙面板的 `scripts` 目录 141 | 2. 在青龙面板中添加定时任务 142 | 3. 配置环境变量(用于推送通知) 143 | 144 | **定时任务示例**: 145 | ```bash 146 | # 顺丰速运 - 每天 08:00 147 | 0 8 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/sf/main.py 148 | 149 | # 恩山论坛 - 每天 09:00 150 | 0 9 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/enshan/sign_in.py 151 | 152 | # 看雪论坛 - 每天 09:30 153 | 30 9 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/kanxue/sign_in.py 154 | 155 | # 上海杨浦 - 每天 10:00 156 | 0 10 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/shyp/main.py 157 | 158 | # 鸿星尔克 - 每天 08:30 159 | 30 8 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/erke/main.py 160 | 161 | # WPS Office - 每天 07:30 162 | 30 7 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/script/wps/main.py 163 | 164 | # 什么值得买 - 每天 07:00 165 | 0 7 * * * python3 /ql/scripts/ZaiZaiCat-Checkin/smzdm/sign_daily_task/main.py 166 | ``` 167 | 168 | ## ⚙️ 配置说明 169 | 170 | ### 配置文件结构 171 | 172 | `config/token.json` 采用 JSON 格式,按平台分类存储账号信息。 173 | 174 | #### 示例配置 175 | 此处需要运行什么脚本,则去相关脚本文件夹中的md文档查看对应的配置说明。 176 | ```json 177 | { 178 | "sf": { 179 | "accounts": [ 180 | { 181 | "account_name": "账号1", 182 | "cookies": "你的Cookie", 183 | "user_id": "用户ID", 184 | "user_agent": "User-Agent", 185 | "channel": "weixin", 186 | "device_id": "设备ID" 187 | } 188 | ] 189 | }, 190 | "enshan": { 191 | "accounts": [ 192 | { 193 | "account_name": "默认账号", 194 | "cookies": "你的Cookie", 195 | "formhash": "表单hash", 196 | "user_agent": "User-Agent" 197 | } 198 | ] 199 | } 200 | } 201 | ``` 202 | 203 | ### 获取配置信息 204 | 205 | #### 1. Cookie 获取方法 206 | 207 | 1. 使用浏览器打开对应平台网站 208 | 2. 登录你的账号 209 | 3. 按 `F12` 打开开发者工具 210 | 4. 切换到 `Network` 标签 211 | 5. 刷新页面或进行操作 212 | 6. 找到请求,查看 `Request Headers` 中的 `Cookie` 213 | 7. 复制完整的 Cookie 字符串 214 | 215 | #### 2. Token 获取方法 216 | 217 | - 部分平台(如华润通、上海杨浦)使用 Token 认证 218 | - 使用抓包工具(如 Charles、Fiddler)获取 219 | - 或从浏览器开发者工具中的请求头查找 `Authorization` 字段 220 | 221 | #### 3. 其他参数 222 | 223 | - `user_agent`: 从请求头中的 `User-Agent` 字段复制 224 | - `device_id`: 从请求参数或请求头中获取 225 | - `formhash`/`csrf_token`: 从页面源码或请求中提取 226 | 227 | ## 🚀 使用方法 228 | 229 | ### 本地运行 230 | 231 | ```bash 232 | # 运行顺丰速运签到 233 | python3 script/sf/main.py 234 | 235 | # 运行恩山论坛签到 236 | python3 script/enshan/sign_in.py 237 | 238 | # 运行看雪论坛签到 239 | python3 script/kanxue/sign_in.py 240 | 241 | # 运行上海云杨浦任务 242 | python3 script/shyp/main.py 243 | 244 | # 运行鸿星尔克签到 245 | python3 script/erke/main.py 246 | 247 | # 运行什么值得买任务 248 | python3 smzdm/sign_daily_task/main.py 249 | ``` 250 | 251 | ### 青龙面板运行 252 | 253 | 在青龙面板中配置定时任务后,脚本会按照设定的时间自动执行。 254 | 255 | ### 查看日志 256 | 257 | - 脚本执行日志会同时输出到控制台和日志文件 258 | - 日志文件位置:各脚本目录下的 `.log` 文件 259 | - 青龙面板日志:在服务管理中查看执行日志 260 | 261 | ## 📱 通知推送 262 | 263 | 本项目已扩展为支持多平台统一推送(而不仅限于 Bark),通过一个集中配置文件或环境变量进行管理。 264 | 参考项目: [dailycheckin](https://github.com/Sitoi/dailycheckin) 265 | 266 | 支持的推送平台(示例,具体以 `notification.py` 中实现为准): 267 | - Bark 268 | - Server酱 (SCKEY / SENDKEY) 269 | - Server酱 Turbo 270 | - CoolPush 271 | - Qmsg酱 272 | - Telegram 273 | - 飞书 (Feishu) 274 | - 钉钉 (DingTalk) 275 | - 企业微信群机器人 276 | - 企业微信应用消息 277 | - PushPlus 278 | - Gotify 279 | - Ntfy 280 | - PushDeer 281 | 282 | 配置方式(优先级) 283 | 1. `config/notification.json` 中的对应字段(推荐用于本地与容器持久化配置) 284 | 2. 环境变量(适用于青龙面板或临时覆盖) 285 | 286 | 配置文件位置 287 | - 文件路径:`config/notification.json`(已新增) 288 | 289 | 如何使用 290 | - 编辑 `config/notification.json` 添加或修改推送服务的配置(推荐) 291 | - 或在部署环境中通过环境变量设置对应字段(示例见下) 292 | - 在脚本中通过 `from notification import send_notification` 调用统一发送接口: 293 | 294 | 示例: 295 | ```python 296 | from notification import send_notification 297 | send_notification("签到结果", "账号 A: 成功\n账号 B: 失败") 298 | ``` 299 | 300 | 示例配置(`config/notification.json`) 301 | ```json 302 | { 303 | "bark": { 304 | "push": "https://api.day.app/your_bark_key_or_url", 305 | "icon": "", 306 | "sound": "birdsong", 307 | "group": "ZaiZaiCat-Checkin", 308 | "level": "active", 309 | "url": "" 310 | }, 311 | "server": { 312 | "sckey": "", 313 | "sendkey": "" 314 | }, 315 | "pushplus": { 316 | "token": "", 317 | "topic": "" 318 | }, 319 | "pushdeer": { 320 | "pushkey": "", 321 | "url": "https://api2.pushdeer.com/message/push", 322 | "type": "text" 323 | }, 324 | "gotify": { 325 | "url": "", 326 | "token": "", 327 | "priority": "3" 328 | }, 329 | "ntfy": { 330 | "url": "https://ntfy.sh", 331 | "topic": "", 332 | "priority": "3" 333 | } 334 | } 335 | ``` 336 | 337 | 常用环境变量(根据不同服务的字段名称)示例 338 | - BARK_PUSH 339 | - SCKEY / SENDKEY 340 | - PUSHPLUS_TOKEN 341 | - PUSHDEER_PUSHKEY (或 PUSHDEER_PUSHKEY) 342 | - GOTIFY_URL / GOTIFY_TOKEN 343 | - NTFY_TOPIC / NTFY_URL 344 | - QMSG_KEY 345 | - TG_BOT_TOKEN / TG_USER_ID 346 | - FSKEY 347 | - DINGTALK_ACCESS_TOKEN / DINGTALK_SECRET 348 | - QYWX_KEY / QYWX_CORPID / QYWX_AGENTID / QYWX_CORPSECRET / QYWX_TOUSER 349 | 350 | 说明与注意事项 351 | - 推荐把敏感字段(如 pushkey、token、sckey)放在环境变量或不提交到仓库的配置文件中。 352 | - `notification.py` 的配置读取优先级为:配置文件 > 环境变量 > 默认值。 353 | - PushDeer 支持两种使用方式: 354 | - 官方在线版:无需自架,使用 `https://api2.pushdeer.com/message/push` 并在 PushDeer 客户端创建 Key;保持 `pushdeer.url` 为默认即可。 355 | - 自架服务端:将 `pushdeer.url` 指向你的服务地址(例如 `https://your-server.example/push`)。 356 | - 不同推送服务的消息格式与支持的特性(图片、Markdown 等)不同,`notification.py` 中会根据各平台的要求做适配。 357 | 358 | 示例:通过 PushDeer 发送 Markdown 类型消息时,在 `config/notification.json` 中把 `pushdeer.type` 设置为 `markdown`,并在调用 `send_notification` 时把内容设置为 Markdown 格式。 359 | 360 | 兼容性与扩展 361 | - 本文档描述的是当前版本支持的平台。如需添加新的推送渠道,可在 `notification.py` 中添加相应的加载、启用检测和发送函数,并在 `config/notification.json` 中加入配置。 362 | 363 | ## ❓ 常见问题 364 | 365 | ### 1. Cookie 失效怎么办? 366 | 367 | Cookie 有有效期限制,失效后需要重新获取并更新配置文件。建议定期检查更新。 368 | 369 | ### 2. 签到失败如何排查? 370 | 371 | 1. 检查 Cookie 是否过期 372 | 2. 查看日志文件中的错误信息 373 | 3. 确认账号是否正常(未被封禁) 374 | 4. 检查网络连接是否正常 375 | 376 | ### 3. 如何添加新账号? 377 | 378 | 在 `config/token.json` 对应平台的 `accounts` 数组中添加新的账号对象即可。 379 | 380 | ### 4. 如何禁用某个平台? 381 | 382 | - 方法1: 删除 `config/token.json` 中对应平台的配置 383 | - 方法2: 在青龙面板中禁用对应的定时任务 384 | 385 | ### 5. 推送通知没有收到? 386 | 387 | 1. 检查 `BARK_PUSH` 环境变量是否配置正确 388 | 2. 确认 Bark App 已正确安装和配置 389 | 3. 检查网络连接是否正常 390 | 4. 查看脚本日志中的推送相关信息 391 | 392 | ### 6. 脚本执行报错怎么办? 393 | 394 | 1. 查看完整的错误日志 395 | 2. 检查 Python 版本和依赖库是否安装 396 | 3. 确认配置文件格式是否正确 397 | 4. 检查文件权限是否正确 398 | 399 | ## 📝 更新日志 400 | 401 | ### 2025-12-18 402 | - ✨ **WPS脚本新增抽奖相关任务**: 403 | - 🎟️ 支持自动参与每日抽奖 404 | - 🎁 支持自动领取抽奖奖励 405 | - 📝 更新 WPS 脚本说明文档,加入抽奖功能使用说明 406 | 407 | ### 2025-12-08 v2.0 408 | - ✨ **新增什么值得买(SMZDM)任务模块**: 完整的自动化任务执行系统 409 | - 📅 **每日签到**: 自动完成每日签到,获取积分奖励 410 | - 🎯 **众测任务**: 自动执行众测相关任务 411 | - 浏览文章任务自动完成 412 | - 互动任务自动处理 413 | - 智能任务奖励领取 414 | - 🎯 **互动任务**: 全面的用户互动任务支持 415 | - 自动浏览指定文章 416 | - 智能关注用户任务 417 | - 自动领取任务奖励 418 | - 👥 **多账号管理**: 支持配置多个账号并行执行 419 | - 📊 **详细统计**: 完整的任务执行统计和结果汇总 420 | - 📝 **完善日志**: 详细的执行日志记录和错误追踪 421 | 422 | ### 2025-12-08 423 | - ✨ 支持多平台统一推送:在 `notification.py` 中新增 多平台推送 支持,并将各平台推送整合到统一接口 `send_notification`。 424 | - 🛠 新增推送配置文件:`config/notification.json`(支持从文件或环境变量加载配置,优先级:文件 > 环境 > 默认)。 425 | - 📝 更新 `README.md` 的“通知推送”文档,加入 使用说明、配置示例和常用环境变量说明。 426 | 427 | ### 2025-12-01 428 | - ✨ 新增 WPS Office 自动签到脚本 429 | - 📝 更新项目说明文档 430 | 431 | ### 2025-11-28 432 | - ✨ 新增鸿星尔克签到脚本 433 | - ✨ 支持鸿星尔克积分明细查询功能 434 | - ❌ Ole'精品超市脚本设为不可用(需要动态获取微信code换取token) 435 | - 📝 更新项目说明文档 436 | 437 | ### 2025-11-24 438 | - ✨ 新增顺丰速运签到脚本 439 | - ✨ 新增恩山论坛签到脚本 440 | - ✨ 新增看雪论坛签到脚本 441 | - ✨ 新增上海杨浦任务脚本 442 | - ✨ 新增华润通多个子平台支持 443 | - ✨ ✨ 999 签到功能完善 444 | - ✨ ✨ 华润通微信小程序签到功能完善 445 | - ✨ ✨ Ole' 精品超市签到功能(已废弃) 446 | - ✨ ✨ 文体未来荟签到功能完善 447 | - ✨ 创建项目 README 文档 448 | - 📝 完善项目说明和使用指南 449 | 450 | 451 | ## 🤝 贡献指南 452 | 453 | 欢迎提交 Issue 和 Pull Request! 454 | 455 | 456 | ## ⚠️ 注意事项 457 | 458 | 1. 本项目仅供学习交流使用,请勿用于商业用途 459 | 2. 使用本项目所造成的一切后果由使用者自行承担 460 | 3. 请合理使用自动签到功能,避免对平台造成负担 461 | 4. Cookie 等敏感信息请妥善保管,不要泄露给他人 462 | 5. 定期更新 Cookie,避免失效影响使用 463 | 6. 代码由 AI 辅助生成,可能存在不规范之处 464 | 465 | ## 📄 开源协议 466 | 467 | 本项目采用 [MIT License](LICENSE) 开源协议。 468 | 469 | ## 🙏 致谢 470 | 471 | - 感谢所有为本项目提供帮助和支持的朋友 472 | - 感谢青龙面板提供的自动化平台 473 | 474 | ## 📧 联系方式 475 | 476 | 如有问题或建议,欢迎通过以下方式联系: 477 | 478 | - GitHub Issues: [提交问题](https://github.com/Cat-zaizai/ZaiZaiCat-Checkin/issues) 479 | - Email: wusan503@gmail.com 480 | 481 | --- 482 | 483 | **⭐ 如果这个项目对你有帮助,欢迎给个 Star!** 484 | 485 | *最后更新: 2025-12-08* 486 | -------------------------------------------------------------------------------- /script/sf/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 顺丰快递积分任务自动化脚本 5 | 6 | 功能: 7 | 1. 从token.json配置文件读取账号信息 8 | 2. 支持多账号管理 9 | 3. 自动执行签到和积分任务 10 | 4. 推送执行结果通知 11 | 12 | Author: ZaiZaiCat 13 | Date: 2025-01-20 14 | """ 15 | 16 | import json 17 | import logging 18 | import sys 19 | import time 20 | import random 21 | from typing import List, Dict, Any 22 | from datetime import datetime 23 | from pathlib import Path 24 | 25 | # 添加项目根目录到Python路径 26 | project_root = Path(__file__).parent.parent.parent 27 | sys.path.insert(0, str(project_root)) 28 | 29 | from notification import send_notification, NotificationSound 30 | 31 | # 导入API模块(当前目录) 32 | from api import SFExpressAPI 33 | 34 | # 延迟时间常量配置 (秒) 35 | DELAY_BETWEEN_ACCOUNTS = (3, 8) # 账号间切换延迟 36 | DELAY_AFTER_SIGN = (2, 5) # 签到后延迟 37 | DELAY_BETWEEN_TASKS = (10, 15) # 任务间延迟 38 | 39 | # 配置日志 40 | logging.basicConfig( 41 | level=logging.INFO, 42 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 43 | ) 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | class SFTasksManager: 48 | """顺丰积分任务管理器""" 49 | 50 | def __init__(self, config_path: str = None): 51 | """ 52 | 初始化任务管理器 53 | 54 | Args: 55 | config_path: 配置文件路径,默认为项目根目录下的config/token.json 56 | """ 57 | if config_path is None: 58 | config_path = project_root / "config" / "token.json" 59 | else: 60 | config_path = Path(config_path) 61 | 62 | self.config_path = config_path 63 | self.site_name = "顺丰速运" 64 | self.accounts = [] 65 | self.task_summary = [] 66 | self.load_config() 67 | 68 | def load_config(self) -> None: 69 | """加载配置文件""" 70 | try: 71 | logger.info(f"正在读取配置文件: {self.config_path}") 72 | with open(self.config_path, 'r', encoding='utf-8') as f: 73 | config = json.load(f) 74 | 75 | # 获取顺丰的配置 76 | sf_config = config.get('sf', {}) 77 | self.accounts = sf_config.get('accounts', []) 78 | 79 | if not self.accounts: 80 | logger.warning("配置文件中没有找到顺丰账号信息") 81 | else: 82 | logger.info(f"成功加载 {len(self.accounts)} 个账号配置") 83 | 84 | except FileNotFoundError: 85 | logger.error(f"配置文件不存在: {self.config_path}") 86 | raise 87 | except json.JSONDecodeError as e: 88 | logger.error(f"配置文件JSON格式错误: {e}") 89 | raise 90 | except Exception as e: 91 | logger.error(f"加载配置文件失败: {e}") 92 | raise 93 | 94 | def get_task_list(self, sf_api: SFExpressAPI) -> List[Dict[str, Any]]: 95 | """ 96 | 获取顺丰积分任务列表 97 | 98 | Args: 99 | sf_api: SF API实例 100 | 101 | Returns: 102 | List[Dict[str, Any]]: 任务列表 103 | """ 104 | try: 105 | result = sf_api.query_point_task_and_sign() 106 | task_list = result.get("obj", {}).get("taskTitleLevels", []) 107 | logger.info(f"获取到 {len(task_list)} 个任务") 108 | return task_list 109 | except Exception as e: 110 | logger.error(f"获取任务列表失败: {e}") 111 | return [] 112 | 113 | def auto_sign_and_fetch_package(self, sf_api: SFExpressAPI, account_name: str) -> Dict[str, Any]: 114 | """ 115 | 自动签到并获取礼包 116 | 117 | Args: 118 | sf_api: SF API实例 119 | account_name: 账号名称 120 | 121 | Returns: 122 | Dict[str, Any]: 签到结果,包含成功状态和连续签到天数 123 | """ 124 | try: 125 | logger.info(f"[{account_name}] 开始执行自动签到获取礼包...") 126 | result = sf_api.automatic_sign_fetch_package() 127 | 128 | if result.get("success"): 129 | obj = result.get("obj", {}) 130 | has_finish_sign = obj.get("hasFinishSign", 0) 131 | count_day = obj.get("countDay", 0) 132 | package_list = obj.get("integralTaskSignPackageVOList", []) 133 | 134 | if has_finish_sign == 1: 135 | logger.info(f"[{account_name}] 今日已完成签到,连续签到 {count_day} 天") 136 | else: 137 | logger.info(f"[{account_name}] 签到成功!连续签到 {count_day} 天") 138 | 139 | # 记录获得的礼包 140 | if package_list: 141 | logger.info(f"[{account_name}] 获得签到礼包:") 142 | for package in package_list: 143 | package_name = package.get("commodityName", "未知礼包") 144 | invalid_date = package.get("invalidDate", "") 145 | logger.info(f"[{account_name}] - {package_name} (有效期至: {invalid_date})") 146 | else: 147 | logger.info(f"[{account_name}] 未获得签到礼包") 148 | 149 | return {'success': True, 'days': count_day, 'already_signed': has_finish_sign == 1} 150 | else: 151 | error_msg = result.get("errorMessage", "未知错误") 152 | logger.warning(f"[{account_name}] 签到失败: {error_msg}") 153 | return {'success': False, 'days': 0, 'error': error_msg} 154 | 155 | except Exception as e: 156 | logger.error(f"[{account_name}] 自动签到时发生错误: {e}") 157 | return {'success': False, 'days': 0, 'error': str(e)} 158 | 159 | def process_single_task(self, task: Dict[str, Any], sf_api: SFExpressAPI, account_name: str) -> Dict[str, Any]: 160 | """ 161 | 处理单个任务 162 | 163 | Args: 164 | task: 任务信息 165 | sf_api: SF API实例 166 | account_name: 账号名称 167 | 168 | Returns: 169 | Dict[str, Any]: 任务执行结果 170 | """ 171 | task_title = task.get('title', '未知任务') 172 | task_status = task.get("status") 173 | task_code = task.get('taskCode') 174 | 175 | if not task_code: 176 | logger.warning(f"[{account_name}] 任务 {task_title} 缺少任务代码,跳过") 177 | return {'title': task_title, 'success': False, 'points': 0} 178 | 179 | try: 180 | finish_result = sf_api.finish_task(task_code) 181 | if finish_result and finish_result.get('success'): 182 | logger.info(f"[{account_name}] 任务 {task_title} 完成成功") 183 | 184 | # 获取任务奖励 185 | reward_result = sf_api.fetch_tasks_reward() 186 | logger.info(f"[{account_name}] 任务奖励获取结果: {reward_result}") 187 | 188 | # 提取获得的积分 189 | points = 0 190 | if reward_result and reward_result.get('success'): 191 | obj_list = reward_result.get('obj', []) 192 | if isinstance(obj_list, list): 193 | for item in obj_list: 194 | points += item.get('point', 0) 195 | 196 | return {'title': task_title, 'success': True, 'points': points} 197 | else: 198 | logger.warning(f"[{account_name}] 任务 {task_title} 完成失败或无返回结果") 199 | return {'title': task_title, 'success': False, 'points': 0} 200 | except Exception as e: 201 | logger.error(f"[{account_name}] 执行任务 {task_title} 时发生错误: {e}") 202 | return {'title': task_title, 'success': False, 'points': 0} 203 | 204 | def process_account_tasks(self, account: Dict[str, Any]) -> Dict[str, Any]: 205 | """ 206 | 处理单个账号的所有任务 207 | 208 | Args: 209 | account: 账号信息 210 | 211 | Returns: 212 | Dict[str, Any]: 账号任务执行统计 213 | """ 214 | cookies = account.get("cookies", "") 215 | device_id = account.get("device_id", "") 216 | user_id = account.get("user_id", "") 217 | user_agent = account.get("user_agent", "") 218 | channel = account.get("channel", "") 219 | account_name = account.get("account_name", user_id) 220 | 221 | # 初始化账号统计 222 | account_stat = { 223 | 'account_name': account_name, 224 | 'sign_success': False, 225 | 'sign_days': 0, 226 | 'total_tasks': 0, 227 | 'completed_tasks': 0, 228 | 'total_points': 0, 229 | 'tasks': [] 230 | } 231 | 232 | if not all([cookies, user_id]): 233 | logger.error(f"账号 {account_name} 配置信息不完整,跳过处理") 234 | account_stat['error'] = '配置信息不完整' 235 | return account_stat 236 | 237 | logger.info(f"开始处理账号: {account_name}") 238 | 239 | try: 240 | # 创建API实例 241 | sf_api = SFExpressAPI( 242 | cookies=cookies, 243 | device_id=device_id, 244 | user_id=user_id, 245 | user_agent=user_agent, 246 | channel=channel 247 | ) 248 | 249 | # 首先执行自动签到获取礼包 250 | sign_result = self.auto_sign_and_fetch_package(sf_api, account_name) 251 | account_stat['sign_success'] = sign_result.get('success', False) 252 | account_stat['sign_days'] = sign_result.get('days', 0) 253 | 254 | # 签到后稍作延时 255 | sign_delay = random.uniform(*DELAY_AFTER_SIGN) 256 | logger.info(f"[{account_name}] 签到完成,延时 {sign_delay:.2f} 秒后继续任务...") 257 | time.sleep(sign_delay) 258 | 259 | # 获取任务列表 260 | task_list = self.get_task_list(sf_api) 261 | 262 | if not task_list: 263 | logger.warning(f"[{account_name}] 未获取到任务列表") 264 | return account_stat 265 | 266 | logger.info(f"[{account_name}] 获取到 {len(task_list)} 个任务") 267 | 268 | # 处理每个任务 269 | for i, task in enumerate(task_list, 1): 270 | logger.info(f"[{account_name}] 开始处理第 {i}/{len(task_list)} 个任务") 271 | 272 | if task.get("taskPeriod") != "D": 273 | logger.info(f"[{account_name}] 任务 {task.get('title', '未知任务')} 非日常任务,跳过") 274 | continue 275 | 276 | account_stat['total_tasks'] += 1 277 | 278 | # 如果任务已完成,跳过 279 | if task.get("status") == 3: 280 | logger.info(f"[{account_name}] 任务 {task.get('title', '未知任务')} 已完成,跳过") 281 | continue 282 | 283 | delay_time = random.uniform(*DELAY_BETWEEN_TASKS) 284 | logger.info(f"[{account_name}] 准备执行任务 {task.get('title', '未知任务')},延时 {delay_time:.2f} 秒...") 285 | time.sleep(delay_time) 286 | 287 | task_result = self.process_single_task(task, sf_api, account_name) 288 | account_stat['tasks'].append(task_result) 289 | 290 | if task_result.get('success'): 291 | account_stat['completed_tasks'] += 1 292 | account_stat['total_points'] += task_result.get('points', 0) 293 | 294 | except Exception as e: 295 | logger.error(f"处理账号 {account_name} 时发生错误: {e}") 296 | account_stat['error'] = str(e) 297 | 298 | return account_stat 299 | 300 | def run_all_accounts(self) -> None: 301 | """执行所有账号的任务处理""" 302 | if not self.accounts: 303 | logger.warning("没有配置的账号,程序退出") 304 | return 305 | 306 | logger.info(f"开始执行任务,共 {len(self.accounts)} 个账号") 307 | 308 | for i, account in enumerate(self.accounts, 1): 309 | logger.info(f"\n{'='*60}") 310 | logger.info(f"处理第 {i}/{len(self.accounts)} 个账号") 311 | logger.info(f"{'='*60}") 312 | 313 | account_stat = self.process_account_tasks(account) 314 | self.task_summary.append(account_stat) 315 | logger.info(f"账号 {i} 处理完成") 316 | 317 | # 账号间添加延时,避免频繁切换 318 | if i < len(self.accounts): 319 | account_delay = random.uniform(*DELAY_BETWEEN_ACCOUNTS) 320 | logger.info(f"账号切换延时 {account_delay:.2f} 秒...") 321 | time.sleep(account_delay) 322 | 323 | logger.info("所有账号任务处理完成") 324 | 325 | def send_notification(self, start_time: datetime, end_time: datetime) -> None: 326 | """ 327 | 发送任务执行汇总推送通知 328 | 329 | Args: 330 | start_time: 任务开始时间 331 | end_time: 任务结束时间 332 | """ 333 | try: 334 | duration = (end_time - start_time).total_seconds() 335 | 336 | # 计算总体统计 337 | total_accounts = len(self.task_summary) 338 | total_sign_success = sum(1 for stat in self.task_summary if stat.get('sign_success')) 339 | total_completed = sum(stat.get('completed_tasks', 0) for stat in self.task_summary) 340 | total_points = sum(stat.get('total_points', 0) for stat in self.task_summary) 341 | 342 | # 构建推送标题 343 | title = f"{self.site_name}积分任务完成 ✅" 344 | 345 | # 构建推送内容 346 | content_parts = [ 347 | f"📊 总体统计", 348 | f"━━━━━━━━━━━━━━━━", 349 | f"👥 账号数量: {total_accounts}个", 350 | f"✅ 签到成功: {total_sign_success}/{total_accounts}", 351 | f"📝 完成任务: {total_completed}个", 352 | f"🎁 获得积分: {total_points}分", 353 | f"⏱️ 执行耗时: {int(duration)}秒", 354 | "", 355 | f"📋 账号详情", 356 | f"━━━━━━━━━━━━━━━━" 357 | ] 358 | 359 | # 添加每个账号的详细信息 360 | for i, stat in enumerate(self.task_summary, 1): 361 | account_name = stat.get('account_name', f'账号{i}') 362 | sign_days = stat.get('sign_days', 0) 363 | completed = stat.get('completed_tasks', 0) 364 | points = stat.get('total_points', 0) 365 | 366 | # 账号摘要 367 | if stat.get('error'): 368 | content_parts.append(f"❌ [{account_name}] 执行失败") 369 | content_parts.append(f" 错误: {stat['error']}") 370 | else: 371 | sign_status = "✅" if stat.get('sign_success') else "❌" 372 | content_parts.append(f"{sign_status} [{account_name}]") 373 | content_parts.append(f" 📅 连续签到: {sign_days}天") 374 | content_parts.append(f" 📝 完成任务: {completed}个") 375 | content_parts.append(f" 🎁 获得积分: {points}分") 376 | 377 | # 账号之间添加空行 378 | if i < len(self.task_summary): 379 | content_parts.append("") 380 | 381 | # 添加完成时间 382 | content_parts.append("━━━━━━━━━━━━━━━━") 383 | content_parts.append(f"🕐 {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 384 | 385 | content = "\n".join(content_parts) 386 | 387 | # 发送推送 388 | send_notification( 389 | title=title, 390 | content=content, 391 | sound=NotificationSound.BIRDSONG 392 | ) 393 | logger.info(f"✅ {self.site_name}任务汇总推送发送成功") 394 | 395 | except Exception as e: 396 | logger.error(f"❌ 发送任务汇总推送失败: {str(e)}", exc_info=True) 397 | 398 | 399 | def main(): 400 | """主函数""" 401 | # 记录开始时间 402 | start_time = datetime.now() 403 | print(f"\n{'='*60}") 404 | print(f"## 顺丰快递积分任务开始") 405 | print(f"## 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 406 | print(f"{'='*60}\n") 407 | 408 | logger.info("="*60) 409 | logger.info(f"顺丰快递积分任务开始执行 - {start_time.strftime('%Y-%m-%d %H:%M:%S')}") 410 | logger.info("="*60) 411 | 412 | try: 413 | # 创建任务管理器 414 | manager = SFTasksManager() 415 | 416 | # 执行所有账号的任务 417 | manager.run_all_accounts() 418 | 419 | # 记录结束时间 420 | end_time = datetime.now() 421 | duration = (end_time - start_time).total_seconds() 422 | 423 | print(f"\n{'='*60}") 424 | print(f"## 顺丰快递积分任务完成") 425 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 426 | print(f"## 执行耗时: {int(duration)} 秒") 427 | print(f"{'='*60}\n") 428 | 429 | logger.info("="*60) 430 | logger.info(f"顺丰快递积分任务执行完成 - {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 431 | logger.info(f"执行耗时: {int(duration)} 秒") 432 | logger.info("="*60) 433 | 434 | # 发送推送通知 435 | if manager.task_summary: 436 | manager.send_notification(start_time, end_time) 437 | 438 | return 0 439 | 440 | except Exception as e: 441 | end_time = datetime.now() 442 | duration = (end_time - start_time).total_seconds() 443 | 444 | logger.error(f"任务执行异常: {str(e)}", exc_info=True) 445 | 446 | print(f"\n{'='*60}") 447 | print(f"## ❌ 任务执行异常") 448 | print(f"## 错误信息: {str(e)}") 449 | print(f"## 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") 450 | print(f"## 执行耗时: {int(duration)} 秒") 451 | print(f"{'='*60}\n") 452 | 453 | # 发送错误通知 454 | try: 455 | send_notification( 456 | title=f"顺丰快递积分任务异常 ❌", 457 | content=( 458 | f"❌ 任务执行异常\n" 459 | f"💬 错误信息: {str(e)}\n" 460 | f"⏱️ 执行耗时: {int(duration)}秒\n" 461 | f"🕐 完成时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}" 462 | ), 463 | sound=NotificationSound.ALARM 464 | ) 465 | except: 466 | pass 467 | 468 | return 1 469 | 470 | 471 | if __name__ == '__main__': 472 | exit_code = main() 473 | sys.exit(exit_code) 474 | -------------------------------------------------------------------------------- /script/smzdm/sign_daily_task/service.py: -------------------------------------------------------------------------------- 1 | """ 2 | 什么值得买业务逻辑服务模块 3 | 功能:处理所有业务逻辑,协调API调用 4 | 版本:2.0 5 | """ 6 | 7 | import logging 8 | import time 9 | from typing import Dict, Any, List, Optional 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class SmzdmService: 15 | """什么值得买业务服务类 - 处理所有业务逻辑""" 16 | 17 | def __init__(self, api): 18 | """ 19 | 初始化业务服务 20 | 21 | Args: 22 | api: SmzdmAPI实例 23 | """ 24 | self.api = api 25 | 26 | # ==================== 数据解析相关 ==================== 27 | 28 | def parse_interactive_tasks(self, task_data: Dict[str, Any]) -> List[Dict[str, Any]]: 29 | """ 30 | 解析互动任务数据,提取所有任务列表 31 | 32 | Args: 33 | task_data: 从API获取的任务数据 34 | 35 | Returns: 36 | 任务列表 37 | """ 38 | all_tasks = [] 39 | 40 | row = task_data.get('rows', [])[0] 41 | if not row: 42 | logger.warning("互动任务数据中没有找到任务行") 43 | return all_tasks 44 | 45 | cell_data = row.get('cell_data', {}) 46 | activity_task = cell_data.get('activity_task', {}) 47 | 48 | # 获取累计任务列表 49 | accumulate_list = activity_task.get('accumulate_list', {}) 50 | task_list_v2 = accumulate_list.get('task_list_v2', []) 51 | 52 | # 遍历每个模块的任务列表 53 | if task_list_v2: 54 | module = task_list_v2[0] 55 | task_list = module.get('task_list', []) 56 | logger.info(f"发现{len(task_list)} 个每日任务") 57 | return task_list 58 | else: 59 | logger.warning("互动任务数据中没有找到任务列表") 60 | return [] 61 | 62 | def print_energy_info(self, user_data: Dict[str, Any]): 63 | """ 64 | 打印用户能量值信息 65 | 66 | Args: 67 | user_data: 用户数据字典 68 | """ 69 | my_energy = user_data.get('my_energy', {}) 70 | my_energy_total = my_energy.get('my_energy_total', 0) 71 | energy_expired_time = my_energy.get('energy_expired_time', '未知') 72 | win_coupon_total = my_energy.get('win_conpou_total', 0) 73 | 74 | logger.info(f"\n 💎 能量值信息:") 75 | logger.info(f" 当前能量值: {my_energy_total}") 76 | logger.info(f" 过期时间: {energy_expired_time}") 77 | logger.info(f" 已兑换必中券: {win_coupon_total} 张") 78 | 79 | # 显示可兑换的必中券列表 80 | exchange_info = user_data.get('exchange_win_coupon', {}) 81 | win_coupon_list = exchange_info.get('win_coupon_list', []) 82 | 83 | if win_coupon_list: 84 | logger.info(f"\n 🎫 可兑换必中券列表:") 85 | for coupon in win_coupon_list: 86 | coupon_name = coupon.get('article_title', '未知') 87 | coupon_energy = coupon.get('article_energy_total', 0) 88 | coupon_desc = coupon.get('article_subtitle', '') 89 | 90 | # 判断能量值是否足够兑换 91 | can_exchange = "✅" if my_energy_total >= coupon_energy else "❌" 92 | logger.info(f" {can_exchange} {coupon_name} - 需要{coupon_energy}能量值 ({coupon_desc})") 93 | 94 | # ==================== 众测任务业务逻辑 ==================== 95 | 96 | def execute_task(self, task: Dict[str, Any]) -> bool: 97 | """ 98 | 根据任务类型执行对应的任务(众测任务) 99 | 100 | Args: 101 | task: 任务信息字典 102 | 103 | Returns: 104 | 是否成功 105 | """ 106 | task_id = task.get('task_id', '') 107 | task_name = task.get('task_name', '未知任务') 108 | task_event_type = task.get('task_event_type', '') 109 | task_status = task.get('task_status', 0) 110 | channel_id = task.get('channel_id', 0) 111 | article_id = task.get('article_id', '') 112 | 113 | # 任务状态: 0-未开始, 1-进行中, 2-未完成, 3-已完成, 4-已领取 114 | if task_status == 4: 115 | logger.info(f"任务 [{task_name}] 已领取奖励,跳过") 116 | return True 117 | elif task_status == 3: 118 | # 已完成未领取,尝试领取奖励 119 | logger.info(f"任务 [{task_name}] 已完成,尝试领取奖励...") 120 | return self.api.receive_reward(task_id) 121 | 122 | logger.info(f"开始执行任务: {task_name} (类型: {task_event_type})") 123 | 124 | # 根据任务类型执行不同的操作 125 | if task_event_type == "interactive.view.article": 126 | # 浏览文章任务 127 | 128 | return self.api.view_article_task(task_id, article_id, channel_id, task_event_type) 129 | 130 | elif task_event_type == "interactive.favorite": 131 | # 收藏文章任务 132 | redirect_url = task.get('task_redirect_url', {}) 133 | article_link_val = redirect_url.get('link_val', '') 134 | 135 | if not article_link_val: 136 | logger.warning(f"任务 [{task_name}] 缺少文章ID,跳过") 137 | return False 138 | 139 | return self.api.favorite_article_task(task_id, article_link_val) 140 | 141 | elif task_event_type == "interactive.rating": 142 | # 点赞文章任务 143 | redirect_url = task.get('task_redirect_url', {}) 144 | article_link_val = redirect_url.get('link_val', '') 145 | 146 | if not article_link_val: 147 | logger.warning(f"任务 [{task_name}] 缺少文章ID,跳过") 148 | return False 149 | 150 | return self.api.rating_article_task(task_id, article_link_val) 151 | 152 | elif task_event_type == "guide.apply_zhongce": 153 | # 申请众测任务 154 | return self.execute_apply_zhongce_task(task) 155 | 156 | elif task_event_type == "interactive.share": 157 | # 分享众测招募任务 158 | return self.execute_share_task(task) 159 | 160 | else: 161 | logger.warning(f"未知任务类型: {task_event_type}") 162 | return False 163 | 164 | def execute_share_task(self, task: Dict[str, Any]) -> bool: 165 | """ 166 | 执行分享众测招募任务 167 | 168 | Args: 169 | task: 任务信息字典 170 | 171 | Returns: 172 | 是否成功 173 | """ 174 | task_name = task.get('task_name', '未知任务') 175 | task_finished_num = task.get('task_finished_num', 0) 176 | task_even_num = task.get('task_even_num', 0) 177 | 178 | # 计算还需要分享的次数 179 | remaining_count = task_even_num - task_finished_num 180 | 181 | if remaining_count <= 0: 182 | logger.info(f"任务 [{task_name}] 已完成所有分享 ({task_finished_num}/{task_even_num})") 183 | return True 184 | 185 | logger.info(f"任务 [{task_name}] 需要分享 {remaining_count} 次 (已完成 {task_finished_num}/{task_even_num})") 186 | 187 | # 获取众测列表 188 | probation_list = self.api.get_probation_list(status='all') 189 | if not probation_list: 190 | logger.error("获取众测列表失败,无法完成分享任务") 191 | return False 192 | 193 | # 提取可分享的众测商品信息 194 | available_shares = [] 195 | for item in probation_list: 196 | article_id = item.get('article_id', '') 197 | article_channel_id = item.get('article_channel_id', '') 198 | article_title = item.get('article_title', '未知商品') 199 | 200 | if article_id and article_channel_id: 201 | available_shares.append({ 202 | 'article_id': article_id, 203 | 'channel_id': article_channel_id, 204 | 'title': article_title 205 | }) 206 | 207 | if not available_shares: 208 | logger.warning("当前没有可分享的众测商品") 209 | return False 210 | 211 | logger.info(f"找到 {len(available_shares)} 个可分享的众测商品") 212 | 213 | # 开始分享 214 | success_count = 0 215 | for i, share_item in enumerate(available_shares): 216 | if success_count >= remaining_count: 217 | break 218 | 219 | logger.info(f" [{i+1}] 分享众测商品: {share_item['title']}") 220 | 221 | # 执行分享 222 | if self.api.share_probation_task(share_item['article_id'], share_item['channel_id']): 223 | success_count += 1 224 | logger.info(f" ✅ 分享成功 (进度: {success_count}/{remaining_count})") 225 | else: 226 | logger.info(f" ❌ 分享失败") 227 | 228 | # 分享间隔 229 | if success_count < remaining_count: 230 | time.sleep(2) 231 | 232 | logger.info(f"分享任务完成,成功分享 {success_count} 次") 233 | return success_count > 0 234 | 235 | def execute_apply_zhongce_task(self, task: Dict[str, Any]) -> bool: 236 | """ 237 | 执行申请众测任务 238 | 239 | Args: 240 | task: 任务信息字典 241 | 242 | Returns: 243 | 是否成功 244 | """ 245 | task_name = task.get('task_name', '未知任务') 246 | task_finished_num = task.get('task_finished_num', 0) 247 | task_even_num = task.get('task_even_num', 0) 248 | 249 | # 计算还需要申请的次数 250 | remaining_count = task_even_num - task_finished_num 251 | 252 | if remaining_count <= 0: 253 | logger.info(f"任务 [{task_name}] 已完成所有申请 ({task_finished_num}/{task_even_num})") 254 | return True 255 | 256 | logger.info(f"任务 [{task_name}] 需要申请 {remaining_count} 次 (已完成 {task_finished_num}/{task_even_num})") 257 | 258 | # 获取众测列表 259 | probation_list = self.api.get_probation_list() 260 | if not probation_list: 261 | logger.error("获取众测列表失败,无法完成申请任务") 262 | return False 263 | 264 | # 过滤出可申请的众测商品 265 | available_probations = [] 266 | for item in probation_list: 267 | article_probation = item.get('article_probation', {}) 268 | product_status = article_probation.get('product_status', '') 269 | 270 | # product_status == "1" 表示可申请 271 | if product_status == '1': 272 | article_id = item.get('article_id', '') 273 | article_title = item.get('article_title', '未知商品') 274 | apply_num = article_probation.get('apply_num', '') 275 | product_num = article_probation.get('product_num', '') 276 | product_status_name = article_probation.get('product_status_name', '') 277 | 278 | available_probations.append({ 279 | 'id': article_id, 280 | 'title': article_title, 281 | 'apply_num': apply_num, 282 | 'product_num': product_num, 283 | 'status_name': product_status_name 284 | }) 285 | 286 | if not available_probations: 287 | logger.warning("当前没有可申请的众测商品") 288 | return False 289 | 290 | logger.info(f"找到 {len(available_probations)} 个可申请的众测商品") 291 | 292 | # 开始申请 293 | success_count = 0 294 | for i, probation in enumerate(available_probations): 295 | if success_count >= remaining_count: 296 | break 297 | 298 | logger.info(f" [{i+1}] {probation['title']} - {probation['apply_num']} - {probation['status_name']}") 299 | 300 | # 提交申请 301 | if self.api.submit_probation_apply(probation['id']): 302 | success_count += 1 303 | logger.info(f" ✅ 申请成功 (进度: {success_count}/{remaining_count})") 304 | else: 305 | logger.info(f" ⏭️ 跳过该商品") 306 | 307 | # 申请间隔 308 | if success_count < remaining_count: 309 | time.sleep(1) 310 | 311 | logger.info(f"众测申请任务完成,成功申请 {success_count} 次") 312 | return success_count > 0 313 | 314 | # ==================== 互动任务业务逻辑 ==================== 315 | 316 | def execute_interactive_task(self, task: Dict[str, Any]) -> bool: 317 | """ 318 | 执行互动任务 319 | 320 | Args: 321 | task: 任务信息字典 322 | 323 | Returns: 324 | 是否成功 325 | """ 326 | task_id = task.get('task_id', '') 327 | task_name = task.get('task_name', '未知任务') 328 | task_event_type = task.get('task_event_type', '') 329 | task_status = task.get('task_status', '0') 330 | task_finished_num = int(task.get('task_finished_num', 0)) 331 | task_even_num = int(task.get('task_even_num', 0)) 332 | module_name = task.get('module_name', '未知模块') 333 | 334 | # 任务状态: "2"-未完成, "3"-已完成, "4"-已领取 335 | if task_status == '4': 336 | logger.info(f"[{module_name}] 任务 [{task_name}] 已领取奖励,跳过") 337 | return True 338 | 339 | # 检查任务是否已完成 340 | if task_finished_num >= task_even_num: 341 | logger.info(f"[{module_name}] 任务 [{task_name}] 已完成 ({task_finished_num}/{task_even_num})") 342 | return True 343 | 344 | logger.info(f"[{module_name}] 开始执行任务: {task_name} (类型: {task_event_type}, 进度: {task_finished_num}/{task_even_num})") 345 | 346 | # 根据任务类型执行不同的操作 347 | if task_event_type == "interactive.view.article": 348 | # 浏览文章任务 349 | article_id = task.get('article_id', '') 350 | channel_id = task.get('channel_id', '0') 351 | 352 | if not article_id or article_id == '0': 353 | logger.warning(f"任务 [{task_name}] 缺少文章ID,跳过") 354 | return False 355 | 356 | # 如果channel_id为0或未提供,尝试通过article_id获取 357 | if not channel_id or channel_id == '0': 358 | fetched_channel_id = self.api.get_article_channel_id(article_id) 359 | if fetched_channel_id is not None: 360 | channel_id = str(fetched_channel_id) 361 | else: 362 | logger.warning(f"任务 [{task_name}] 无法获取channel_id,使用默认值") 363 | channel_id = '3' # 默认频道ID 364 | 365 | return self.api.view_article_task(task_id, article_id, channel_id, task_event_type) 366 | 367 | elif task_event_type == "interactive.follow.user": 368 | # 关注用户任务 369 | logger.warning(f"任务 [{task_name}] 类型为关注用户,暂不支持自动执行") 370 | return False 371 | 372 | elif task_event_type == "interactive.comment": 373 | # 评论任务 374 | logger.warning(f"任务 [{task_name}] 类型为评论,暂不支持自动执行") 375 | return False 376 | 377 | elif task_event_type in ["publish.baoliao_new", "publish.biji_new", "publish.yuanchuang_new", "publish.zhuanzai"]: 378 | # 发布类任务(爆料、笔记、原创、推荐) 379 | logger.warning(f"任务 [{task_name}] 类型为发布内容,暂不支持自动执行") 380 | return False 381 | 382 | else: 383 | logger.warning(f"未知任务类型: {task_event_type}") 384 | return False 385 | 386 | # ==================== 关注用户业务逻辑 ==================== 387 | 388 | def execute_follow_task(self, max_follow_count: int = 5) -> Dict[str, int]: 389 | """ 390 | 执行关注任务(关注用户后立即取消关注) 391 | 392 | Args: 393 | max_follow_count: 最大关注用户数量,默认为5 394 | 395 | Returns: 396 | 执行统计字典 {success: 成功数, fail: 失败数} 397 | """ 398 | logger.info(f"开始执行关注任务,最大关注用户数: {max_follow_count}") 399 | 400 | success_count = 0 401 | fail_count = 0 402 | 403 | try: 404 | # 获取用户列表 405 | user_data = self.api.get_follow_user_list() 406 | if not user_data: 407 | logger.error("获取用户列表失败") 408 | return {'success': 0, 'fail': 1} 409 | 410 | # 解析用户列表 411 | rows = user_data.get('rows', []) 412 | if not rows: 413 | logger.warning("用户列表为空") 414 | return {'success': 0, 'fail': 1} 415 | 416 | logger.info(f"获取到 {len(rows)} 个用户") 417 | 418 | processed_count = 0 419 | for user_row in rows: 420 | if processed_count >= max_follow_count: 421 | break 422 | 423 | # 提取用户信息 424 | article_title = user_row.get('article_title', '') 425 | user_id = user_row.get('keyword_id', '') 426 | 427 | if not article_title or not user_id: 428 | logger.warning(f"用户信息不完整,跳过: {user_row}") 429 | continue 430 | 431 | logger.info(f" [{processed_count + 1}] 处理用户: {article_title}") 432 | 433 | # 执行关注 434 | if self.api.follow_user(article_title, user_id): 435 | logger.info(f" ✅ 关注成功") 436 | 437 | # 等待一下再取消关注 438 | time.sleep(2) 439 | 440 | # 取消关注 441 | if self.api.unfollow_user(article_title, user_id): 442 | logger.info(f" ✅ 取消关注成功") 443 | success_count += 1 444 | else: 445 | logger.info(f" ❌ 取消关注失败") 446 | fail_count += 1 447 | else: 448 | logger.info(f" ❌ 关注失败") 449 | fail_count += 1 450 | 451 | processed_count += 1 452 | 453 | # 处理间隔 454 | if processed_count < max_follow_count: 455 | time.sleep(3) 456 | 457 | logger.info(f"关注任务执行完成: 成功 {success_count} 个, 失败 {fail_count} 个") 458 | return {'success': success_count, 'fail': fail_count} 459 | 460 | except Exception as e: 461 | logger.error(f"执行关注任务时发生错误: {str(e)}") 462 | return {'success': success_count, 'fail': fail_count + 1} 463 | 464 | # ==================== 每日签到业务逻辑 ==================== 465 | 466 | def print_checkin_info(self, checkin_data: Dict[str, Any]): 467 | """ 468 | 打印签到信息 469 | 470 | Args: 471 | checkin_data: 签到返回的数据字典 472 | """ 473 | # 提取签到信息 474 | cpadd = checkin_data.get('cpadd', 0) # 本次新增积分 475 | daily_num = checkin_data.get('daily_num', 0) # 连续签到天数 476 | cpoints = checkin_data.get('cpoints', 0) # 当前积分 477 | cexperience = checkin_data.get('cexperience', 0) # 当前经验值 478 | cgold = checkin_data.get('cgold', 0) # 当前金币余额 479 | cprestige = checkin_data.get('cprestige', 0) # 声望值 480 | slogan = checkin_data.get('slogan', '') # 个性签名 481 | lottery_type = checkin_data.get('lottery_type', '') # 抽奖类型 482 | pre_re_silver = int(checkin_data.get('pre_re_silver', 0)) # 上次获得的银币 483 | 484 | logger.info(f"\n 📅 签到成功!") 485 | logger.info(f" " + "="*50) 486 | 487 | # 签到基本信息 488 | logger.info(f" 📊 签到统计:") 489 | logger.info(f" • 连续签到: {daily_num} 天") 490 | 491 | 492 | # 账户余额信息 493 | logger.info(f"\n 💰 账户余额:") 494 | logger.info(f" • 当前积分: {cpoints}") 495 | logger.info(f" • 当前金币: {cgold}") 496 | logger.info(f" • 当前经验: {cexperience}") 497 | logger.info(f" • 声望值: {cprestige}") 498 | 499 | # 抽奖信息 500 | if lottery_type: 501 | logger.info(f"\n 🎰 抽奖信息:") 502 | logger.info(f" • 抽奖类型: {lottery_type}") 503 | if pre_re_silver > 0: 504 | logger.info(f" • 上次银币奖励: {pre_re_silver}") 505 | 506 | # 个性签名 507 | if slogan: 508 | logger.info(f"\n 💭 个性签名: {slogan}") 509 | 510 | logger.info(f" " + "="*50) 511 | -------------------------------------------------------------------------------- /script/shyp/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 上海云媒体 API Interface 5 | 上海云媒体积分任务查询接口 6 | 7 | Author: Assistant 8 | Date: 2025-11-06 9 | """ 10 | 11 | import requests 12 | import json 13 | import logging 14 | from typing import Dict, Any, Optional 15 | 16 | 17 | class ShypAPI: 18 | """上海云媒体API接口类""" 19 | 20 | def __init__(self, token: str, device_id: str, site_id: str = "310110", user_agent: str = None): 21 | """ 22 | 初始化上海云媒体 API 23 | 24 | Args: 25 | token: 用户token 26 | device_id: 设备ID 27 | site_id: 站点ID,默认为"310110" 28 | user_agent: 用户代理字符串 29 | """ 30 | self.base_url = "https://app.ypmedia.cn" 31 | self.session = requests.Session() 32 | self.token = token 33 | self.device_id = device_id 34 | self.site_id = site_id 35 | self.logger = logging.getLogger(__name__) 36 | self.user_agent = user_agent or "okhttp/4.10.0" 37 | 38 | # 默认请求头 39 | self.default_headers = { 40 | "User-Agent": self.user_agent, 41 | "Accept-Encoding": "gzip", 42 | "Content-Type": "application/json", 43 | "log-header": "I am the log request header.", 44 | "deviceid": self.device_id, 45 | "siteid": self.site_id, 46 | "token": self.token, 47 | "content-type": "application/json; charset=UTF-8" 48 | } 49 | 50 | def _make_request(self, method: str, endpoint: str, data: Dict = None, 51 | headers: Dict = None) -> Optional[Dict[str, Any]]: 52 | """ 53 | 发送HTTP请求的通用方法 54 | 55 | Args: 56 | method: 请求方法 (GET, POST等) 57 | endpoint: API端点 58 | data: 请求数据 59 | headers: 额外的请求头 60 | 61 | Returns: 62 | Dict: API响应结果,失败返回None 63 | """ 64 | url = f"{self.base_url}{endpoint}" 65 | request_headers = self.default_headers.copy() 66 | if headers: 67 | request_headers.update(headers) 68 | 69 | try: 70 | self.logger.debug(f"发送{method}请求: {url}") 71 | self.logger.debug(f"请求数据: {json.dumps(data, ensure_ascii=False)}") 72 | 73 | response = self.session.request( 74 | method=method, 75 | url=url, 76 | json=data, 77 | headers=request_headers, 78 | timeout=30 79 | ) 80 | 81 | self.logger.debug(f"响应状态码: {response.status_code}") 82 | 83 | response.raise_for_status() 84 | result = response.json() 85 | 86 | self.logger.debug(f"响应结果: {json.dumps(result, ensure_ascii=False)[:200]}...") 87 | 88 | return result 89 | 90 | except requests.exceptions.Timeout: 91 | self.logger.error(f"请求超时: {url}") 92 | return None 93 | except requests.exceptions.RequestException as e: 94 | self.logger.error(f"请求失败: {url}, 错误: {str(e)}") 95 | return None 96 | except json.JSONDecodeError as e: 97 | self.logger.error(f"JSON解析失败: {str(e)}") 98 | return None 99 | except Exception as e: 100 | self.logger.error(f"未知错误: {str(e)}") 101 | return None 102 | 103 | def get_score_info(self, order_by: str = "release_desc", 104 | request_type: str = "2") -> Optional[Dict[str, Any]]: 105 | """ 106 | 获取积分信息和任务列表 107 | 108 | Args: 109 | order_by: 排序方式,默认为"release_desc" 110 | request_type: 请求类型,默认为"2" 111 | 112 | Returns: 113 | Dict: 包含积分信息、任务列表、签到信息等,失败返回None 114 | 115 | Response Example: 116 | { 117 | "code": 0, 118 | "msg": "success", 119 | "data": { 120 | "signTitle": "已经连续签到 1 天", 121 | "totalScore": 526, 122 | "increaseScore": 526, 123 | "reduceScore": 0, 124 | "todayPoint": 526, 125 | "todayIncreasePoint": 526, 126 | "todayReducePoint": 0, 127 | "jobs": [...], 128 | "signs": [...], 129 | "mallSetting": {...} 130 | } 131 | } 132 | """ 133 | endpoint = "/media-basic-port/api/app/personal/score/info" 134 | 135 | data = { 136 | "orderBy": order_by, 137 | "requestType": request_type, 138 | "siteId": self.site_id 139 | } 140 | 141 | self.logger.info("正在获取积分信息和任务列表...") 142 | result = self._make_request("POST", endpoint, data=data) 143 | 144 | if result and result.get("code") == 0: 145 | self.logger.info("成功获取积分信息") 146 | data = result.get("data", {}) 147 | self.logger.info(f"总积分: {data.get('totalScore')}, 今日积分: {data.get('todayPoint')}") 148 | return result 149 | else: 150 | msg = result.get("msg", "未知错误") if result else "请求失败" 151 | self.logger.error(f"获取积分信息失败: {msg}") 152 | return None 153 | 154 | def parse_task_list(self, score_info: Dict[str, Any]) -> Dict[str, Any]: 155 | """ 156 | 解析任务列表,提取关键信息 157 | 158 | Args: 159 | score_info: get_score_info返回的完整信息 160 | 161 | Returns: 162 | Dict: 包含解析后的任务信息 163 | { 164 | "total_score": 总积分, 165 | "today_point": 今日积分, 166 | "sign_status": 签到状态信息, 167 | "incomplete_tasks": 未完成的任务列表, 168 | "completed_tasks": 已完成的任务列表, 169 | "all_tasks": 所有任务列表 170 | } 171 | """ 172 | if not score_info or score_info.get("code") != 0: 173 | self.logger.warning("无效的积分信息,无法解析任务列表") 174 | return {} 175 | 176 | data = score_info.get("data", {}) 177 | jobs = data.get("jobs", []) 178 | signs = data.get("signs", []) 179 | 180 | # 分类任务 181 | incomplete_tasks = [] 182 | completed_tasks = [] 183 | 184 | for job in jobs: 185 | task_info = { 186 | "id": job.get("id"), 187 | "title": job.get("title"), 188 | "summary": job.get("summary"), 189 | "status": job.get("status"), 190 | "progress": job.get("progress"), 191 | "total_progress": job.get("totalProgress"), 192 | "all_progress": job.get("allProgress") 193 | } 194 | 195 | if job.get("status") == "1": # 已完成 196 | completed_tasks.append(task_info) 197 | else: # 未完成 198 | incomplete_tasks.append(task_info) 199 | 200 | # 解析签到状态 201 | sign_status = { 202 | "sign_title": data.get("signTitle"), 203 | "today_signed": any(s.get("status") == "signed" for s in signs), 204 | "signs": signs 205 | } 206 | 207 | result = { 208 | "total_score": data.get("totalScore"), 209 | "today_point": data.get("todayPoint"), 210 | "today_increase_point": data.get("todayIncreasePoint"), 211 | "sign_status": sign_status, 212 | "incomplete_tasks": incomplete_tasks, 213 | "completed_tasks": completed_tasks, 214 | "all_tasks": jobs 215 | } 216 | 217 | self.logger.info(f"任务统计 - 已完成: {len(completed_tasks)}, 未完成: {len(incomplete_tasks)}") 218 | 219 | return result 220 | 221 | def check_token_validity(self) -> bool: 222 | """ 223 | 检查token是否有效 224 | 225 | Returns: 226 | bool: token有效返回True,否则返回False 227 | """ 228 | self.logger.info("正在检查token有效性...") 229 | result = self.get_score_info() 230 | 231 | if result and result.get("code") == 0: 232 | self.logger.info("Token有效") 233 | return True 234 | else: 235 | self.logger.error("Token无效或已过期") 236 | return False 237 | 238 | def get_article_list(self, channel_id: str = "a978f44b3e284e5e86777f9d4e3be7bb", 239 | page_no: int = 1, page_size: int = 10, 240 | order_by: str = "release_desc", 241 | request_type: str = "1") -> Optional[Dict[str, Any]]: 242 | """ 243 | 获取文章列表 244 | 245 | Args: 246 | channel_id: 频道ID,默认为推荐频道 247 | page_no: 页码,默认为1 248 | page_size: 每页数量,默认为10 249 | order_by: 排序方式,默认为"release_desc" 250 | request_type: 请求类型,默认为"1" 251 | 252 | Returns: 253 | Dict: 文章列表数据,失败返回None 254 | """ 255 | endpoint = "/media-basic-port/api/app/news/content/list" 256 | 257 | data = { 258 | "channel": {"id": channel_id}, 259 | "pageNo": page_no, 260 | "pageSize": page_size, 261 | "orderBy": order_by, 262 | "requestType": request_type, 263 | "siteId": self.site_id 264 | } 265 | 266 | self.logger.info(f"正在获取文章列表 (页码: {page_no}, 每页: {page_size})...") 267 | result = self._make_request("POST", endpoint, data=data) 268 | 269 | if result and result.get("code") == 0: 270 | data = result.get("data", {}) 271 | total_count = data.get("totalCount", 0) 272 | records = data.get("records", []) 273 | self.logger.info(f"成功获取文章列表,共 {len(records)} 篇文章") 274 | return result 275 | else: 276 | msg = result.get("msg", "未知错误") if result else "请求失败" 277 | self.logger.error(f"获取文章列表失败: {msg}") 278 | return None 279 | 280 | def increase_read_count(self, article_id: str, count_type: str = "contentRead", 281 | order_by: str = "release_desc", 282 | request_type: str = "1") -> Optional[Dict[str, Any]]: 283 | """ 284 | 增加文章阅读计数 285 | 286 | Args: 287 | article_id: 文章ID 288 | count_type: 计数类型,默认为"contentRead" 289 | order_by: 排序方式,默认为"release_desc" 290 | request_type: 请求类型,默认为"1" 291 | 292 | Returns: 293 | Dict: API响应结果,失败返回None 294 | """ 295 | endpoint = "/media-basic-port/api/app/common/count/usage/inc" 296 | 297 | data = { 298 | "countType": count_type, 299 | "id": article_id, 300 | "orderBy": order_by, 301 | "requestType": request_type, 302 | "siteId": self.site_id 303 | } 304 | 305 | self.logger.debug(f"正在增加文章阅读计数: {article_id}") 306 | result = self._make_request("POST", endpoint, data=data) 307 | 308 | if result and result.get("code") == 0: 309 | self.logger.debug("成功增加阅读计数") 310 | return result 311 | else: 312 | msg = result.get("msg", "未知错误") if result else "请求失败" 313 | self.logger.warning(f"增加阅读计数失败: {msg}") 314 | return None 315 | 316 | def complete_read_task(self, order_by: str = "release_desc", 317 | request_type: str = "1") -> Optional[Dict[str, Any]]: 318 | """ 319 | 完成阅读任务(提交阅读积分) 320 | 321 | Args: 322 | order_by: 排序方式,默认为"release_desc" 323 | request_type: 请求类型,默认为"1" 324 | 325 | Returns: 326 | Dict: API响应结果,失败返回None 327 | """ 328 | endpoint = "/media-basic-port/api/app/points/read/add" 329 | 330 | data = { 331 | "orderBy": order_by, 332 | "requestType": request_type, 333 | "siteId": self.site_id 334 | } 335 | 336 | self.logger.debug("正在提交阅读任务...") 337 | result = self._make_request("POST", endpoint, data=data) 338 | 339 | if result and result.get("code") == 0: 340 | self.logger.info("✅ 阅读任务完成") 341 | return result 342 | else: 343 | msg = result.get("msg", "未知错误") if result else "请求失败" 344 | self.logger.warning(f"完成阅读任务失败: {msg}") 345 | return None 346 | 347 | def get_video_list(self, channel_id: str = "d7036c2839e047b48fe64bc36987650c", 348 | page_no: int = 1, page_size: int = 10, 349 | order_by: str = "release_desc", 350 | request_type: str = "1") -> Optional[Dict[str, Any]]: 351 | """ 352 | 获取视频列表 353 | 354 | Args: 355 | channel_id: 频道ID,默认为短视频频道 356 | page_no: 页码,默认为1 357 | page_size: 每页数量,默认为10 358 | order_by: 排序方式,默认为"release_desc" 359 | request_type: 请求类型,默认为"1" 360 | 361 | Returns: 362 | Dict: 视频列表数据,失败返回None 363 | """ 364 | endpoint = "/media-basic-port/api/app/news/content/list" 365 | 366 | data = { 367 | "channel": {"id": channel_id}, 368 | "pageNo": page_no, 369 | "pageSize": page_size, 370 | "orderBy": order_by, 371 | "requestType": request_type, 372 | "siteId": self.site_id 373 | } 374 | 375 | self.logger.info(f"正在获取视频列表 (页码: {page_no}, 每页: {page_size})...") 376 | result = self._make_request("POST", endpoint, data=data) 377 | 378 | if result and result.get("code") == 0: 379 | data = result.get("data", {}) 380 | records = data.get("records", []) 381 | self.logger.info(f"成功获取视频列表,共 {len(records)} 个视频") 382 | return result 383 | else: 384 | msg = result.get("msg", "未知错误") if result else "请求失败" 385 | self.logger.error(f"获取视频列表失败: {msg}") 386 | return None 387 | 388 | def get_video_detail(self, video_id: str, order_by: str = "release_desc", 389 | request_type: str = "1") -> Optional[Dict[str, Any]]: 390 | """ 391 | 获取视频详情 392 | 393 | Args: 394 | video_id: 视频ID 395 | order_by: 排序方式,默认为"release_desc" 396 | request_type: 请求类型,默认为"1" 397 | 398 | Returns: 399 | Dict: 视频详情数据,失败返回None 400 | """ 401 | endpoint = "/media-basic-port/api/app/multimedia/drama/get" 402 | 403 | data = { 404 | "id": video_id, 405 | "orderBy": order_by, 406 | "requestType": request_type, 407 | "siteId": self.site_id 408 | } 409 | 410 | self.logger.debug(f"正在获取视频详情: {video_id}") 411 | result = self._make_request("POST", endpoint, data=data) 412 | 413 | if result and result.get("code") == 0: 414 | self.logger.debug("成功获取视频详情") 415 | return result 416 | else: 417 | msg = result.get("msg", "未知错误") if result else "请求失败" 418 | self.logger.warning(f"获取视频详情失败: {msg}") 419 | return None 420 | 421 | def complete_video_task(self, order_by: str = "release_desc", 422 | request_type: str = "1") -> Optional[Dict[str, Any]]: 423 | """ 424 | 完成视频任务(提交视频积分) 425 | 426 | Args: 427 | order_by: 排序方式,默认为"release_desc" 428 | request_type: 请求类型,默认为"1" 429 | 430 | Returns: 431 | Dict: API响应结果,失败返回None 432 | """ 433 | endpoint = "/media-basic-port/api/app/points/video/add" 434 | 435 | data = { 436 | "orderBy": order_by, 437 | "requestType": request_type, 438 | "siteId": self.site_id 439 | } 440 | 441 | self.logger.debug("正在提交视频任务...") 442 | result = self._make_request("POST", endpoint, data=data) 443 | 444 | if result and result.get("code") == 0: 445 | self.logger.info("✅ 视频任务完成") 446 | return result 447 | else: 448 | msg = result.get("msg", "未知错误") if result else "请求失败" 449 | self.logger.warning(f"完成视频任务失败: {msg}") 450 | return None 451 | 452 | def favor_content(self, content_id: str) -> Optional[Dict[str, Any]]: 453 | """ 454 | 收藏内容 455 | 456 | Args: 457 | content_id: 内容ID 458 | 459 | Returns: 460 | Dict: API响应结果,失败返回None 461 | """ 462 | endpoint = "/media-basic-port/api/app/news/content/favor" 463 | 464 | data = { 465 | "id": content_id 466 | } 467 | 468 | self.logger.debug(f"正在收藏内容: {content_id}") 469 | result = self._make_request("POST", endpoint, data=data) 470 | 471 | if result and result.get("code") == 0: 472 | self.logger.info("✅ 收藏成功") 473 | return result 474 | else: 475 | msg = result.get("msg", "未知错误") if result else "请求失败" 476 | self.logger.warning(f"收藏失败: {msg}") 477 | return None 478 | 479 | def disfavor_content(self, content_id: str) -> Optional[Dict[str, Any]]: 480 | """ 481 | 取消收藏内容 482 | 483 | Args: 484 | content_id: 内容ID 485 | 486 | Returns: 487 | Dict: API响应结果,失败返回None 488 | """ 489 | endpoint = "/media-basic-port/api/app/news/content/disfavor" 490 | 491 | data = { 492 | "id": content_id 493 | } 494 | 495 | self.logger.debug(f"正在取消收藏: {content_id}") 496 | result = self._make_request("POST", endpoint, data=data) 497 | 498 | if result and result.get("code") == 0: 499 | self.logger.debug("取消收藏成功") 500 | return result 501 | else: 502 | msg = result.get("msg", "未知错误") if result else "请求失败" 503 | self.logger.warning(f"取消收藏失败: {msg}") 504 | return None 505 | 506 | def add_comment(self, target_id: str, content: str, 507 | target_type: str = "content") -> Optional[Dict[str, Any]]: 508 | """ 509 | 添加评论 510 | 511 | Args: 512 | target_id: 目标ID(文章ID) 513 | content: 评论内容 514 | target_type: 目标类型,默认为"content" 515 | 516 | Returns: 517 | Dict: API响应结果,失败返回None 518 | """ 519 | endpoint = "/media-basic-port/api/app/common/comment/add" 520 | 521 | data = { 522 | "displayResources": [], 523 | "content": content, 524 | "targetType": target_type, 525 | "targetId": target_id 526 | } 527 | 528 | self.logger.debug(f"正在评论内容: {target_id}, 评论: {content}") 529 | result = self._make_request("POST", endpoint, data=data) 530 | 531 | if result and result.get("code") == 0: 532 | self.logger.info("✅ 评论成功") 533 | return result 534 | else: 535 | msg = result.get("msg", "未知错误") if result else "请求失败" 536 | self.logger.warning(f"评论失败: {msg}") 537 | return None 538 | 539 | def complete_share_task(self, order_by: str = "release_desc", 540 | request_type: str = "1") -> Optional[Dict[str, Any]]: 541 | """ 542 | 完成分享任务(提交分享积分) 543 | 544 | Args: 545 | order_by: 排序方式,默认为"release_desc" 546 | request_type: 请求类型,默认为"1" 547 | 548 | Returns: 549 | Dict: API响应结果,失败返回None 550 | """ 551 | endpoint = "/media-basic-port/api/app/points/share/add" 552 | 553 | data = { 554 | "orderBy": order_by, 555 | "requestType": request_type, 556 | "siteId": self.site_id 557 | } 558 | 559 | self.logger.debug("正在提交分享任务...") 560 | result = self._make_request("POST", endpoint, data=data) 561 | 562 | if result and result.get("code") == 0: 563 | self.logger.info("✅ 分享任务完成") 564 | return result 565 | else: 566 | msg = result.get("msg", "未知错误") if result else "请求失败" 567 | self.logger.warning(f"完成分享任务失败: {msg}") 568 | return None 569 | 570 | --------------------------------------------------------------------------------