├── .gitignore ├── .npmrc ├── README.md ├── deprecated ├── .gitkeep ├── ql_check_url_update.ts ├── ql_dkl.ts ├── ql_jwyb.ts ├── ql_sysxc.ts └── ql_ws_jwcn_card_sign.ts ├── package.json ├── ql_60s.ts ├── ql_ModifySendNotify.js ├── ql_aicnn.ts ├── ql_aima.ts ├── ql_alipan-clean.ts ├── ql_alipan_signin.ts ├── ql_chml.ts ├── ql_gujing.ts ├── ql_hl.ts ├── ql_huluwa.ts ├── ql_identical.ts ├── ql_ikuuu.ts ├── ql_imaotai.ts ├── ql_install_whistle.x-scripts.ts ├── ql_ipzan_signin.ts ├── ql_iqiyi.ts ├── ql_jsbaxfls.ts ├── ql_ssone.ts ├── ql_thsSignIn.ts ├── ql_videoqq.ts ├── ql_weather.ts ├── ql_xmlySign.ts ├── ql_youzan-liteapp.ts ├── ql_ysfqd.ts ├── sample └── lzwme_ql_config.json5 ├── todo ├── .gitkeep ├── ql_v2free.ts └── update-readme.mjs ├── tsconfig.json └── utils ├── Env.ts ├── common.ts └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # build dist 5 | .nyc_output 6 | *.log 7 | /logs 8 | /dist/ 9 | /cjs/ 10 | /esm/ 11 | /release/ 12 | /debug/ 13 | /coverage/ 14 | /docs/ 15 | /tmp 16 | /.tmp 17 | /cache 18 | /.cache 19 | 20 | # manager 21 | npm-debug.log 22 | package-lock.json 23 | yarn.lock 24 | yarn-error.log 25 | pnpm-lock.yaml 26 | .pnpm-debug.log 27 | 28 | # config 29 | /.flh.config.js 30 | /lzwme_ql_config.* 31 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | registry="https://registry.npmmirror.com" 3 | 4 | # for pnpm 5 | strict-peer-dependencies=false 6 | shamefully-hoist=true 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 支持青龙面板的脚本集 2 | 3 | ## 免责说明 4 | 5 | - 本项目发布的脚本及其中涉及的任何逆向解密分析内容,仅用于个人测试和学习研究,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关。 6 | - 请自行评估使用本项目内容可能产生的安全风险,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。本人对使用本项目涉及的任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失或损害。 7 | - 间接使用脚本的任何用户,包括但不限于建立 VPS 或在某些行为违反国家/地区法律或相关法规的情况下进行传播, 本人对于由此引起的任何隐私泄漏或其他后果概不负责。 8 | - 本项目内所有资源文件,禁止任何公众号、自媒体进行任何形式的转载、发布。 9 | - 如果任何单位或个人认为该项目的脚本可能涉嫌侵犯其权利,则应及时通知并提供身份证明,所有权证明,我们将在收到认证文件后删除相关脚本。 10 | - 任何以任何方式查看此项目的人或直接或间接使用该项目的任何脚本的使用者都应仔细阅读此声明。本人保留随时更改或补充此免责声明的权利。一旦使用并复制了任何相关脚本或 Script 项目的规则,则视为您已接受此免责声明。 11 | - **请在下载本项目并完成学习研究后立即予以删除全部内容。** 12 | - 您使用或者复制了本仓库制作的任何脚本,则视为 **已接受** 以上声明,请仔细阅读。 13 | 14 | ## 安装 15 | 16 | ### 命令行方式 17 | 18 | ```bash 19 | # ql repo 20 | ql repo https://github.com/lzwme/ql-scripts.git "ql_|ql-" "backup|todo|deprecated" "utils" "" "js ts" 21 | cd /ql/scripts 22 | pnpm add @lzwme/fe-utils commander enquirer moment json5 crypto-js axios 23 | ``` 24 | 25 | ### 面板方式 26 | 27 | `订阅管理 -> 创建订阅`,表单填写参考: 28 | 29 | - 名称:`lzwme/ql-scripts` 30 | - 链接:`https://github.com/lzwme/ql-scripts.git` 31 | - 分支:`main` 32 | - 定时:`0 0 1 * * *` 33 | - 白名单:`ql_|ql-` 34 | - 黑名单:`backup|todo|deprecated` 35 | - 依赖文件:`utils` 36 | - 文件后缀:`js ts` 37 | 38 | 依赖管理 -> `nodejs` 类型依赖添加:`@lzwme/fe-utils axios commander console-log-colors crypto-js enquirer json5 moment` 39 | 40 | ## 配置 41 | 42 | 青龙面板 `配置文件` -> 编辑 `config.sh` 文件,搜索 `RepoFileExtensions`,增加 `ts` 配置。参考: 43 | 44 | ```bash 45 | RepoFileExtensions="ts js py" 46 | ``` 47 | 48 | 各脚本的具体配置,可参考具体脚本内注释说明进行设置。 49 | 50 | ### 通用环境变量 51 | 52 | - `process.env.LZWME_QL_NOTIFY_TYPE` 配置通知策略: 53 | - 0 - 关闭通知 54 | - 1 - 仅发送异常时通知。`默认值` 55 | - 2 - 全通知 56 | 57 | ### 脚本变量快速自动获取与更新至青龙面板的方法参考 58 | 59 | 一些脚本的认证参数有效期较短,频繁的手动更新比较麻烦。下面介绍一种基于 `whistle` 代理工具及插件 `@lzwme/whistle.x-scripts` 编写规则,实现自动收集相关环境变量参数并更新至青龙面板的方法。 60 | 61 | 环境安装与配置(编辑青龙面板的 `配置文件 - extra.sh` 文件,追加如下内容): 62 | 63 | ```bash 64 | # 全局安装 whistle 代理工具。注意,需本机已安装 node.js 65 | npm i -g whistle @lzwme/whistle.x-scripts 66 | 67 | # 创建工作目录(青龙面板示例) 68 | mkdir -p /ql/data/scripts/whistle 69 | cd /ql/data/scripts/whistle 70 | 71 | if [ ! -e w2.x-scripts.config.js ]; then 72 | cp /usr/local/lib/node_modules/@lzwme/whistle.x-scripts/w2.x-scripts.config.sample.js w2.x-scripts.config.js 73 | fi 74 | 75 | # 拉取公开供参考学习的常用脚本规则 76 | if [ ! -e x-scripts-rules ]; then 77 | git clone https://ghpr.cc/github.com/lzwme/x-scripts-rules.git 78 | fi 79 | 80 | # 用于存放自定义的脚本规则 81 | # 脚本规则编写方法参考:https://github.com/lzwme/whistle.x-scripts.git 82 | mkdir local-x-scripts-rules 83 | 84 | # 启动代理插件 85 | w2 start 86 | ``` 87 | 88 | 接着 PC 或手机设置代理地址为 `w2 start` 启动打印的地址。代理设置方法参考:https://github.com/lzwme/whistle.x-scripts.git 89 | 90 | 最后, 从 PC 或手机访问相关脚本对应的 APP 或小程序。在正常使用过程中,当代理插件脚本匹配到目标参数数据,即会自动更新至青龙面板的环境变量中。 91 | 92 | 此外,还可以在“脚本管理”中新建一个脚本(如 `rules-update.sh`),并新建一个定时任务每天执行一次,用于定时拉取更新公开的脚本规则。示例: 93 | 94 | ```bash 95 | #! /usr/bin/env bash 96 | cd /ql/data/scripts/whistle/x-scripts-rules 97 | git pull -r -n -v 98 | cd .. 99 | w2 restart 100 | ``` 101 | 102 | 扩展参考: 103 | 104 | - https://github.com/lzwme/whistle.x-scripts.git 105 | - https://github.com/lzwme/x-scripts-rules.git 106 | 107 | ## 脚本列表(22): 108 | 109 | > 注意:本仓库脚本不支持单独订阅。可订阅仓库并禁用不需要的脚本。 110 | 111 | - [每日早报-60s 读懂世界](./ql_60s.ts) 112 | - [爱玛会员俱乐部小程序](./ql_aima.ts) 113 | - [小雅挂载阿里云资源盘清理](./ql_alipan-clean.ts) 114 | - [阿里云盘签到](./ql_alipan_signin.ts) 115 | - [长虹美菱小程序签到](./ql_chml.ts) 116 | - [古井贡酒会员中心小程序](./ql_gujing.ts) 117 | - [哈啰签到](./ql_hl.ts) 118 | - [葫芦娃预约](./ql_huluwa.ts) 119 | - [禁用青龙重复脚本](./ql_identical.ts) 120 | - [ikuuu 机场签到](./ql_ikuuu.ts) 121 | - [I 茅台预约](./ql_imaotai.ts) 122 | - [whistle.x-scripts 插件安装与更新](./ql_install_whistle.x-scripts.ts) 123 | - [品赞代理签到](./ql_ipzan_signin.ts) 124 | - [爱奇艺签到](./ql_iqiyi.ts) 125 | - [杰士邦安心福利社-小程序](./ql_jsbaxfls.ts) 126 | - [青龙 sendNotify 通知修改拦截](./ql_ModifySendNotify.js) 127 | - [ssone 机场签到](./ql_ssone.ts) 128 | - [同花顺签到](./ql_thsSignIn.ts) 129 | - [腾讯视频 VIP 会员签到](./ql_videoqq.ts) 130 | - [喜马拉雅签到](./ql_xmlySign.ts) 131 | - [有赞小程序签到](./ql_youzan-liteapp.ts) 132 | - [云闪付签到](./ql_ysfqd.ts) 133 | 134 | ## 其他相关 135 | 136 | ### 获取指定位置的经纬度 137 | 138 | - [腾讯位置服务](https://lbs.qq.com/getPoint/) 139 | - [高德地图坐标拾取器](https://lbs.amap.com/tools/picker) 140 | - [详细地址解析成经纬度/GPS 坐标在线工具](https://www.toolnb.com/tools/areaDataToGps.html) 141 | - [高德地图拾取器](https://www.toolnb.com/tools/gaodegetmap.html) 142 | 143 | ### 相关链接 144 | 145 | - [青龙面板](https://github.com/whyour/qinglong) 146 | - [Whistle](https://github.com/avwo/whistle) 147 | - [Whistle.x-scripts](https://github.com/lzwme/whistle.x-scripts) 148 | - [x-scripts-rules](https://github.com/lzwme/x-scripts-rules) 149 | -------------------------------------------------------------------------------- /deprecated/.gitkeep: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /deprecated/ql_check_url_update.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-03-25 13:48:52 6 | * @Description: 脚本内容变更检测。根据指定的脚本 URL 或本地访问路径获取脚本内容,并与缓存 hash 比对 7 | 8 | new Env('脚本内容变更检测') 9 | cron: 50 8 * * * 10 | 环节变量: CHECK_URLS ,指定要检查的 URL 或文件路径,多个以换行或 $$ 分割 11 | */ 12 | import { existsSync, readFileSync } from 'node:fs'; 13 | import { dateFormat, md5 } from '@lzwme/fe-utils'; 14 | import { Env, getConfigStorage } from './utils'; 15 | 16 | const $ = new Env('脚本内容变更检测', { sep: ['\n', '$$'] }); 17 | const cacheStor = getConfigStorage>('urlCheck', 'cache/lzwme_check_url_cache.json'); 18 | 19 | export async function urlCheck(url: string) { 20 | if (!url || url.startsWith('#') || url.startsWith('//')) return $.log(`忽略注释: ${url}`); 21 | 22 | let content = ''; 23 | $.req.setHeaders({ 'content-type': 'text/plain' }); 24 | if (url.startsWith('http')) content = await $.req.get(url).then(d => d.data); 25 | else if (existsSync(url)) content = readFileSync(url, 'utf-8'); 26 | else return $.log(`文件不存在:${url}`, 'error'); 27 | 28 | const hash = md5(content); 29 | const cacheHash = cacheStor.getItem(url); 30 | if (cacheHash?.hash !== hash) { 31 | if (cacheHash) $.log(`🔔 内容已变更: ${url} [上次变更:${dateFormat('yyyy-MM-dd hh:mm:ss', cacheHash.t)}]`, 'error'); 32 | else $.log(`➡ 首次检查,存入缓存:${url}`); 33 | cacheStor.setItem(url, { t: Date.now(), hash }); 34 | } else console.log(`✅ 内容未变更: ${url}`); 35 | } 36 | 37 | (async function start() { 38 | let val = process.env.CHECK_URLS; 39 | if (val) { 40 | if (existsSync(val)) val = readFileSync(val, 'utf-8'); 41 | const urls = val.split('\n').filter(d => d && !d.startsWith('#') && !d.startsWith('//')); 42 | $.log(`开始检查,共 ${urls.length} 个URL`); 43 | for (const url of urls) await urlCheck(url.trim()).catch(e => $.log(`处理失败: ${url}\n${(e as Error).message}`, 'error')); 44 | } else $.log('未指定要检查的URL列表。请配置环境变量 CHECK_URLS'); 45 | $.done(); 46 | })(); 47 | -------------------------------------------------------------------------------- /deprecated/ql_dkl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-04-02 14:12:13 6 | * @Description: 迪卡侬签到。奖励:积攒奖励金可换手机话费重置抵用券 7 | 8 | cron: 50 8 * * * 9 | 环境变量: dkl_token 抓包 api-cn.decathlon.com.cn 请求 header 里面的 Authorization 10 | 示例:export dkl_token="23fexxxxxxxxxxxxxxxxxxxxxxxxxxxx" 11 | */ 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('迪卡侬签到'); 15 | 16 | export async function signCheckIn(token: string) { 17 | const signUrl = 'https://api-cn.decathlon.com.cn/membership/membership-portal/mp/api/v1/business-center/reward/CHECK_IN_DAILY'; 18 | $.req.setHeaders({ 19 | Authorization: token.startsWith('Bearer ') ? token : `Bearer ${token}`, 20 | referer: 'https://servicewechat.com/wxdbc3f1ac061903dd/337/page-frame.html', 21 | 'User-Agent': 22 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x6309092b) XWEB/9079', 23 | }); 24 | const { data: signRes } = await $.req.post(signUrl, {}); 25 | 26 | if (+signRes.code == 0) { 27 | $.log(`签到成功,获取积分: ${signRes.data?.point_change}。当前可用积分:${signRes.data?.point_balance}`); 28 | } else if (String(signRes.code).includes('1006')) { 29 | $.log(`今日已签到 ${signRes.msg || JSON.stringify(signRes)}`); 30 | } else { 31 | console.error(signRes); 32 | $.log(`签到失败:${JSON.stringify(signRes.msg)}`, 'error'); 33 | } 34 | } 35 | 36 | // process.env.dkl_token = ''; 37 | if (require.main === module) $.init(signCheckIn, 'dkl_token').then(() => $.done()); 38 | -------------------------------------------------------------------------------- /deprecated/ql_jwyb.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-03-22 10:00:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-03-31 09:45:54 6 | * 7 | cron: 25 8 * * * 8 | new Env('绝味鸭脖小程序30天连续签到挑战') 9 | 环境变量: jwyb_urls: 抓取该地址的完整请求URL:https://p3720226302625sh3s-saas.xiaoman-activity.meta-xuantan.com/activityMultiport.html 10 | 多账户用 @ 或换行分割 11 | 有效期较短,重新认证需获取应用微信 code,故废弃 12 | */ 13 | 14 | import { dateFormat, generateUuid, md5, Request, wait } from '@lzwme/fe-utils'; 15 | import { Env } from './utils'; 16 | 17 | const $ = new Env('绝味鸭脖小程序30天连续签到挑战', { sep: ['@', '\n'] }); 18 | const req = new Request({ baseURL: 'https://p3720226302625sh3s-saas.xiaoman-activity.meta-xuantan.com' }); 19 | 20 | class UserInfo { 21 | private li = ''; 22 | private xmToken = ''; 23 | baseKey = atob('dWgzJEhnJl5ISzg3NiVnYnhWRzdmJCVwPTBNfj5zMXg='); 24 | constructor(url: string, private index: number) { 25 | req.setHeaders({ 26 | 'user-agent': 27 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.56(0x1800383b) NetType/WIFI Language/zh_CN miniProgram/wxf6ec04edb4802bec', 28 | Referer: url, 29 | 'content-type': 'application/json', 30 | ri: '', 31 | bdrk: '', 32 | }); 33 | this.li = new URL(url).searchParams.get('li')!; 34 | } 35 | async start() { 36 | await this.sign(); 37 | // await this.getTaskList(); 38 | } 39 | objKeySort(t: any) { 40 | const n: any = {}; 41 | Object.keys(t) 42 | .sort() 43 | .forEach((key) => { 44 | if (key != null && key != 'null') n[key] = t[key]; 45 | }); 46 | return n; 47 | } 48 | genTokenSign(li = this.li) { 49 | const e = { 50 | nonceStr: generateUuid().replace(/-/g, '').toLocaleLowerCase(), 51 | timestamp: new Date().getTime(), 52 | tokenSign: li, 53 | }; 54 | e.tokenSign += e.nonceStr; 55 | e.tokenSign += e.timestamp; 56 | e.tokenSign += atob('SjdoOCZeQmdzNSNibio3aG4lIT1raDMwOCpidjIhc14='); 57 | e.tokenSign = md5(e.tokenSign); 58 | return e; 59 | } 60 | genXmSign(params: any) { 61 | let q = ''; 62 | const m = { 63 | nonceStr: generateUuid().replace(/-/g, '').toLocaleLowerCase(), 64 | xmTimestamp: new Date().getTime(), 65 | xmToken: this.xmToken, 66 | }; 67 | 68 | params = this.objKeySort(Object.assign({}, params, m)); 69 | 70 | Object.values(params).forEach((v) => { 71 | q += typeof v === 'object' ? JSON.stringify(v) : v; 72 | }); 73 | // console.log('q:', q, params); 74 | 75 | const xmSign = md5(q + this.baseKey); 76 | return { xmSign, ...m }; 77 | } 78 | async getUserToken() { 79 | if (this.xmToken) return this.xmToken; 80 | // const url = `https://p3720226302625sh3s-saas.xiaoman-activity.meta-xuantan.com/xm/token/getUserToken`; 81 | const query = this.genTokenSign(); 82 | // const res = await y.httpGet(url, query); 83 | const headers = { ...this.genXmSign(query) }; 84 | const { data: res } = await req.get('/xm/token/getUserToken', query, headers); 85 | console.log('getUserToken:', res); 86 | 87 | if (res.code != 0) { 88 | $.log(`账号[${this.index}] 获取用户token失败: ${res.desc}`, 'error'); 89 | console.log(res); 90 | return ''; 91 | } 92 | // xmToken 93 | req.setHeaders({ xmToken: res.data }); 94 | this.xmToken = res.data; 95 | return res.data; 96 | } 97 | async getTaskList() { 98 | const headers = { ...this.genXmSign({}), functionId: '100540002' }; 99 | const { data: res } = await req.get('/activity/function/task/get', {}, headers); 100 | 101 | if (res.data?.taskList?.length) { 102 | console.log(`账号[${this.index}] 任务列表:`, res.data.taskList); 103 | // todo: 完成任务 104 | // https://p3720226302625sh3s-saas.xiaoman-activity.meta-xuantan.com/activity/function/task/finish 105 | for (const task of res.data.taskList) { 106 | if (task.status !== 0 || task.taskNum <= task.finishNum) continue; 107 | 108 | console.log('task:', task.taskTile, `${task.finishNum}/${task.taskNum}`); 109 | const body = { 110 | actOpId: '84991480', 111 | actionId: 0, 112 | actionParams: {}, 113 | taskId: task.taskId, 114 | timestamp: new Date().getTime(), 115 | functionId: '', 116 | }; 117 | 118 | const { data: res } = await req.post('/activity/function/task/finish', body, { 119 | functionId: task.functionId || '100540002', 120 | ...this.genXmSign({ ...body, xmToken: this.xmToken }), 121 | }); 122 | if (res.code == 0) { 123 | $.log(`账号[${this.index}] 任务[${task.taskTitle}]完成成功: ${res.desc}`); 124 | } else { 125 | $.log(`账号[${this.index}] 任务[${task.taskTitle}]完成失败: ${res.desc}`, 'error'); 126 | console.log('finishTask:', res); 127 | } 128 | } 129 | } else { 130 | $.log(`账号[${this.index}] 获取任务列表失败: ${res.desc}`, 'error'); 131 | console.log('getTaskList:', res); 132 | } 133 | 134 | return res; 135 | } 136 | async sign() { 137 | await this.getUserToken(); 138 | if (!this.xmToken) return; 139 | 140 | await wait(1000 * 30, 1000 * 60); 141 | 142 | const body = { patchDate: dateFormat('yyyy-MM-dd', new Date()) }; 143 | // const res = await y.httpPost2(url, body, 0); 144 | const { data: res } = await req.post('/sign/action', body, { 145 | functionid: '0', 146 | ...this.genXmSign({ ...body, xmToken: this.xmToken }), 147 | }); 148 | if (res.code == 0) { 149 | $.log(`账号[${this.index}] 签到成功: ${res.desc}`); 150 | } else { 151 | $.log(`账号[${this.index}] 签到失败: ${res.desc}`, 'error'); 152 | console.log('sign:', res); 153 | } 154 | return res; 155 | } 156 | } 157 | // process.env.jwyb_urls = ''; 158 | $.init(UserInfo, 'jwyb_urls').then(() => $.done()); 159 | -------------------------------------------------------------------------------- /deprecated/ql_sysxc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-19 13:34:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-05-09 09:01:39 6 | * @Description: 书亦烧仙草小程序签到 7 | 8 | cron: 11 10 * * * 9 | const $ = new Env("书亦烧仙草签到"); 10 | 环境变量: 11 | - sysxc,抓包获取 header 中的 auth,多个账户以 & 或 \n 换行分割 12 | - LZWME_OCR_API 自建 OCR 访问地址。若为空则使用 `@u4/opencv4nodejs` 模块。可基于该仓库搭建: https://github.com/lzwme/captcha-cv-ocr 13 | */ 14 | import CryptoJS from 'crypto-js'; 15 | import axios from 'axios'; 16 | import { Env } from './utils'; 17 | 18 | // process.env.sysxc = ''; 19 | // process.env.LZWME_OCR_API = ''; 20 | // process.env.LZWME_OCR_TOKEN = ''; 21 | const $ = new Env('书亦烧仙草签到'); 22 | $.init(signIn, 'sysxc').then(() => $.done()); 23 | 24 | async function slider_match(img1: string, img2: string, type = 'api', ocrApi = process.env.LZWME_OCR_API, token = process.env.LZWME_OCR_TOKEN) { 25 | if (!ocrApi && token) ocrApi = 'https://captcha_cv_ocr.lzw.me/ocr'; 26 | 27 | if (ocrApi && type === 'api') { 28 | const body = { mode: 'slide_match', base64: img1, originalBase64: img2 }; 29 | const b = await fetch(ocrApi, { 30 | method: 'post', 31 | headers: { 'Content-Type': 'application/json', origin: '*', token: token || '' }, 32 | body: JSON.stringify(body), 33 | }).then((d) => d.json()); 34 | if (!b.data?.maxLoc.x) console.log('[ocr] decode by lzw:', b); 35 | return b.data?.maxLoc.x; 36 | } else { 37 | const cv = require('@u4/opencv4nodejs'); 38 | const img1CV = cv.imdecode(Buffer.from(img1, 'base64')); 39 | const img2CV = cv.imdecode(Buffer.from(img2, 'base64')); 40 | const matched = img1CV.matchTemplate(img2CV, cv.TM_CCOEFF_NORMED); 41 | const matchedPoints = matched.minMaxLoc(); 42 | return matchedPoints.maxLoc.x; 43 | } 44 | } 45 | 46 | function AES_Encrypt(word: string, k: string) { 47 | const key = CryptoJS.enc.Utf8.parse(k); 48 | const srcs = CryptoJS.enc.Utf8.parse(word); 49 | const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); 50 | return encrypted.toString(); 51 | } 52 | 53 | async function signIn(auth: string) { 54 | const headers = { 55 | auth, 56 | hostname: 'scrm-prod.shuyi.org.cn', 57 | 'content-type': 'application/json', 58 | host: 'scrm-prod.shuyi.org.cn', 59 | 'User-Agent': 60 | 'Mozilla/5.0 (Linux; Android 10; V2203A Build/SP1A.210812.003; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/107.0.5304.141 Mobile Safari/537.36 XWEB/5023 MMWEBSDK/20221012 MMWEBID/1571 MicroMessenger/8.0.30.2260(0x28001E55) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android', 61 | }; 62 | let url = 'https://scrm-prod.shuyi.org.cn/saas-gateway/api/agg-trade/v1/signIn/getVCode'; 63 | const { data: vCodeRes } = await axios.post(url, { captchaType: 'blockPuzzle', clientUid: '', ts: new Date().getTime() }, { headers }); 64 | if (!vCodeRes.data) return $.log(`获取验证码失败!${vCodeRes.resultMsg}`, 'error'); 65 | 66 | const { secretKey, token, jigsawImageBase64: img1, originalImageBase64: img2 } = vCodeRes.data; 67 | const x = await slider_match(img1, img2); 68 | if (!x) return $.log('验证码识别失败!', 'error'); 69 | 70 | url = 'https://scrm-prod.shuyi.org.cn/saas-gateway/api/agg-trade/v1/signIn/checkVCode'; 71 | const pointJson = AES_Encrypt(JSON.stringify({ x, y: 5 }), secretKey); 72 | const { data: checkVCodeRes } = await axios.post(url, { captchaType: 'blockPuzzle', pointJson, token }, { headers }); 73 | if (checkVCodeRes.resultMsg) console.log(checkVCodeRes.resultMsg); 74 | 75 | const captchaVerification = AES_Encrypt(token + '---' + JSON.stringify({ x, y: 5 }), secretKey); 76 | url = 'https://scrm-prod.shuyi.org.cn/saas-gateway/api/agg-trade/v1/signIn/insertSignInV3'; 77 | const { data: signInRes } = await axios.post(url, `{"captchaVerification":"${captchaVerification}"}`, { headers }); 78 | if (signInRes.resultMsg == 'success') $.log('签到成功'); 79 | else if (String(signInRes.resultMsg).includes('签到')) $.log(signInRes.resultMsg); 80 | else $.log(signInRes.resultMsg, 'error'); 81 | 82 | url = 'https://scrm-prod.shuyi.org.cn/saas-gateway/api/agg-trade/v1/member/points/list?pageNum=1&pageSize=10&type=0&isQueryWillExpire=0'; 83 | const { data: listRes } = await axios.get(url, { headers }); 84 | if (listRes.resultCode == 0) { 85 | $.log(`当前积分:${listRes.extFields.availablePoints}`); 86 | } else { 87 | $.log(`获取积分失败: ${listRes.resultMsg}`, 'error'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /deprecated/ql_ws_jwcn_card_sign.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-05 13:40:37 6 | * @Description: 佳为软件,微信会员签到 7 | 8 | cron: 50 8 1 1 1 9 | 环境变量: WS_JWCN_SIGN 抓包 https://ws.jwcn.net/weixinpl/card/card_sign.php?card_member_id= ,URL 中的 card_member_id、card_id 以及请求 cookie 里的 PHPSESSID,用;连接 10 | 示例:export WS_JWCN_SIGN="card_member_id=xxx; card_id=1111; PHPSESSID=xxxx" 11 | 多个账号用换行分隔 12 | */ 13 | import { cookieParse } from '@lzwme/fe-utils'; 14 | import { Env } from './utils'; 15 | 16 | const $ = new Env('奇奥超市签到'); 17 | 18 | export async function signCheckIn(WS_JWCN_SIGN: string) { 19 | const { card_member_id, card_id, PHPSESSID = '' } = cookieParse(WS_JWCN_SIGN); 20 | let url = `https://ws.jwcn.net/weixinpl/card/sign_score.php?callback=jsonpCallback_signscore&card_member_id=${card_member_id}&card_id=${card_id}`; 21 | let txt = await fetch(url).then(d => d.text()); 22 | 23 | txt = txt.replaceAll('jsonpCallback_signscore', '').replace(/^\(/, '').replace(');', ''); 24 | console.log(txt, card_member_id, card_id, PHPSESSID, url); 25 | 26 | if (/card_member_id:(\d+)/.exec(txt)) { 27 | $.log('签到成功!'); 28 | } else { 29 | $.log(`签到失败!${txt}`, 'error'); 30 | } 31 | 32 | url = `https://ws.jwcn.net/weixinpl/card/card_sign.php?card_member_id=${card_member_id}&card_id=${card_id}`; 33 | txt = await fetch(url, { 34 | headers: { 35 | cookie: `PHPSESSID=${PHPSESSID || ''}`, 36 | referer: `https://ws.jwcn.net/weixinpl/card/card_sign.php?card_id=`, 37 | 'user-agent': `Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.50(0x18003231) NetType/WIFI Language/zh_CN`, 38 | }, 39 | }).then(d => d.text()); 40 | 41 | const m = /

(今日已签到.+) $.done()); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ql-scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "scripts": { 8 | "start": "npm run u", 9 | "u": "node todo/update-readme.mjs", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@lzwme/fe-utils": "^1.9.0", 17 | "axios": "^1.9.0", 18 | "commander": "^13.1.0", 19 | "console-log-colors": "^0.5.0", 20 | "crypto-js": "^4.2.0", 21 | "enquirer": "^2.4.1", 22 | "json5": "^2.2.3", 23 | "moment": "^2.30.1" 24 | }, 25 | "devDependencies": { 26 | "@iarna/toml": "^2.2.5", 27 | "@types/crypto-js": "^4.2.2", 28 | "@types/node": "^22.15.18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ql_60s.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-06-08 10:10:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-04-25 08:47:58 6 | * 7 | cron: 30 7 1 1 1 8 | new Env('每日早报-60s读懂世界') 9 | 10 | 环境变量: 11 | export QL_60s_API='https://60s.lzw.me' # 60s API 地址。可本地搭建后自定义为本地地址,以保证可控的稳定性 12 | 13 | export QL_60s_TYPE='60s' # 订阅的类型,可订阅多个,以逗号分隔,每个订阅单独发送一条消息。默认为 60s。 14 | 可选:  60s, bili, weibo, zhihu, toutiao, douyin, hisyory 15 | 详情参考: https://github.com/lzwme/60s-php 16 | */ 17 | 18 | import { sendNotify } from './utils'; 19 | 20 | const ALL_TYPE = { 21 | '60s': '60s读懂世界', 22 | bili: 'B站热搜', 23 | weibo: '微博热搜', 24 | zhihu: '知乎热榜', 25 | toutiao: '头条热搜', 26 | douyin: '抖音热搜', 27 | hisyory: '历史上的今天', 28 | }; 29 | 30 | async function notify(msg: string, title = '60s读懂世界') { 31 | await sendNotify(title, msg, { notifyType: 2, isPrint: true }); 32 | } 33 | 34 | async function start() { 35 | const API = process.env.QL_60s_API || 'https://60s.lzw.me'; 36 | const types = (process.env.QL_60s_TYPE || '60s').split(','); 37 | 38 | for (let type of types) { 39 | type = type.trim(); 40 | try { 41 | if (type in ALL_TYPE) { 42 | const info: { data: { news: string[]; date: string; tip: string } } = await fetch(`${API}?type=${type}`).then(d => d.json()); 43 | const title = ALL_TYPE[type as never as keyof typeof ALL_TYPE]; 44 | // console.log(`发送通知: [${type}][${title}]`); 45 | const msg = info.data.news.map((d, i) => `${i + 1}. ${d}`).join('\n'); 46 | await notify(`${msg}\n\n[${info.data.date}]${info.data.tip}`, `${title}`); 47 | } 48 | } catch (error) { 49 | console.log(error); 50 | sendNotify(`[💌]每日早报[${type}]`, `error: ` + (error as Error).message); 51 | } 52 | } 53 | } 54 | 55 | start().finally(() => process.exit()); 56 | -------------------------------------------------------------------------------- /ql_ModifySendNotify.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-19 13:34:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-04-01 15:01:42 6 | * @Description: 青龙面板sendNotify通知修改拦截。 7 | * @link https://github.com/lzwme/ql-scripts/blob/main/ql_ModifySendNotify.js 8 | * 9 | * 背景:拉取的第三方脚本,执行成功与否都会发大量的广告通知。但我们希望失败时才通知,否则消息轰炸会很烦。 10 | * 本脚本通过注入的方式修改青龙 sendNotify 函数,可以实现仅允许消息中包含自定义关键字时才发送通知,否则拦截处理。 11 | * 建议配置到订阅脚本的 "执行后" 内容里: node /ql/data/scripts/lzwme_ql-scripts/ql_ModifySendNotify.js 12 | 13 | cron: 1 18 * * * 14 | const $ = new Env("青龙sendNotify通知修改拦截"); 15 | 16 | 环境变量: 17 | - QL_NOTIFY_ALLOW_WORD 通知中包含指定的关键词则允许发送通知,其它均拦截,多个关键词用逗号分割。默认值参考代码中定义 18 | - QL_NOTIFY_BAN_WORD 通知中包含指定的关键字则禁止发送通知。 19 | - QL_NOTIFY_REPO_WORD 允许修改的仓库名称关键字,多个关键词用逗号分割。默认为空,则全部订阅的仓库都处理 20 | - QL_SCRIPTS_DIR 青龙面板 scripts 目录的路径。默认为 `/ql/data/scripts` 21 | - SKIP_PUSH_TITLE 青龙面板 sendNotify 自带支持的环境变量,支持配置脚本通知名称,多个使用换行分割 22 | */ 23 | 24 | const { resolve } = require('path'); 25 | 26 | async function modifySendNotify() { 27 | const allowWordsString = 28 | process.env.QL_NOTIFY_ALLOW_WORD || 29 | '登录失败,签到失败,异常,未登录,❌,已失效,无效,重新登录,未找到,水果奖励,京东资产统计,[60s],[🔔]'; 30 | const ignoreWordsString = process.env.QL_NOTIFY_BAN_WORD || ''; 31 | 32 | const allowWords = allowWordsString 33 | .split(',') 34 | .map(d => d.trim()) 35 | .filter(Boolean); 36 | const banWords = ignoreWordsString 37 | .split(',') 38 | .map(d => d.trim()) 39 | .filter(Boolean); 40 | const allowRepoWords = (process.env.QL_NOTIFY_REPO_WORD || '') 41 | .split(',') 42 | .map(d => d.trim()) 43 | .filter(Boolean); 44 | 45 | if (allowWords.length === 0) return; 46 | 47 | const fs = require('fs'); 48 | const scriptsDir = process.env.QL_SCRIPTS_DIR || '/ql/data/scripts'; 49 | 50 | if (!fs.existsSync(scriptsDir)) { 51 | console.log(`cwd: ${process.cwd()} \nscriptsDir: ${scriptsDir}`); 52 | return console.error('青龙脚本目录不存在!请配置环境变量 QL_SCRIPTS_DIR 指定正确的位置'); 53 | } 54 | 55 | const notifyFiles = { 56 | 'sendNotify.js': [], 57 | 'notify.py': [], 58 | }; 59 | const allowModifyFiles = new Set(); 60 | const findNotifyFiles = dir => { 61 | fs.readdirSync(dir).forEach(filename => { 62 | const filepath = resolve(dir, filename); 63 | 64 | if (fs.statSync(filepath).isDirectory()) findNotifyFiles(filepath); 65 | else if (filename in notifyFiles) { 66 | notifyFiles[filename].push(filepath); 67 | 68 | if (allowRepoWords.length === 0 || allowRepoWords.some(d => dir.includes(d))) { 69 | allowModifyFiles.add(filepath); 70 | } 71 | } 72 | }); 73 | }; 74 | let insertStr = [ 75 | `var allowWords = ${JSON.stringify(allowWords)};`, 76 | `if (!allowWords.some(k => String(desp).includes(k) || String(text).includes(k))) return console.log('已忽略消息推送[ALLOW_WORDS]');`, 77 | `var banWords = ${JSON.stringify(banWords)};`, 78 | `if (banWords.some(k => String(desp).includes(k) || String(text).includes(k))) return console.log('消息推送已忽略');`, 79 | ].join('\n'); 80 | 81 | findNotifyFiles(scriptsDir); 82 | 83 | for (const filepath of notifyFiles['sendNotify.js']) { 84 | let content = fs.readFileSync(filepath, 'utf8'); 85 | 86 | if (!allowModifyFiles.has(filepath)) { 87 | const newContent = removeInsertCode(content, 'js'); 88 | if (content !== newContent) { 89 | fs.writeFileSync(filepath, newContent, 'utf8'); 90 | console.log(`[js]不在允许修改列表中,移除修改内容`, filepath); 91 | } 92 | 93 | continue; 94 | } 95 | 96 | if (content.includes('function sendNotify(')) { 97 | if (content.includes(insertStr)) { 98 | console.log('[js]已存在插入内容:', filepath); 99 | } else { 100 | content = removeInsertCode(content, 'js'); 101 | 102 | if (/desp \+=.+author/.test(content)) { 103 | content = content.replace(/(desp \+=.+author.+)/, `$1\n${insertStr}`); 104 | } else { 105 | content = content.replace(/(function sendNotify\(.+)/, `$1\n${insertStr}`); 106 | } 107 | fs.writeFileSync(filepath, content, 'utf8'); 108 | console.log('[js]文件修改成功:', filepath); 109 | } 110 | } 111 | } 112 | 113 | insertStr = [ 114 | ` allow_words = ${JSON.stringify(allowWords)}`, 115 | ` if not any(k in str(content) for k in allow_words):`, 116 | ` print("已忽略消息推送[ALLOW_WORDS]")`, 117 | ` return`, 118 | ` ban_words = ${JSON.stringify(banWords)}`, 119 | ` if any(k in str(content) for k in ban_words):`, 120 | ` print("消息推送已忽略")`, 121 | ` return`, 122 | ].join('\n'); 123 | 124 | for (const filepath of notifyFiles['notify.py']) { 125 | let content = fs.readFileSync(filepath, 'utf8'); 126 | 127 | if (!allowModifyFiles.has(filepath)) { 128 | const newContent = removeInsertCode(content, 'py'); 129 | if (content !== newContent) { 130 | fs.writeFileSync(filepath, newContent, 'utf8'); 131 | console.log(`[py]不在允许修改列表中,移除修改内容`, filepath); 132 | } 133 | 134 | return; 135 | } 136 | 137 | if (content.includes('def send(title') && content.includes('if not content:')) { 138 | if (content.includes(insertStr)) { 139 | console.log('[py]已存在插入内容:', filepath); 140 | } else { 141 | content = removeInsertCode(content, 'py').replace(/( +if not content:.*)/, `${insertStr}\n\n$1`); 142 | 143 | fs.writeFileSync(filepath, content, 'utf8'); 144 | console.log('[py]文件修改成功:', filepath); 145 | } 146 | } 147 | } 148 | } 149 | 150 | function removeInsertCode(content, type = 'js') { 151 | if (type === 'js') { 152 | return content.replaceAll(/var allowWords = (.+\r?\n)+.+消息推送已忽略'\);\r?\n/g, ''); 153 | } 154 | return content.replaceAll(/ +allow_words = (.+\r?\n.+)+消息推送已忽略"\)(\r?\n\ +return)?(\r?\n)+/g, '\n'); 155 | } 156 | 157 | process.env.QL_SCRIPTS_DIR = 'tmp'; 158 | modifySendNotify(); 159 | -------------------------------------------------------------------------------- /ql_aicnn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2025-02-28 21:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-03-05 17:10:40 6 | * @Description: AICNN签到领积分 - 积分可以兑换大模型 API 调用量 7 | 8 | 注册地址(使用此邀请注册可多得8888积分): http://aicnn.cn/loginPage?aff=4MvsDBGxfZ 9 | 10 | 参考:提供免费额度大模型 API 的邀请注册(通过邀请方式注册,双方均可多得额度) 11 | - aicnn(送8888积分):https://aicnn.cn/loginPage?aff=4MvsDBGxfZ 12 | - 硅基流动(送14元赠金):https://cloud.siliconflow.cn/i/hDM9hDR6 13 | - 火山方舟(送145元赠金):https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2D8BETN6 14 | - 派欧算力云(送50元赠金):https://ppinfra.com/user/register?invited_by=XRMRL5 15 | 16 | cron: 30 8 * * * 17 | 18 | 用法: 19 | - 环境变量:aicnn_token 20 | - 抓包 https://api.aicnn.cn/app-api/system/auth/refresh-token,获取 URL 中的 refreshToken 参数 21 | - 多账号使用 & 或换行符分割。示例:export aicnn_token="9c83d7144d7840a4ad4xxxxxxxxxxaad##memberId2&xxxxxx##memberId2" 22 | */ 23 | import { Env } from './utils'; 24 | 25 | const $ = new Env('AICNN签到领积分'); 26 | 27 | export async function signCheckIn(refreshToken: string) { 28 | const { data: r } = await $.req.post(`https://api.aicnn.cn/app-api/system/auth/refresh-token?refreshToken=${refreshToken}`, {}); 29 | if (r.code !== 0) { 30 | $.log(`刷新Token失败:${r.msg}`, 'error') 31 | return; 32 | } 33 | const token = r.data?.accessToken; 34 | 35 | $.req.setHeaders({ 36 | authorization: `Bearer ${token.replace('Bearer ', '')}`, 37 | accept: 'application/json', 38 | referer: 'https://aicnn.cn', 39 | }); 40 | const { data } = await $.req.get('https://api.aicnn.cn/app-api/system/user/signin'); 41 | 42 | if (String(data?.msg).includes('已经签到过')) { 43 | $.log(data.msg); 44 | } else if (data?.code == 0) { 45 | // emoji 对钩 46 | $.log(`✅签到成功!${data.msg}`); 47 | } else { 48 | $.log(`签到失败!${data.msg}`, 'error'); 49 | console.log(data); 50 | } 51 | } 52 | 53 | // process.env.aicnn_token = ''; 54 | if (require.main === module) $.init(signCheckIn, 'aicnn_token').then(() => $.done()); 55 | -------------------------------------------------------------------------------- /ql_aima.ts: -------------------------------------------------------------------------------- 1 | /** 2 | cron: 50 8 * * * 3 | 环境变量: aima 抓包 https://scrm.aimatech.com 请求里面的 access-token 4 | 示例:export aima="eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxx##memberId" 5 | */ 6 | 7 | import { generateUuid } from '@lzwme/fe-utils'; 8 | import { Env } from './utils'; 9 | 10 | const $ = new Env('爱玛会员俱乐部小程序'); 11 | 12 | export async function signCheckIn(token: string) { 13 | const { sign, nonce, timestamp } = getSign(token); 14 | 15 | $.req.setHeaders({ 16 | 'access-token': token, 17 | Accept: 'application/json, text/plain, */*', 18 | 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Android WebView";v="122"', 19 | 'Content-Type': 'application/json;charset=UTF-8', 20 | Origin: 'https://scrm.aimatech.com', 21 | referer: 'https://servicewechat.com/wx2dcfb409fd5ddfb4/183/page-frame.html', 22 | sign, 23 | 'time-stamp': timestamp, 24 | charset: 'utf-8', 25 | 'tracelog-id': nonce, 26 | 'app-id': 'scrm', 27 | xweb_xhr: 1, 28 | // 'X-Requested-With': 'com.tencent.mm', 29 | 'Sec-Fetch-Site': 'same-origin', 30 | 'Sec-Fetch-Mode': 'cors', 31 | 'Sec-Fetch-Dest': 'empty', 32 | 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 33 | 'user-agent': 34 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.50(0x1800323c) NetType/WIFI Language/zh_CN miniProgram/wxba9855bdb1a45c8e', 35 | }); 36 | 37 | const params = { 38 | activityId: '100000954', 39 | activitySceneId: null, 40 | }; 41 | 42 | const r = await $.req.post('https://scrm.aimatech.com/aima/wxclient/mkt/activities/sign:search', params); 43 | // console.log(result); 44 | 45 | if (r.data?.code == 200) { 46 | if (r.data.content.signed !== 2) { 47 | $.log(`未签到 ===> 签到ing`); 48 | const { data } = await $.req.post('https://scrm.aimatech.com/aima/wxclient/mkt/activities/sign:join', params); 49 | 50 | if (data.code == 200 && data.content?.point) { 51 | $.log(`签到成功!获取积分 ${data.content.point}`); 52 | } else { 53 | console.log(data); 54 | $.log(`签到失败!${data.chnDesc}`, 'error'); 55 | } 56 | } else { 57 | $.log(`已签到 ===> 什么都不做`); 58 | } 59 | } else { 60 | $.log(`签到失败 [${JSON.stringify(r.data)}]`, 'error'); 61 | } 62 | } 63 | 64 | function getSign(token: string) { 65 | const accessToken = token.substring(50, 80); 66 | const nonce = generateUuid(); 67 | const timestamp = Date.now(); 68 | const text = `App-IdscrmTime-Stamp${timestamp}TraceLog-Id${nonce}Access-Token${accessToken}AimaScrm321_^`; 69 | 70 | const sign = require('crypto').createHash('md5').update(text).digest('hex'); 71 | 72 | return { nonce, sign, timestamp }; 73 | } 74 | 75 | // process.env.aima = ''; 76 | if (require.main === module) $.init(signCheckIn, 'aima').then(() => $.done()); 77 | -------------------------------------------------------------------------------- /ql_alipan-clean.ts: -------------------------------------------------------------------------------- 1 | /* 2 | new Env("小雅挂载阿里云资源盘清理") 3 | cron: 20 22 1 1 1 4 | 5 | process.env.ALIPAN_CLEAN = 'host=http://192.168.1.10:5678;password=3aVyo8YnaXJ2XJjoTjxxxxxxxxxx'; 6 | 设置环境变量 ALIPAN_CLEAN,格式为: 7 | username=[admin];password=xxx;token=认证token;dir=xxx;host=xxx;limit=50 8 | 参数说明: 9 | username 默认为 admin 10 | password 管理员密码。可执行该命令获取: ./alist admin 11 | token 登录认证 token,与 passord 设置其一即可 12 | host 为小雅访问地址,默认值为: http://127.0.0.1:5678 13 | dir 为小雅挂载阿里云盘缓存目录的路径,默认为: /📀我的阿里云盘/资源盘/小雅转存 14 | */ 15 | 16 | import { cookieParse, Request } from '@lzwme/fe-utils'; 17 | 18 | interface XiaoYaResponse { 19 | code: number; 20 | message: string; 21 | data?: T; 22 | } 23 | type FsItem = { name: string; modified: string | number; size: number; is_dir: string }; 24 | 25 | const config = { 26 | host: 'http://127.0.0.1:5678', 27 | dir: '/📀我的阿里云盘/资源盘/小雅转存', 28 | username: 'admin', 29 | password: '', // 获取方法: ./alist admin 30 | token: '', 31 | limit: 10, 32 | }; 33 | const req = new Request('', { 'content-type': 'application/json' }); 34 | 35 | /** 小雅挂载的阿里云盘指定目录内容删除 */ 36 | async function alipanDirClean() { 37 | if (process.env.ALIPAN_CLEAN) Object.assign(config, cookieParse(process.env.ALIPAN_CLEAN)); 38 | else 39 | return console.log( 40 | '未设置环境变量 ALIPAN_CLEAN 配置。格式参考:username=admin;password=xxx;dir=xxx;host=http://127.0.0.1:5678;limit=50' 41 | ); 42 | if (!config.dir) return console.log('未设置阿里云盘缓存目录在小雅的挂载路径 dir 参数'); 43 | 44 | const getAuthUrl = `${config.host}/api/auth/login`; 45 | const fsListUrl = `${config.host}/api/fs/list`; 46 | const fsRemoveUrl = `${config.host}/api/fs/remove`; 47 | 48 | const { username, password } = config; 49 | if (username && password) { 50 | const { data: auth } = await req.post>(getAuthUrl, { username, password }); 51 | config.token = auth.data?.token || ''; 52 | 53 | if (!config.token) throw new Error('Failed to authenticate'); 54 | 55 | req.setHeaders({ Authorization: config.token }); 56 | } else if (!config.token) { 57 | return console.log('请设置小雅登录的 username 和 password,或 token 参数'); 58 | } 59 | 60 | const listParams = { 61 | path: config.dir, 62 | password: '', 63 | page: 1, 64 | per_page: 0, 65 | refresh: false, 66 | }; 67 | const { data: listJson } = await req.post }>>(fsListUrl, listParams); 68 | const contentList = listJson.data?.content || []; 69 | console.log(`当前[${config.dir}]中共有资源数量:`, contentList.length); 70 | 71 | if (contentList.length > config.limit) { 72 | const names = contentList 73 | .sort((a, b) => { 74 | if (typeof a.modified === 'string') a.modified = new Date(a.modified).getTime(); 75 | if (typeof b.modified === 'string') b.modified = new Date(b.modified).getTime(); 76 | return b.modified - a.modified; 77 | }) 78 | .slice(config.limit) 79 | .map((d) => d.name); 80 | 81 | const { data: removeJson } = await req.post(fsRemoveUrl, { dir: config.dir, names }); 82 | if (removeJson.code === 200) console.log(`本次删除了 ${names.length} 个资源。`, removeJson); 83 | else console.log('删除失败!', names, removeJson); 84 | } 85 | } 86 | 87 | alipanDirClean() 88 | .then(() => console.log('Process completed')) 89 | .catch((error) => console.error('Error:', error)); 90 | -------------------------------------------------------------------------------- /ql_alipan_signin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-23 13:52:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-01-15 08:55:33 6 | * 7 | cron: 25 7 * * * 8 | new Env('阿里云盘签到') 9 | 环境变量: alyp 抓取请求中的 refresh_token。多账户用 @ 或换行分割 10 | */ 11 | 12 | import { Env } from './utils'; 13 | const $ = new Env('阿里云盘签到', { sep: ['@', '\n'] }); 14 | 15 | class UserInfo { 16 | private access_token = ''; 17 | private nick_name = ''; 18 | private signInDay = 1; 19 | constructor(private refresh_token: string, private index: number) {} 20 | async start() { 21 | let { data: res } = await $.req.post(`https://auth.aliyundrive.com/v2/account/token`, { 22 | grant_type: 'refresh_token', 23 | refresh_token: this.refresh_token, 24 | }); 25 | if (res.status == 'enabled') { 26 | this.access_token = res.access_token; 27 | this.nick_name = res.nick_name; 28 | await this.sign(); 29 | } else $.log(`❌账号[${this.nick_name}] 更新token失败`), console.log(res); 30 | } 31 | async sign() { 32 | // todo: 获取限时任务 33 | // 'https://member.alipan.com/v2/activity/sign_in_info' data.result.rewards[] 34 | 35 | const { data: res } = await $.req.post( 36 | `https://member.aliyundrive.com/v1/activity/sign_in_list`, 37 | { isReward: false }, 38 | { authorization: `Bearer ${this.access_token}` } 39 | ); 40 | 41 | if (res.success == true) { 42 | this.signInDay = res.result.signInCount; 43 | const o = this.signInDay - 1; 44 | $.log(`账号 [${this.nick_name} ] 签到成功 ${res.result.signInLogs[o].calendarChinese} \n ${res.result.signInLogs[o].reward.notice}`); 45 | await this.reward(); 46 | } else { 47 | $.log(`❌账号[${this.index}] 签到失败`, 'error'); 48 | console.log(res); 49 | } 50 | 51 | await this.Sendtg_bot(); 52 | } 53 | async reward() { 54 | return $.log('请手动领取签到奖励'); 55 | const { data: res } = await $.req.post( 56 | `https://member.aliyundrive.com/v1/activity/sign_in_reward`, 57 | { signInDay: this.signInDay, month: (new Date().getMonth() + 1) }, 58 | { authorization: `Bearer ${this.access_token}` } 59 | ); 60 | 61 | if (res.success == true) { 62 | $.log(` ${res.result.description || res.result.notice} `); 63 | } else { 64 | $.log(`❌账号[${this.index}] 领取奖励失败`, 'error'); 65 | console.log(res); 66 | } 67 | } 68 | async Sendtg_bot() { 69 | const tg_token = process.env.tg_token; 70 | const tg_chatId = process.env.tg_chatId; 71 | if (!tg_token || !tg_chatId) return; 72 | 73 | const TelegramBot = require('node-telegram-bot-api'); 74 | const bot = new TelegramBot(tg_token); 75 | return bot.sendMessage(tg_chatId, $.getMsgs()); 76 | } 77 | } 78 | // process.env.alyp = ''; 79 | $.init(UserInfo, 'alyp').then(() => $.done()); 80 | -------------------------------------------------------------------------------- /ql_chml.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-07-10 21:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-08 15:27:50 6 | * @Description: 长虹美菱小程序签到 7 | 8 | cron: 50 8 * * * 9 | 环境变量: chmlck 抓包 https://hongke.changhong.com 请求里面的 token 。多账号换行或 & 分割 10 | 示例:export chmlck="eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 11 | */ 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('长虹美菱小程序签到'); 15 | 16 | export async function signCheckIn(token: string) { 17 | // const signUrl = 'https://hongke.changhong.com/gw/applet/aggr/signin?aggrId=608'; 18 | const signUrl = 'https://hongke.changhong.com/gw/applet/aggr/signin?aggrId=661'; 19 | const signRes = await fetch(signUrl, { 20 | method: 'post', 21 | headers: { 22 | 'User-Agent': 23 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.50(0x1800322d) NetType/WIFI Language/zh_CN', 24 | 'Accept-Encoding': 'gzip, deflate', 25 | 'content-type': 'application/json', 26 | 'Referer': 'https://servicewechat.com/wx36c3413e8fe39263/212/page-frame.html', 27 | smarthome: token.trim(), 28 | token: token.trim(), 29 | }, 30 | }).then((d) => d.json()); 31 | 32 | if (signRes.status_code == 200 || String(signRes.message).includes('成功')) { 33 | $.log(`签到:${signRes.message}`); 34 | } else { 35 | console.error(signRes); 36 | $.log(`签到失败:${signRes.message || JSON.stringify(signRes)}`, 'error'); 37 | } 38 | } 39 | 40 | // process.env.chmlck = ''; 41 | if (require.main === module) $.init(signCheckIn, 'chmlck').then(() => $.done()); 42 | -------------------------------------------------------------------------------- /ql_gujing.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-07-10 21:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-09 16:01:17 6 | * @Description: 古井贡酒会员中心小程序 7 | 8 | cron: 55 8 * * * 9 | 环境变量: gujing 抓包 https://scrm.gujing.com/gujing_scrm/ 请求里面的 access-token 10 | 示例:export gujing="eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxx##memberId" 11 | */ 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('古井贡酒会员中心小程序'); 15 | 16 | export async function signCheckIn(token: string) { 17 | $.req.setHeaders({ 18 | 'access-token': token, 19 | Accept: 'application/json, text/plain, */*', 20 | 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Android WebView";v="122"', 21 | 'Content-Type': 'application/json;charset=UTF-8', 22 | Origin: 'https://scrm.gujing.com', 23 | 'X-Requested-With': 'com.tencent.mm', 24 | 'Sec-Fetch-Site': 'same-origin', 25 | 'Sec-Fetch-Mode': 'cors', 26 | 'Sec-Fetch-Dest': 'empty', 27 | 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 28 | 'user-agent': 29 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.50(0x1800323c) NetType/WIFI Language/zh_CN miniProgram/wxba9855bdb1a45c8e', 30 | }); 31 | 32 | const params = { 33 | activityId: '110001000', 34 | preview: false, 35 | }; 36 | 37 | const r = await $.req.post('https://scrm.gujing.com/gujing_scrm/wxclient/mkt/activities/sign:search', params); 38 | // console.log(result); 39 | 40 | if (r.data?.code == 200) { 41 | if (r.data.content.signed !== 1) { 42 | $.log(`未签到 ===> 签到ing`); 43 | const { data } = await $.req.post('https://scrm.gujing.com/gujing_scrm/wxclient/mkt/activities/sign:join', params); 44 | if (data.code == 200 && data.content?.point) { 45 | $.log(`签到成功!获取积分 ${r.data.content.point}`); 46 | } else { 47 | console.log(data); 48 | $.log(`签到失败!${data.chnDesc}`, 'error'); 49 | } 50 | } else { 51 | $.log(`已签到 ===> 什么都不做`); 52 | } 53 | } else { 54 | $.log(`签到失败 [${JSON.stringify(r.data)}]`, 'error'); 55 | } 56 | } 57 | 58 | // process.env.gujing = ''; 59 | if (require.main === module) $.init(signCheckIn, 'gujing').then(() => $.done()); 60 | -------------------------------------------------------------------------------- /ql_hl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-10 11:58:06 6 | * @Description: 哈啰签到。奖励:积攒奖励金可换手机话费重置抵用券 7 | 8 | cron: 50 8 * * * 9 | 环境变量: hlToken 抓包 api.hellobike.com/api?urser 请求里面的 token 10 | 示例:export hlToken="23fexxxxxxxxxxxxxxxxxxxxxxxxxxxx" 11 | */ 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('哈啰签到'); 15 | 16 | export async function signCheckIn(hlToken: string) { 17 | // 签到接口 18 | const signUrl = 'https://api.hellobike.com/api?common.welfare.signAndRecommend'; 19 | const signData = { 20 | from: 'h5', 21 | systemCode: 62, 22 | platform: 4, 23 | version: '6.46.0', 24 | action: 'common.welfare.signAndRecommend', 25 | token: hlToken, 26 | pointType: 1, 27 | }; 28 | const { data: signRes } = await $.req.post(signUrl, signData); 29 | if (signRes.code === 0) { 30 | if (signRes.data.didSignToday === true) { 31 | $.log(`今日已签到成功 金币+${signRes.data.bountyCountToday}。${signRes.data.title}`); 32 | } else { 33 | console.log(signRes); 34 | $.log(`今日未签到,请检查TOKEN是否过期。`, 'error'); 35 | } 36 | } else { 37 | $.log(`签到失败:${JSON.stringify(signRes.msg)}`, 'error'); 38 | console.error(signRes); 39 | return; 40 | } 41 | 42 | const pointInfoUrl = 'https://api.hellobike.com/api?user.taurus.pointInfo'; 43 | const pointInfoData = { 44 | from: 'h5', 45 | systemCode: 61, 46 | platform: 4, 47 | version: '6.46.0', 48 | action: 'user.taurus.pointInfo', 49 | token: hlToken, 50 | pointType: 1, 51 | }; 52 | const { data: pRes } = await $.req.post(pointInfoUrl, pointInfoData); 53 | if (pRes.code === 0 && pRes.data) { 54 | const { points, expiring } = pRes.data; 55 | $.log(`可用奖励金为 ${points},过期 ${expiring}`); 56 | } else { 57 | $.log(`查询奖励金信息失败: ${pRes.msg}`); 58 | console.error(pRes); 59 | } 60 | } 61 | 62 | // process.env.hlToken = ''; 63 | if (require.main === module) $.init(signCheckIn, 'hlToken').then(() => $.done()); 64 | -------------------------------------------------------------------------------- /ql_huluwa.ts: -------------------------------------------------------------------------------- 1 | /** 2 | 葫芦娃预约 v1.06 3 | 4 | cron: 10 10 * * * 5 | const $ = new Env("葫芦娃预约"); 6 | 7 | 抓包把 X-access-token 的值(在请求头里)填到环境变量中, 多账号用 & 隔开(可自定义) 8 | 9 | 环境变量 XLHG_TOKEN 偲源惠购 10 | 环境变量 GLYP_TOKEN 贵旅优品 11 | 环境变量 KGLG_TOKEN 空港乐购 12 | 环境变量 HLQG_TOKEN 航旅黔购 13 | 环境变量 ZHCS_TOKEN 遵航出山 14 | 环境变量 GYQP_TOKEN 贵盐黔品 15 | 环境变量 LLSC_TOKEN 乐旅商城 16 | 环境变量 YLQX_TOKEN 驿路黔寻 17 | 18 | https://blog.168api.cn/jsjiami-com-v7-%e5%9c%a8%e7%ba%bf%e8%a7%a3%e5%af%86 19 | */ 20 | 21 | import { createHmac } from 'node:crypto'; 22 | import type { IncomingHttpHeaders } from 'node:http'; 23 | import moment from 'moment'; 24 | import { assign, dateFormat, Request, sleep } from '@lzwme/fe-utils'; 25 | import { getConfigStorage } from './utils'; 26 | import { Env } from './utils'; 27 | 28 | const $ = new Env('葫芦娃预约'); 29 | const SPLIT = '&'; // 分割符(可自定义) 30 | const config = { 31 | token: { 32 | 偲源惠购: (process.env.XLHG_COOKIE || process.env.XLTH_COOKIE || '').split(SPLIT).filter(Boolean), 33 | 贵旅优品: [] as string[], 34 | 空港乐购: [] as string[], 35 | 航旅黔购: [] as string[], 36 | 遵航出山: [] as string[], 37 | 贵盐黔品: [] as string[], 38 | 乐旅商城: [] as string[], 39 | 驿路黔寻: [] as string[], 40 | }, 41 | }; 42 | const stor = getConfigStorage('葫芦娃预约'); 43 | const req = new Request('', { 'content-type': 'application/json' }); 44 | const constants = { 45 | app: { 46 | 偲源惠购: { key: 'XLHG', channelId: '8', appId: 'wxded2e7e6d60ac09d' }, 47 | 贵旅优品: { key: 'GLYP', channelId: '7', appId: 'wx61549642d715f361' }, 48 | 空港乐购: { key: 'KGLG', channelId: '2', appId: 'wx613ba8ea6a002aa8' }, 49 | 航旅黔购: { key: 'HLQG', channelId: '6', appId: 'wx936aa5357931e226' }, 50 | 遵航出山: { key: 'ZXCS', channelId: '5', appId: 'wx624149b74233c99a' }, 51 | 贵盐黔品: { key: 'GYQP', channelId: '3', appId: 'wx5508e31ffe9366b8' }, 52 | 乐旅商城: { key: 'LLSC', channelId: '1', appId: 'wx821fb4d8604ed4d6' }, 53 | 驿路黔寻: { key: 'YLQX', channelId: '9', appId: 'wxee0ce83ab4b26f9c' }, 54 | }, 55 | apiUrl: 'https://gw.huiqunchina.com', 56 | akskUrl: 'https://callback.huiqunchina.com', 57 | api: { 58 | queryById: '/front-manager/api/customer/queryById/token', 59 | channelActivity: '/front-manager/api/customer/promotion/channelActivity', 60 | appoint: '/front-manager/api/customer/promotion/appoint', 61 | checkCustomerInQianggou: '/front-manager/api/customer/promotion/checkCustomerInQianggou', 62 | }, 63 | }; 64 | const cache = { 65 | ak: '00670fb03584fbf44dd6b136e534f495', 66 | sk: '0d65f24dbe2bc1ede3c3ceeb96ef71bb', 67 | }; 68 | 69 | function hmacSignature(method: string, pathname: string, ak: string, sk: string, date: string) { 70 | const text = method.toUpperCase() + '\n' + pathname + '\n\n' + ak + '\n' + date + '\n'; 71 | return createHmac('sha256', sk).update(text).digest('base64'); 72 | } 73 | function formatHeaders(method: 'get' | 'post', pathname: string, paramsStr: string) { 74 | const date = moment().utc().format('ddd, DD MMM YYYY HH:mm:ss [GMT]'); 75 | return { 76 | 'X-HMAC-SIGNATURE': hmacSignature(method, pathname, cache.ak, cache.sk, date), 77 | 'X-HMAC-ACCESS-KEY': cache.ak, 78 | 'X-HMAC-ALGORITHM': 'hmac-sha256', 79 | 'X-HMAC-DIGEST': createHmac('sha256', cache.sk).update(paramsStr).digest('base64'), 80 | 'X-HMAC-Date': date, 81 | } as IncomingHttpHeaders; 82 | } 83 | function post(pathname: string, data: Record) { 84 | const headers = formatHeaders('post', pathname, JSON.stringify(data)); 85 | return req.post<{ code: string; message: string; data: T }>(constants.apiUrl + pathname, data, headers).then(d => d.data); 86 | } 87 | async function getAkSk(appId: string) { 88 | const { data } = await req.post(`${constants.akskUrl}/api/getInfo`, { appId }); 89 | if (data.code == '10000') assign(cache, data.data); // data.data.ak、sk 90 | else $.log(`获取 ak/sk 异常:${data?.message || data}`, 'error'); 91 | } 92 | async function getWinningCustomers({ activityId = '', activityName = '' }) { 93 | const url = '/front-manager/api/customer/promotion/getWinningCustomers'; 94 | const data = await post(url, { activityId }); 95 | if (data.code != '10000') return $.log(`查询中签结果失败:` + data.message, 'error'); 96 | else $.log(`[${activityName}]中签结果:${data.message}`, data.data ? 'error' : 'info'); 97 | } 98 | async function reservation(appId: string, channelId: string) { 99 | try { 100 | const userInfo = await post<{ phone: string; idcard: string; realName: string }>(constants.api.queryById, { appId }); 101 | if (userInfo.code != '10000') return $.log(userInfo.message, 'error'); 102 | 103 | const activityRes = await post(constants.api.channelActivity, { id: channelId }); 104 | const aData = activityRes.data; 105 | if (activityRes.code != '10000') return $.log(activityRes.message, 'error'); 106 | // console.log(aData) 107 | if (aData.endTime && Date.now() - aData.endTime > 1000 * 60 * 1000) { 108 | $.log(`----暂无新活动。最近活动为「${dateFormat('MM-dd hh:mm:00', aData.endTime)}」【${aData.name}】----`); 109 | return '活动已结束'; 110 | } 111 | 112 | $.log(`当前用户[${userInfo.data.phone}]`); 113 | if (aData.appointCounts > 1 && aData.drawTime < Date.now()) { 114 | $.log( 115 | `[${aData.name}]结果已公布,中签人数[${aData.appointCounts}],${aData.isAppoint ? '您可能已中签,尽快进小程序确认!' : '您未中签'}`, 116 | aData.isAppoint ? 'error' : 'info' 117 | ); 118 | await getWinningCustomers({ activityId: aData.id, activityName: aData.name }); 119 | return; 120 | } 121 | 122 | $.log(`活动名称[${aData.name}]`); 123 | const checkRes = await post(constants.api.checkCustomerInQianggou, { activityId: aData.id, channelId }); 124 | if (checkRes.code != '10000') return $.log(checkRes.message, 'error'); 125 | 126 | if (checkRes.data == false) { 127 | const r = await post(constants.api.appoint, { activityId: aData.id, channelId: channelId }); 128 | $.log(`预约结果[appoint][${r.message}]`, String(r.message).includes('验证码') ? 'error' : 'info'); 129 | } else $.log(`预约结果[已经预约成功,无需重复预约]`); 130 | } catch (error) { 131 | $.log(`运行异常[${(error as Error).message}]`, 'error'); 132 | } 133 | } 134 | async function start() { 135 | await stor.ready(); 136 | assign(config, stor.get()); 137 | 138 | for (let [appName, tokens] of Object.entries(config.token)) { 139 | try { 140 | if (/^[a-zA-Z]$/.test(appName)) { 141 | const item = Object.entries(constants.app).find(d => d[1].key === appName); 142 | if (!item) continue; 143 | appName = item[0]; 144 | } 145 | 146 | const key = Object.entries(constants.app).find(d => d[0] === appName)?.[1].key; 147 | if (key) { 148 | const _tokens = (process.env[`${key}_COOKIE`] || process.env[`${key}_TOKEN`] || '').split(SPLIT).filter(Boolean); 149 | if (_tokens.length > 0) tokens = _tokens; 150 | } 151 | if (!tokens.length) { 152 | $.log(`${appName}: 请配置环境变量 ${key}_TOKEN`); 153 | continue; 154 | } 155 | 156 | const constant = constants.app[appName as keyof typeof constants.app]; 157 | if (!constant) { 158 | $.log(`未知的配置:${appName}`); 159 | continue; 160 | } 161 | $.log(`${appName}预约开始`); 162 | 163 | await getAkSk(constant.appId); 164 | for (const [idx, token] of tokens.entries()) { 165 | $.log('----第' + (idx + 1) + '个号----'); 166 | req.setHeaders({ 167 | 'X-access-token': token.split(token.includes('|') ? '|' : '##')[0].trim(), 168 | 'user-agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x6309080f)XWEB/8461`, 169 | }); 170 | const msg = await reservation(constant.appId, constant.channelId); 171 | if (msg === '活动已结束') break; 172 | 173 | await sleep(1000); 174 | } 175 | $.log(`${appName}预约结束\n`); 176 | } catch (e) { 177 | $.log(`[${appName}]运行异常[${(e as Error).message}]`, 'error'); 178 | } 179 | } 180 | 181 | await $.done(); 182 | } 183 | 184 | start(); 185 | -------------------------------------------------------------------------------- /ql_identical.ts: -------------------------------------------------------------------------------- 1 | /** 2 | new Env("禁用青龙重复脚本") 3 | cron: 20 20 * * * 4 | 环境变量: 5 | - IPPORT 青龙面板访问的地址。若不设置,默认为 http://localhost:5700 6 | - QL_TOKEN 从浏览器请求中获取。但若多点登录时则会失效刷新 7 | - QL_OPENAPI 采用 openAI 方式请求时。格式:clientId&clientSecret 8 | clientId&clientSecret 的获取方式:青龙面板 - 系统设置 - 应用设置 - 创建应用,权限选择“环境变量”和“定时任务” 9 | */ 10 | 11 | import { Request, readJsonFileSync } from '@lzwme/fe-utils'; 12 | import { logger, sendNotify } from './utils'; 13 | import { existsSync } from 'node:fs'; 14 | 15 | const req = new Request('', { 'Content-Type': 'application/json' }); 16 | let host = 'http://localhost:5700'; 17 | 18 | 19 | const T = { 20 | isOpenApi: false, 21 | getHost() { 22 | if (process.env.IPPORT) { 23 | host = process.env.IPPORT; 24 | if (!host.startsWith('http')) host = `http://localhost:${host}`; 25 | } else { 26 | console.log( 27 | `如果报错请在环境变量中添加你的真实 IP:端口\n名称:IPPORT\t值:127.0.0.1:5700\n或在 config.sh 中添加 export IPPORT='127.0.0.1:5700'` 28 | ); 29 | } 30 | }, 31 | async getToken() { 32 | let qlToken = process.env.QL_TOKEN; 33 | if (!qlToken) { 34 | let qlAuthFile = '/ql/data/config/auth.json'; 35 | if (!existsSync(qlAuthFile)) qlAuthFile = '/ql/config/auth.json'; 36 | if (existsSync(qlAuthFile)) qlToken = readJsonFileSync<{ token: string }>(qlAuthFile).token; 37 | } 38 | 39 | if (!qlToken && process.env.QL_OPENAPI) { 40 | const [clientId, clientSecret] = process.env.QL_OPENAPI.split('&'); 41 | 42 | if (clientId && clientSecret) { 43 | type Res = { code: number;message: string; data: { token: string; } }; 44 | const { data } = await req.get( 45 | `/open/auth/token?client_id=${clientId}&client_secret=${clientSecret}` 46 | ); 47 | if (data.data?.token) { 48 | qlToken = data.data.token; 49 | this.isOpenApi = true; 50 | logger.log(`[QL]OpenApi 获取 token 成功!`); 51 | } else logger.error(`[QL]OpenApi 获取 token 异常: ${data.message || JSON.stringify(data)}`); 52 | } 53 | } 54 | 55 | req.setHeaders({ Authorization: `Bearer ${qlToken}` }); 56 | return qlToken; 57 | }, 58 | 59 | async getTaskList() { 60 | const apikey = this.isOpenApi ? 'open' : 'api'; 61 | const { data } = await req.get<{ code: number; data: { data: TaskItem[] } }>(`${host}/${apikey}/crons?searchValue=&t=${Date.now()}`); 62 | return data.data?.data || []; 63 | }, 64 | async disableTask(tasks: TaskItem[]) { 65 | const ids = tasks.map(d => d.id); 66 | const apikey = this.isOpenApi ? 'open' : 'api'; 67 | const { data } = await req.request('PUT', `${host}/${apikey}/crons/disable`, ids); 68 | return data.code === 200 ? '🎉成功禁用重复任务~' : `❌出错!!!错误信息为:${JSON.stringify(data)}`; 69 | }, 70 | 71 | async start() { 72 | const msg: string[] = []; 73 | const disableTaskList: TaskItem[] = []; 74 | 75 | T.getHost(); 76 | if (await T.getToken()) { 77 | const taskMap = new Map(); 78 | const tasklist = await T.getTaskList(); 79 | const disabledList = tasklist.filter(d => d.isDisabled); 80 | 81 | msg.push(`总任务数:${tasklist.length},已禁用:${disabledList.length},已开启:${tasklist.length - disabledList.length}\n`); 82 | 83 | for (const item of tasklist) { 84 | if (!item.command) continue; 85 | if (!taskMap.has(item.name)) taskMap.set(item.name, []); 86 | taskMap.get(item.name)!.push(item); 87 | } 88 | 89 | for (const list of taskMap.values()) { 90 | if (list.length > 1) { 91 | // console.log('发现存在重复的任务:\n', list.map(d => `[${d.name}][${d.command.replace('task ', '')}]`).join('\n')); 92 | const enabledList = list.filter(d => d.isDisabled === 0); 93 | 94 | if (enabledList.length > 1) { 95 | const sorted = enabledList.sort((a, b) => { 96 | if (a.command.includes('lzwme')) return -1; 97 | return b.last_execution_time - a.last_execution_time; 98 | }); 99 | 100 | msg.push(sorted.map((d, i) => `${i ? '【🚫禁用】' : '【✅保留】'}[${d.name}] ${d.command.replace('task ', '')}`).join('\n'), '\n'); 101 | disableTaskList.push(...sorted.slice(1)); 102 | } 103 | } 104 | } 105 | 106 | msg.push(disableTaskList.length ? await T.disableTask(disableTaskList) : '✅没有需要禁用的重复任务'); 107 | } else { 108 | msg.push(`💔禁用重复任务失败!无法获取 token!`); 109 | } 110 | 111 | if (disableTaskList.length) await sendNotify('禁用青龙重复脚本', msg.join('\n')); 112 | else console.log(msg.join('\n')); 113 | } 114 | }; 115 | 116 | T.start(); 117 | 118 | interface TaskItem { 119 | id: number; 120 | name: string; 121 | command: string; 122 | schedule: string; 123 | timestamp: string; 124 | saved: boolean; 125 | status: number; 126 | isSystem: number; 127 | pid: number; 128 | isDisabled: number; 129 | isPinned: number; 130 | log_path: string; 131 | labels: any[]; 132 | last_running_time: number; 133 | last_execution_time: number; 134 | sub_id: number; 135 | extra_schedules?: string; 136 | task_before?: string; 137 | task_after?: string; 138 | createdAt: string; 139 | updatedAt: string; 140 | _disable: boolean; 141 | } 142 | -------------------------------------------------------------------------------- /ql_ikuuu.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-05-16 10:50:43 6 | * @Description: ikuuu机场签到。注册: https://ikuuu.pw/auth/register?code=75PU 7 | 8 | cron: 20 9 * * * 9 | 环境变量: IKUUU,必填。格式: 邮箱#密码,也可以是 cookie(有效期一个星期)。多个账户以 & 或 \n 换行分割 10 | 环节变量: SSPANEL_HOST,可选。可以指定任何基于 SSPANEL 搭建的机场用于签到 11 | 示例:process.env.IKUUU=邮箱1#密码1&邮箱2#密码2 12 | 或:process.env.IKUUU=cookie1&cookie2 13 | */ 14 | import { Env } from './utils'; 15 | 16 | const $ = new Env('ikuuu机场签到'); 17 | 18 | async function getIkuuuHost() { 19 | if (process.env.SSPANEL_HOST) return process.env.SSPANEL_HOST; 20 | let host = 'https://ikuuu.pw'; 21 | try { 22 | const {data:html} = await $.req.get('https://ikuuu.club', {}, { 'content-type': 'text/html' }); 23 | host = /

d.split(';')[0]).join(';'); 54 | $.log(data.msg || `登录成功!`); 55 | cache[cacheKey] = cookie; 56 | $.storage.setItem(`ikuuu_cookie`, cache); 57 | } else { 58 | $.log(data.msg || `登录失败!`, 'error'); 59 | return; 60 | } 61 | } 62 | 63 | $.req.setHeaders({ cookie }); 64 | return checkin(url.checkin); 65 | } 66 | 67 | async function checkin(url: string, isUseCache = false) { 68 | console.log('checkin url:', url); 69 | const { data } = await $.req.request('POST', url, {}, {}, false); 70 | if (data.ret === 1 || String(data.msg).includes('签到过')) { 71 | $.log(`签到成功!${data.msg}`); 72 | return true; 73 | } else { 74 | $.log(`❌签到失败:${data.msg}`, isUseCache ? 'info' : 'error'); 75 | } 76 | return false; 77 | } 78 | 79 | // process.env.IKUUU = ''; 80 | if (require.main === module) $.init(signCheckIn, 'IKUUU').then(() => $.done()); 81 | -------------------------------------------------------------------------------- /ql_imaotai.ts: -------------------------------------------------------------------------------- 1 | /** 2 | I茅台预约 v1.0 3 | 4 | cron: 20 9 * * * 5 | const $ = new Env("I茅台预约"); 6 | 7 | 执行 `tsx src/active/imaotai.ts -m <手机号>`,可以输入收到的验证码,并请求返回 token,并在当前文件夹下保存 8 | 自行抓包并在 lzwme_ql_config.json5 文件中配置 config 信息 9 | 10 | 支持环境变量配置方式(多个账号以 & 或换行分割): 11 | export QL_IMAOTAI=userId=106xxx;token=xxx;tokenWap=xxx;city=北京市;province=北京市&userId=138xxxx;token=xxx... 12 | */ 13 | 14 | import { Request, dateFormat, assign, md5, aesEncrypt, formatToUuid, color, cookieParse } from '@lzwme/fe-utils'; 15 | import { program } from 'commander'; 16 | import { IncomingHttpHeaders } from 'node:http'; 17 | import { homedir, hostname } from 'node:os'; 18 | import { resolve } from 'node:path'; 19 | import { getGeoByGD, getConfigStorage, Env, getLocationByIp } from './utils'; 20 | 21 | // process.env.QL_IMAOTAI=`` 22 | 23 | const $ = new Env('I茅台预约'); 24 | const itemMap: Record = { 25 | 10213: '贵州茅台酒(癸卯兔年)', 26 | 10056: '茅台1935', 27 | 2478: '贵州茅台酒(珍品)', 28 | 10214: '贵州茅台酒(癸卯兔年)x2', 29 | 10941: '贵州茅台酒(甲辰龙年)', 30 | 10942: '贵州茅台酒(甲辰龙年)x2', 31 | }; 32 | const config = { 33 | AMAP_KEY: '', // 高德地图 key,用于命令行方式登录获取经纬度,可以不用 34 | appVersion: '1.7.2', // APP 版本,可以不写,会尝试自动获取 35 | // 预约店铺策略。max: 最大投放量;maxRate: 近30日中签率最高;nearby: 距离最近店铺(默认); keyword: shopKeywords 列表优先 36 | type: 'nearby' as 'max' | 'maxRate' | 'nearby' | 'keyword', 37 | shopKeywords: [], // 店铺白名单:用于指定高优先级的店铺,type=keyword 时,优先查找符合列表关键字的店铺申购 38 | shopKeywordsFilter: [], // 店铺黑名单:用于过滤不希望申购的店铺,避免距离过远无法去领取 39 | user: [ 40 | { 41 | disabled: false, // 是否禁用 42 | userId: '' as string | number, // 用户编号 43 | mobile: '', // 手机号码,用于账号配置识别 44 | itemCodes: [] as string[], // ['10941', '10942'], // 要预约的类型,若不设置,默认过滤 1935 和 珍品 45 | province: 'xx省', 46 | city: 'xx市', 47 | shopKeywords: [] as string[], // 店铺白名单(优先级更高):用于指定高优先级的店铺,若设置了该项,则优先查找符合列表关键字的店铺申购 48 | shopKeywordsFilter: [] as string[], // 店铺黑名单(优先级更高):用于过滤不希望申购的店铺,避免距离过远无法去领取 49 | // 以下项可抓包获取 50 | lng: '', //经度 51 | lat: '', // 纬度 52 | deviceId: formatToUuid(md5(hostname() + homedir()))[0].toUpperCase(), // MT-Device-ID 53 | token: '', // MT-Token 54 | tokenWap: '', // MT-Token-Wap 55 | header: { 56 | // 自定义 header,抓包自己的 header,避免被识别为多设备登录的可能性 57 | h5: { 58 | 'Client-User-Agent': 'iOS;15.0.1;Apple;iPhone 12 ProMax', 59 | } as IncomingHttpHeaders, 60 | app: { 61 | 'User-Agent': 'iOS;16.0.1;Apple;iPhone 14 ProMax', 62 | } as IncomingHttpHeaders, 63 | }, 64 | }, 65 | ], 66 | }; 67 | const defautUser = config.user[0]; 68 | config.user[0] = assign({} as any, defautUser); 69 | 70 | const { green, red, cyan, cyanBright } = color; 71 | const today = dateFormat('yyyy-MM-dd', new Date()); 72 | const time_keys = new Date().setHours(0, 0, 0, 0); 73 | const configStor = getConfigStorage('I茅台预约'); 74 | const cacheInfo = { 75 | info: { 76 | date: '', 77 | sessionInfo: { 78 | sessionId: 0, 79 | itemList: [] as Record<'title' | 'itemCode' | 'content', string>[], 80 | }, 81 | }, 82 | lottery: {} as { [shopId: string]: { [sessionId: string]: { [itemCode: string]: ILottery } } }, 83 | }; 84 | const cacheStor = getConfigStorage>('I茅台预约缓存', resolve(process.cwd(), 'cache/imaotai-cache.json5')); 85 | 86 | const req = new Request('', { 87 | 'MT-User-Tag': '0', 88 | 'Accept-Language': 'zh-Hans-CN;q=1, en-CN;q=0.9', 89 | 'Accept-Encoding': 'gzip, deflate, br', 90 | userId: '1', 91 | 'mt-token': '2', 92 | }); 93 | const mt_r = 'clips_OlU6TmFRag5rCXwbNAQ/Tz1SKlN8THcecBp/'; 94 | const AES_KEY = 'qbhajinldepmucsonaaaccgypwuvcjaa'; 95 | const AES_IV = '2018534749963515'; 96 | const SALT = '2af72f100c356273d46284f6fd1dfc08'; 97 | const imaotai = { 98 | debug: process.env.DEBUG === '1', 99 | /** 所有的店铺信息 */ 100 | mall: {} as { 101 | [shopId: string]: IShopInfo; 102 | }, 103 | user: { ...defautUser }, 104 | async getMap() { 105 | if (!Object.keys(this.mall).length) { 106 | const res = await req.get('https://static.moutai519.com.cn/mt-backend/xhr/front/mall/resource/get'); 107 | const r = await req.get(res.data.data.mtshops_pc.url, {}, { 'content-type': 'application/json' }); 108 | this.mall = r.data; 109 | } 110 | return this.mall; 111 | }, 112 | async getMtv(_MT_K: string) { 113 | return ''; 114 | // try { 115 | // const { data: mtv } = await req.get( 116 | // `http://82.157.10.108:8086/get_mtv?DeviceID=${this.user.deviceId}&MTk=${MT_K}&version=${config.appVersion}&key=yaohuo`, 117 | // {}, 118 | // { 'content-type': 'text/html' }, 119 | // { timeout: 1000 }, 120 | // ); 121 | // return mtv; 122 | // } catch (e) { 123 | // return ''; 124 | // } 125 | }, 126 | async mtAdd(itemId: string, shopId: string, sessionId: number, userId: string) { 127 | const MT_K = Date.now().toString(); 128 | const headers = this.getHeaders({ 'MT-K': MT_K, 'MT-V': await this.getMtv(MT_K) }); 129 | const d = { itemInfoList: [{ count: 1, itemId }], sessionId: sessionId, userId: String(userId), shopId: String(shopId) }; 130 | const actParam = aesEncrypt(JSON.stringify(d), AES_KEY, 'aes-256-cbc', AES_IV).toString('base64'); 131 | const params = { ...d, actParam }; 132 | const r = await req.post('https://app.moutai519.com.cn/xhr/front/mall/reservation/add', params, headers); 133 | if (this.debug) console.log('[mtAdd]', r.data); 134 | if (r.data.code == 2000) return r.data?.data?.successDesc || '未知'; 135 | return '申购失败:' + (r.data.message || JSON.stringify(r.data)); 136 | }, 137 | async getSessionId() { 138 | if (cacheInfo.info.date !== today) { 139 | const r = await req.get<{ data: { itemList: any[]; sessionId: number } }>( 140 | `https://static.moutai519.com.cn/mt-backend/xhr/front/mall/index/session/get/${time_keys}` 141 | ); 142 | if (!r.data.data?.sessionId) console.log('获取 sessionId 失败', JSON.stringify(r.data)); 143 | else { 144 | cacheInfo.info.date = today; 145 | cacheInfo.info.sessionInfo = r.data.data; 146 | cacheStor.save({ info: cacheInfo.info }); 147 | } 148 | } 149 | return cacheInfo.info.sessionInfo || {}; 150 | }, 151 | /** 查询所在省市的投放产品和数量 */ 152 | async queryMallShopList(sessionId: number, itemId: string, province: string, city?: string) { 153 | // https://static.moutai519.com.cn/mt-backend/xhr/front/mall/shop/list/slim/v3/837/%E9%87%8D%E5%BA%86%E5%B8%82/10213/1701100800000 154 | const url = `https://static.moutai519.com.cn/mt-backend/xhr/front/mall/shop/list/slim/v3/${sessionId}/${province}/${itemId}/${time_keys}`; 155 | const r = await req.get<{ 156 | code: number; 157 | data: { 158 | shops: { shopId: string; items: MallShopItem[] }[]; 159 | validTime: number; 160 | items: { title: string; itemId: string; price: string }[]; 161 | }; 162 | }>(url); 163 | const data = r.data.data; 164 | if (!data) { 165 | console.error(`【${city}】未获取到投放店铺数据`, r.data); 166 | return; 167 | } 168 | if (city) { 169 | data.shops = data.shops.filter(shop => this.mall[shop.shopId]?.cityName === city); 170 | } 171 | return data; 172 | }, 173 | /** 查询投放量,返回要预约的店铺 id */ 174 | async getShopItem(sessionId: number, itemId: string, province: string, city: string) { 175 | const data = await this.queryMallShopList(sessionId, itemId, province, city); 176 | const selectedShopItem: { shopId: string; item?: MallShopItem } = { shopId: '' }; 177 | 178 | if (!data) return; 179 | 180 | data.shops = data.shops.filter(s => this.mall[s.shopId]); 181 | 182 | // 黑名单关键词过滤 183 | if (this.user.shopKeywordsFilter?.length > 0) { 184 | data.shops = data.shops.filter(shop => { 185 | const shopInfo = this.mall[shop.shopId]; 186 | for (const keyword of this.user.shopKeywordsFilter) { 187 | if (shopInfo.name.includes(keyword) || shopInfo.name.includes(keyword)) return false; 188 | } 189 | return true; 190 | }); 191 | } 192 | 193 | // 白名单关键词优先 194 | if (config.type === 'keyword') { 195 | for (const keyword of this.user.shopKeywords) { 196 | const t = data.shops.find(shop => { 197 | const shopInfo = this.mall[shop.shopId]; 198 | if (!shopInfo || (!shopInfo.name.includes(keyword) && !shopInfo.fullAddress.includes(keyword))) return false; 199 | 200 | const item = shop.items.find(d => d.itemId == itemId); 201 | if (!item) return false; 202 | selectedShopItem.shopId = shop.shopId; 203 | selectedShopItem.item = item; 204 | return true; 205 | }); 206 | 207 | if (t) return selectedShopItem; 208 | } 209 | } 210 | 211 | // 最大申购率店铺 212 | if (config.type === 'maxRate') { 213 | const r = await this.cityLotteyStat(this.user.city, [itemId]); 214 | 215 | for (const maxItem of r[itemId].list) { 216 | const shop = data.shops.find(d => d.shopId === maxItem.shop.shopId); 217 | if (shop) { 218 | const item = shop.items.find(d => d.itemId == itemId); 219 | if (item) { 220 | // console.log(`选取近N天中签率最高的店铺:[${maxItem.shop.name}][${maxItem.rate}%]`); 221 | selectedShopItem.shopId = shop.shopId; 222 | selectedShopItem.item = item; 223 | return selectedShopItem; 224 | } 225 | } 226 | } 227 | } 228 | 229 | for (const shop of data.shops) { 230 | const item = shop.items.find(d => d.itemId == itemId); 231 | if (!item) continue; 232 | 233 | if (!selectedShopItem.item || selectedShopItem.item.inventory < item.inventory!) { 234 | selectedShopItem.shopId = shop.shopId; 235 | selectedShopItem.item = item; 236 | 237 | if (config.type === 'nearby') return selectedShopItem; 238 | } 239 | } 240 | 241 | return selectedShopItem; 242 | }, 243 | getHeaders(cheaders: IncomingHttpHeaders = {}, type: 'h5' | 'app' = 'app', setcookie = true) { 244 | const header: IncomingHttpHeaders = { 'MT-R': mt_r }; 245 | 246 | if (type === 'app') { 247 | Object.assign(header, { 248 | Accept: '*/*', 249 | 'MT-Bundle-ID': 'com.moutai.mall', 250 | 'content-type': 'application/json', 251 | 'MT-Network-Type': 'WIFI', 252 | 'MT-User-Tag': '0', 253 | 'MT-Info': '028e7f96f6369cafe1d105579c5b9377', 254 | 'MT-Device-ID': this.user.deviceId, 255 | 'MT-Lat': this.user.lat, 256 | 'MT-Lng': this.user.lng, 257 | ...this.user.header.app, 258 | }); 259 | } else { 260 | Object.assign(header, { 261 | Connection: 'keep-alive', 262 | 'X-Requested-With': 'XMLHttpRequest', 263 | Accept: 'application/json, text/javascript, */*; q=0.01', 264 | YX_SUPPORT_WEBP: '1', 265 | 'MT-Device-ID-Wap': this.user.deviceId, 266 | ...this.user.header.h5, 267 | }); 268 | if (setcookie && !header.cookie) { 269 | header.cookie = [`MT-Device-ID-Wap=${this.user.deviceId}`, `MT-Token-Wap='${this.user.tokenWap}`, 'YX_SUPPORT_WEBP=1'].join(';'); 270 | } 271 | } 272 | 273 | header['MT-Request-ID'] = `${Date.now() * 100000 + Math.ceil(10000 * Math.random())}`; 274 | return Object.assign(header, cheaders); 275 | }, 276 | async getUserId(): Promise<{ userName: string; userId: string; mobile: string }> { 277 | const { data: r } = await req.get('https://app.moutai519.com.cn/xhr/front/user/info', {}, this.getHeaders()); 278 | 279 | if (r.code != 2000) { 280 | if (r.data?.version && r.data.version != config.appVersion) { 281 | console.log('不是最新的版本号', config.appVersion, r.data); 282 | config.appVersion = r.data.version; 283 | configStor.save(config); 284 | return this.getUserId() as any; 285 | } 286 | console.log(`[error][getuserid][${this.user.mobile || this.user.userId}]:`, r); 287 | } 288 | return r.data || {}; // userName, userId, mobile 289 | }, 290 | async getAppVersion(isSave = true) { 291 | const f = await req.get('https://apps.apple.com/cn/app/i%E8%8C%85%E5%8F%B0/id1600482450', {}, { 'content-type': 'text/html' }); 292 | const r = String(f.data).match(/whats-new__latest__version.+(\d+\.\d+\.\d+)/); 293 | 294 | if (r && r[1] !== config.appVersion) { 295 | console.log(`获取到新版本:${config.appVersion} => ${r[1]}`); 296 | config.appVersion = r[1]; 297 | if (isSave) configStor.save(config); 298 | } 299 | req.setHeaders({ 'MT-APP-Version': config.appVersion }); 300 | }, 301 | signature(data: Record, timestamp = Date.now().toString()) { 302 | const keys = Object.keys(data).sort(); 303 | const text = SALT + keys.map(k => data[k]).join('') + timestamp; 304 | return md5(text); 305 | }, 306 | /** 获取手机验证码 */ 307 | async getPhoneCode(mobile: string | number, timestamp = Date.now().toString()) { 308 | const params: Record = { mobile }; 309 | params.md5 = this.signature(params, timestamp); 310 | params.timestamp = timestamp; 311 | params['MT-APP-Version'] = config.appVersion; 312 | const headers = this.getHeaders({ 'mt-lat': this.user.lat, 'mt-lng': this.user.lng }); 313 | const { data } = await req.post('https://app.moutai519.com.cn/xhr/front/user/register/vcode', params, headers); 314 | if (+data.code !== 2000) console.log(`发送验证码异常【${mobile}】:`, data); 315 | return data; 316 | }, 317 | /** 指定城市中签率统计 */ 318 | async cityLotteyStat(city = '广州市', itemCodes = ['10941', '10942', '10213', '10214']) { 319 | await imaotai.getMap(); 320 | await imaotai.getSessionId(); 321 | 322 | const shopList = Object.values(imaotai.mall).filter(d => d.cityName == city); 323 | const stats = {} as { [itemCode: string]: { list: Awaited>[]; rankingDetail: string } }; 324 | 325 | for (const itemCode of itemCodes) { 326 | stats[itemCode] = { list: [], rankingDetail: `【${cyanBright(itemMap[itemCode])}】【${cyan(city)}】近30日中签率统计排行:\n` }; 327 | 328 | for (const shop of shopList) { 329 | const info = await shopLotteryStats(shop, { itemCode, days: 30, tryUpdateFailed: false }); 330 | if (info.rate) stats[itemCode].list.push(info); 331 | else if (imaotai.debug) console.log(` > 获取中签率失败:${itemMap[itemCode]} ${shop.name}`); 332 | } 333 | 334 | stats[itemCode].list = stats[itemCode].list.sort((a, b) => b.rate - a.rate); 335 | stats[itemCode].rankingDetail += stats[itemCode].list 336 | .map( 337 | (d, i) => 338 | `[${String(i + 1).padStart(2, '0')}] ${green(d.rate)}% (${cyan(d.hitCntTotal)}/${cyanBright(d.reservationCntTotal)}) [${ 339 | d.shop.name 340 | }]` 341 | ) 342 | .join('\n'); 343 | } 344 | 345 | if (imaotai.debug) { 346 | for (const itemCode of itemCodes) console.log(`\n${stats[itemCode].rankingDetail}\n\n`); 347 | } 348 | 349 | return stats; 350 | }, 351 | /** 登录 */ 352 | async login(mobile: string | number, vCode: string) { 353 | const params: Record = { 354 | mobile, 355 | vCode, 356 | ydToken: '', 357 | ydLogId: '', 358 | }; 359 | const timestamp = Date.now().toString(); 360 | params.md5 = this.signature(params, timestamp); 361 | params.timestamp = timestamp; 362 | params.MT_APP_Version = config.appVersion; 363 | 364 | const { data } = await req.post<{ 365 | code: number; 366 | data: { token: string; userId: number; cookie: string; did: string; verifyStatus: number; idCode: string; birthday: string }; 367 | }>('https://app.moutai519.com.cn/xhr/front/user/register/login', params, this.getHeaders()); 368 | console.log('\n', data.data?.token ? '[login]:' : '登录失败,请重试:', data); 369 | if (data.data?.verifyStatus !== 1) console.warn('请注意,该账号尚未实名认证'); 370 | return data.data; 371 | }, 372 | /** 领取连续申购奖励 */ 373 | async getUserEnergyAward() { 374 | const headers = this.getHeaders({ Referer: 'https://h5.moutai519.com.cn/gux/game/main?appConfig=2_1_2' }, 'h5'); 375 | const r = await req.post('https://h5.moutai519.com.cn/game/isolationPage/getUserEnergyAward', {}, headers); 376 | if (r.data.code !== 2000) { 377 | console.error('领取耐力值失败:', r.data); 378 | $.log(`⚠︎ 领取耐力值:${r.data.message || '领取奖励失败'}`); // , 'error' 379 | } else { 380 | $.log(`🔸领取耐力值:${r.data.message || '领取奖励成功'}`); 381 | } 382 | }, 383 | /** 领取 7 日连续申购 */ 384 | async receive7DaysApplyingReward() { 385 | const qurl = 'https://h5.moutai519.com.cn/game/xmyApplyingReward/7DaysContinuouslyApplyingProgress'; 386 | 387 | type R1 = Res<{ previousProgress: number; appliedToday: boolean; rewardReceived: boolean }>; 388 | const { data: r1 } = await req.post(qurl, {}, this.getHeaders({}, 'h5')); 389 | if (imaotai.debug) console.log(r1); 390 | if (r1.code !== 2000) { 391 | console.log(r1); 392 | return $.log(`❌ 连续申购:${r1.message || r1.error}`, 'error'); 393 | } 394 | 395 | if (r1.data.rewardReceived) return $.log('连续申购:今日已领取'); 396 | if (r1.data.previousProgress < 6) return $.log(`连续申购:[${r1.data.previousProgress}]连续申购不满7日,无法领取`); 397 | 398 | const url = 'https://h5.moutai519.com.cn/game/xmyApplyingReward/receive7DaysContinuouslyApplyingReward'; 399 | const { data: r2 } = await req.post(url, {}, this.getHeaders({}, 'h5')); 400 | if (imaotai.debug) console.log(r2); 401 | if (r2.code !== 2000) { 402 | console.error(r2); 403 | $.log(`❌ 连续申购:${r2.message}`, 'error'); 404 | } else $.log(`🔸连续申购:领取小茅运 +${r2.data?.rewardAmount}`); 405 | }, 406 | /** 领取累计申购奖励 */ 407 | async cumulativelyApplyingDays() { 408 | const qurl = 'https://h5.moutai519.com.cn/game/xmyApplyingReward/cumulativelyApplyingDays'; 409 | type R1 = Res<{ previousDays: number; appliedToday: boolean; rewardReceived: Record<'7' | '14' | '21' | '28', boolean> }>; 410 | const { data: r1 } = await req.post(qurl, {}, this.getHeaders({}, 'h5')); 411 | if (imaotai.debug) console.log(r1); 412 | if (r1.code !== 2000) { 413 | console.error(r1); 414 | return $.log(`❌ 累计申购:${r1.message || r1.error}`, 'error'); 415 | } 416 | 417 | for (const day of [7, 14, 21, 28] as const) { 418 | if (r1.data.rewardReceived[day]) continue; 419 | if (r1.data.previousDays + 1 < day) break; 420 | 421 | const url = `https://h5.moutai519.com.cn/game/xmyApplyingReward/receiveCumulativelyApplyingReward`; 422 | const { data: r2 } = await req.post(url, {}, this.getHeaders({}, 'h5')); 423 | if (imaotai.debug) console.log(r1); 424 | if (r2.code !== 2000) { 425 | $.hasError = true; 426 | console.error(r2); 427 | $.log(`❌ 累计申购${day}天:${r2.message}`, 'error'); 428 | } else { 429 | $.log(`🔸累计申购${day}天:领取小茅运 +${r2.data?.rewardAmount}`); 430 | } 431 | } 432 | }, 433 | async start(inputData = config) { 434 | let userCount = 0; 435 | 436 | try { 437 | await this.getAppVersion(); 438 | await this.getMap(); 439 | const sessionInfo = await this.getSessionId(); 440 | 441 | if (!sessionInfo.sessionId) { 442 | $.log(`❌ 获取 sessionId 失败: ${JSON.stringify(sessionInfo)}`, 'error'); 443 | } else { 444 | for (let user of inputData.user) { 445 | userCount++; 446 | 447 | try { 448 | this.user = user = assign( 449 | {} as any, 450 | { 451 | ...defautUser, 452 | shopKeywords: inputData.shopKeywords, 453 | shopKeywordsFilter: inputData.shopKeywordsFilter, 454 | }, 455 | user, 456 | { 457 | header: { 458 | app: { 'MT-Token': user.token }, 459 | h5: { 'MT-Token-Wap': user.tokenWap }, 460 | }, 461 | } as Partial 462 | ); 463 | 464 | if (user.disabled || !user.token) continue; 465 | 466 | const { userName, userId, mobile } = await this.getUserId(); 467 | if (!userId) { 468 | $.log(`❌ 第 ${userCount} 个用户 token 失效,请重新登录`, 'error'); 469 | continue; 470 | } 471 | 472 | req.setHeaders({ userId }); 473 | $.log(`😀 第 ${userCount} 个用户【${userName}_${mobile}】开始任务`); 474 | 475 | if (!user.itemCodes?.length || !user.itemCodes.some(d => sessionInfo.itemList.some(e => e.itemCode == d))) { 476 | user.itemCodes = sessionInfo.itemList 477 | .filter(d => { 478 | return !['10056', '2478'].includes(d.itemCode); 479 | // const title = String(d.title); 480 | // return title.includes('贵州茅台酒') && !title.includes('珍品'); 481 | }) 482 | .map(d => d.itemCode); 483 | } 484 | 485 | for (const item of sessionInfo.itemList) { 486 | if (user.itemCodes.includes(item.itemCode)) { 487 | const shop = await this.getShopItem(sessionInfo.sessionId, item.itemCode, user.province, user.city); 488 | if (shop?.shopId) { 489 | const shopInfo = this.mall[shop.shopId]; 490 | const r = await this.mtAdd(item.itemCode, shop.shopId, sessionInfo.sessionId, userId); 491 | $.log(`✅ 选中店铺:【${shopInfo.name}】【${shopInfo.fullAddress}】【投放量:${shop.item!.inventory}】`); 492 | $.log(`➡️ [${item.itemCode}_${item.title}]${r}`); 493 | } else { 494 | $.log(`❌ [${item.itemCode}_${item.title}]未获取到可预约的店铺,未能预约`, 'error'); 495 | } 496 | } 497 | } 498 | 499 | await this.getUserEnergyAward(); 500 | await this.receive7DaysApplyingReward(); 501 | await this.cumulativelyApplyingDays(); 502 | } catch (err) { 503 | console.error(err); 504 | $.log(`[${userCount}]error: ${(err as Error).message || JSON.stringify(err)}`, 'error'); 505 | } 506 | } 507 | } 508 | } catch (err) { 509 | console.error(err); 510 | $.log(`❌ error: ${(err as Error).message || JSON.stringify(err)}`, 'error'); 511 | } 512 | console.log(`执行完毕。共执行了 ${userCount} 个账号`); 513 | 514 | await $.done(); 515 | }, 516 | }; 517 | 518 | async function promptLogin(opts: { login: boolean; force?: boolean }) { 519 | const { prompt } = (await import('enquirer')).default; 520 | const inputData = { 521 | AMAP_KEY: process.env.AMAP_KEY || config.AMAP_KEY, 522 | vcode: '', 523 | }; 524 | const { mobile } = await prompt<{ mobile: string }>([ 525 | { 526 | type: 'input', 527 | name: 'mobile', 528 | message: '请输入登录使用的 11 位手机号码', 529 | initial: '', 530 | validate: mobile => /\d{11}/.test(mobile) || '请输入正确的11位手机号码', 531 | }, 532 | ]); 533 | 534 | const existUser = config.user.find(d => d.mobile === mobile); 535 | if (existUser) imaotai.user = existUser; 536 | 537 | imaotai.user.mobile = mobile; 538 | 539 | if (!imaotai.user.lat || opts.force) { 540 | if (!inputData.AMAP_KEY) { 541 | const { AMAP_KEY } = await prompt<{ AMAP_KEY: string }>({ 542 | type: 'input', 543 | name: 'AMAP_KEY', 544 | initial: config.AMAP_KEY, 545 | message: '请输入高德地图 KEY (用于获取经纬度,没有则直接回车下一步)', 546 | }); 547 | if (AMAP_KEY.length > 20) config.AMAP_KEY = inputData.AMAP_KEY = AMAP_KEY; 548 | } 549 | 550 | if (inputData.AMAP_KEY) { 551 | const { address } = await prompt<{ address: string }>([ 552 | { 553 | type: 'input', 554 | name: 'address', 555 | message: '请输入您当前的位置或要预约的地点(如:北京市朝阳区xxx路xx小区)', 556 | initial: '', 557 | validate: address => address.length > 4 || '输入字符太少', 558 | }, 559 | ]); 560 | const list = await getGeoByGD(address, inputData.AMAP_KEY); 561 | const { idx } = await prompt<{ idx: string }>({ 562 | type: 'select', 563 | name: 'idx', 564 | message: '请选择最近的一个位置', 565 | choices: list.map(d => ({ 566 | name: d.formatted_address, 567 | message: `地址:${d.formatted_address}, 定位:${d.location}`, 568 | })), 569 | }); 570 | const item = list.find(d => d.formatted_address === idx) || list[0]; 571 | const [lng, lat] = item.location.split(','); 572 | const t = { 573 | province: item.province, 574 | city: item.city, 575 | lat: +lat < +lng ? lat : lng, 576 | lng: +lat > +lng ? lat : lng, 577 | }; 578 | assign(imaotai.user, t); 579 | } else { 580 | await prompt([ 581 | { 582 | type: 'input', 583 | name: 'province', 584 | message: '请输入预约的省份(如广东省)', 585 | initial: imaotai.user.province, 586 | validate: async province => { 587 | imaotai.user.province = province.trim(); 588 | return /省|市$/.test(province.trim()) || '输入格式不正确'; 589 | }, 590 | }, 591 | { 592 | type: 'input', 593 | name: 'city', 594 | message: '请输入预约的城市(如广州市)', 595 | initial: imaotai.user.city, 596 | validate: async city => { 597 | imaotai.user.city = city.trim(); 598 | return /市$/.test(city.trim()) || '输入格式不正确'; 599 | }, 600 | }, 601 | { 602 | type: 'input', 603 | name: 'location', 604 | message: '请输入预约位置经纬度,逗号分割', 605 | validate: async location => { 606 | if (!/\d+.\d+,\d+.\d+/.test(location.trim())) return '输入格式不正确'; 607 | const [lng, lat] = location.split(','); 608 | imaotai.user.lat = +lat < +lng ? lat : lng; 609 | imaotai.user.lng = +lat > +lng ? lat : lng; 610 | 611 | return true; 612 | }, 613 | }, 614 | ]); 615 | } 616 | } 617 | 618 | const r = await imaotai.getPhoneCode(mobile); 619 | if (r.code != 2000) { 620 | console.error('发送验证码错误', r); 621 | return; 622 | } 623 | 624 | await prompt([ 625 | { 626 | type: 'input', 627 | name: 'vcode', 628 | message: '请输入接收到的验证码', 629 | validate: async vcode => { 630 | if (!/\d+/.test(vcode)) return false; 631 | const data = await imaotai.login(mobile, vcode); 632 | if (data?.token) { 633 | inputData.vcode = vcode; 634 | imaotai.user.token = data.token; 635 | imaotai.user.tokenWap = data.cookie || ''; 636 | imaotai.user.userId = data.userId; 637 | } 638 | return Boolean(data?.token); 639 | }, 640 | }, 641 | ]); 642 | 643 | if (!existUser) config.user.push(imaotai.user); 644 | 645 | for (const key in imaotai.user) if (!imaotai.user[key as never]) delete imaotai.user[key as never]; 646 | configStor.save(config); 647 | 648 | console.log(`获取到token信息:`, imaotai.user.token); 649 | // @ts-ignore 650 | console.log('请在配置文件中补充完善配置:', configStor.options.filepath, '\n', JSON.stringify(imaotai.user, null, 2)); 651 | } 652 | 653 | async function shopLotteryStats(shop: IShopInfo, { itemCode = '10213', sessionId = 0, days = 30, tryUpdateFailed = false }) { 654 | if (!sessionId) { 655 | await imaotai.getSessionId(); 656 | sessionId = cacheInfo.info.sessionInfo.sessionId; 657 | } 658 | if (!cacheInfo.lottery[shop.shopId]) cacheInfo.lottery[shop.shopId] = {}; 659 | 660 | const req = new Request(); 661 | const sessionList = new Array(days) 662 | .fill(+sessionId) 663 | .map((v, idx) => v - idx) 664 | .filter(v => v > 100); 665 | 666 | let failedCount = 0; 667 | for (let sId of sessionList) { 668 | if (!cacheInfo.lottery[shop.shopId][sId]) cacheInfo.lottery[shop.shopId][sId] = {}; 669 | 670 | const item = cacheInfo.lottery[shop.shopId][sId][itemCode]; 671 | if (item && (!tryUpdateFailed || item.hitCnt)) continue; 672 | // 连续失败次数大于 5,则不再继续获取 673 | if (failedCount >= 3) { 674 | cacheInfo.lottery[shop.shopId][sId][itemCode] = {} as never; 675 | continue; 676 | } 677 | 678 | const url = `https://static.moutai519.com.cn/mt-backend/mt/lottery/${sId}/${itemCode}/${shop.shopId}/page1.json?csrf_token`; 679 | const { data } = await req.get<{ code: string; data: ILottery }>(url); 680 | console.log(`[get][${data.data?.lotteryDate ? green('ok') : red('failed')}]`, cyan(url)); 681 | if (data.data?.lotteryDate) { 682 | const info = (cacheInfo.lottery[shop.shopId][sId][itemCode] = data.data); 683 | info.rate = Math.floor((info.hitCnt / info.reservationCnt) * 100000) / 1000; 684 | info.rateAll = Math.floor((info.allItemDetail.hitCnt / info.allItemDetail.reservationCnt) * 100000) / 1000; 685 | failedCount = 0; 686 | } else { 687 | if (imaotai.debug && (typeof data !== 'string' || String(data).includes('xml '))) console.log(red(' > 获取失败:'), data); 688 | cacheInfo.lottery[shop.shopId][sId][itemCode] = {} as never; 689 | failedCount++; 690 | } 691 | } 692 | cacheStor.save(cacheInfo); 693 | 694 | const list = sessionList.map(sId => cacheInfo.lottery[shop.shopId][sId][itemCode]).filter(d => d?.hitCnt); 695 | const rankingInfo = list 696 | .sort((a, b) => b.rate - a.rate) 697 | .map((v, idx) => `${idx}. 【${dateFormat('yyyy-MM-dd', v?.lotteryDate)}】 中签率:${v.rate}%(${v.hitCnt}/${v.reservationCnt})`); 698 | const result = { hitCntTotal: 0, reservationCntTotal: 0, rate: 0, rankingInfo, itemCode, shop }; 699 | 700 | list.forEach(item => { 701 | result.hitCntTotal += item.hitCnt; 702 | result.reservationCntTotal += item.reservationCnt; 703 | }); 704 | result.rate = Math.floor((result.hitCntTotal / result.reservationCntTotal) * 100000) / 1000; 705 | 706 | if (imaotai.debug && result.hitCntTotal) { 707 | console.log( 708 | `[${green(itemMap[itemCode])}][${cyan(shop.name)}]店铺近${cyan(days)}日平均中签率:${green(result.rate)}% (${result.hitCntTotal} / ${ 709 | result.reservationCntTotal 710 | })。中签率排名:\n`, 711 | result.rankingInfo.join('\n') 712 | ); 713 | } 714 | 715 | return result; 716 | } 717 | 718 | program 719 | .option('-l,--login', '是否位 login 模式,登录模式会写入到配置文件中供预约使用。默认为预约模式') 720 | .option('-f, --force', '强制模式') 721 | .option('-s, --stat [city]', '统计指定城市中签率') 722 | .option('-d, --debug', '调试模式') 723 | .action(async (opts: { login: boolean; force?: boolean; debug: boolean; stat?: string }) => { 724 | await configStor.ready(); 725 | assign(config, configStor.get()); 726 | assign(cacheInfo, cacheStor.get()); 727 | if (opts.debug) imaotai.debug = opts.debug; 728 | 729 | // 支持按 deviceId 从环境变量读取已配置的值 730 | if (process.env.QL_IMAOTAI) { 731 | const list = process.env.QL_IMAOTAI.split(process.env.QL_IMAOTAI.includes('&') ? '&' : '\n'); 732 | 733 | for (const line of list) { 734 | const item: Partial<(typeof config)['user'][0]> = cookieParse(line); 735 | 736 | if (item.token) { 737 | const o = config.user.find(d => d.mobile === item.mobile || d.userId === item.userId); 738 | 739 | if (o) { 740 | Object.assign(o, item); 741 | } else { 742 | if (!item.city) { 743 | const t = await getLocationByIp(); 744 | if (t) { 745 | item.city = t.city; 746 | item.province = t.province; 747 | } 748 | } 749 | 750 | if (item.city) { 751 | if (!config.user[0].userId) Object.assign(config.user[0], item); 752 | else config.user.push(item as never); 753 | } 754 | } 755 | } 756 | } 757 | } 758 | 759 | if (opts.stat) { 760 | imaotai.debug = true; 761 | await imaotai.cityLotteyStat(typeof opts.stat === 'string' ? opts.stat : '广州市'); 762 | } else { 763 | await imaotai.getAppVersion(false); 764 | opts.login ? promptLogin(opts) : imaotai.start(); 765 | } 766 | }) 767 | .parseAsync(); 768 | 769 | type ILottery = { 770 | rate: number; 771 | rateAll: number; 772 | actualInvCnt: number; 773 | hitCnt: number; 774 | lotteryDate: string; 775 | reservationCnt: number; 776 | allItemDetail: { actualInvCnt: number; hitCnt: number; hitDisCnt: number; reservationCnt: number; reservationDisCnt: number }; 777 | }; 778 | type IShopInfo = { 779 | lat: number; 780 | lng: number; 781 | city: number; 782 | cityName: string; 783 | province: number; 784 | fullAddress: string; 785 | layaway: boolean; 786 | provinceName: string; 787 | name: string; 788 | shopId: string; 789 | }; 790 | type MallShopItem = { count: number; itemId: string; ownerName: string; maxReserveCount: number; inventory: number }; 791 | type Res = { 792 | code: number; 793 | message?: string; 794 | data: T; 795 | error?: string; 796 | }; 797 | -------------------------------------------------------------------------------- /ql_install_whistle.x-scripts.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-04-03 19:33:28 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-10 11:58:38 6 | * @Description: 7 | * 8 | * new Env('whistle.x-scripts 插件安装与更新') 9 | * cron: 0 18 * * * 10 | */ 11 | import fs from 'node:fs'; 12 | import { dirname, resolve } from 'node:path'; 13 | import { execPromisfy, rmrf, mkdirp } from '@lzwme/fe-utils'; 14 | 15 | const githubProxyUrl = process.env.GH_PROXY_URL ?? ''; 16 | const baseDir = process.env.QL_WHISTLE_BASEDIR || '/ql/data/scripts/whistle/'; 17 | 18 | const repoList = ['x-scripts-rules', 'whistle.x-scripts']; 19 | 20 | async function updateRepo(repoName: string) { 21 | if (repoName === 'whistle.x-scripts' && process.env.WS_GLOBAL_INSTALL !== '0') { 22 | await execPromisfy(`npm i -g @lzwme/whistle.x-scripts`); 23 | return; 24 | } 25 | 26 | const dir = resolve(baseDir, repoName); 27 | 28 | if (fs.existsSync(resolve(dir, '.git/config'))) { 29 | await execPromisfy(`git fetch --all && git reset --hard remotes/origin/main`, true, { cwd: dir }); 30 | } else { 31 | rmrf(dir); 32 | await execPromisfy(`git clone ${githubProxyUrl}https://github.com/lzwme/${repoName}.git`, true, { cwd: dirname(dir) }); 33 | } 34 | 35 | if (repoName === 'whistle.x-scripts') { 36 | await execPromisfy('pnpm install && pnpm build && npm link .', true, { cwd: dir }); 37 | } 38 | } 39 | 40 | async function start() { 41 | mkdirp(baseDir); 42 | process.chdir(baseDir); 43 | console.log(process.cwd()); 44 | 45 | const r = await execPromisfy('w2 stop', true, { cwd: baseDir }); 46 | if (r.stderr) await execPromisfy('npm i -g whistle', true, { cwd: baseDir }); 47 | for (const repoName of repoList) await updateRepo(repoName); 48 | 49 | await execPromisfy('w2 start', true, { cwd: baseDir }); 50 | } 51 | 52 | start() 53 | .catch((e) => console.error(e)) 54 | .finally(() => process.exit()); 55 | -------------------------------------------------------------------------------- /ql_ipzan_signin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-01-09 10:11:48 6 | * @Description: 品赞 HTTP 代理签到。品赞是一个HTTP优质代理IP服务供应商。 7 | 8 | const $ = new Env("品赞代理签到"); 9 | cron: 10 0 * * 0 10 | 11 | 环境变量: export IPZAN_ACCOUNT="phone=手机号;pwd=密码;token=xxx" 12 | 13 | phone: 登录账号 pwd: 登录密码 14 | token: (可选)抓取请求 header 中的 Authorization 设置为 token 值。但容易超时失效 15 | 16 | 多账号使用换行或 & 分割。 17 | token 与 账号密码取其一即可。若担心设置账号与密码会泄露,可只设置为 token 值。但容易超时失效 18 | 19 | 奖励:每周签到得 3 金币(1 金币约等于 1 块钱),可获取有效期 3分钟的 IP 约 500 次。代理 IP 可用于多账号任务并发执行,避免被检测到而封号 20 | 注册地址: https://www.ipzan.com?pid=e9rdab1c 注册赠送 10 金币,可免费获取时效 3 分钟的 IP 代理 1666 次 21 | */ 22 | 23 | import { cookieParse } from '@lzwme/fe-utils'; 24 | import { Env } from './utils'; 25 | 26 | const $ = new Env('品赞代理签到'); 27 | 28 | export async function signCheckIn(cookie: string) { 29 | const info = cookieParse(cookie); 30 | let token = info.token; 31 | 32 | if (info.phone && info.pwd) token = (await login(info.phone, info.pwd)) || info.token; 33 | if (!token) return $.log('品赞代理签到: 未设置账号与密码或 token'); 34 | 35 | $.req.setHeaders({ authorization: `Bearer ${token.replace('Bearer ', '')}` }); 36 | 37 | const { data: pRes } = await $.req.get('https://service.ipzan.com/home/userWallet-receive'); 38 | 39 | if (pRes.code === 0 || String(pRes.message).includes('已领取')) $.log(`签到成功:${pRes.message}`); 40 | else if (pRes.message === '登录已过期' && info.phone && info.pwd) { 41 | await login(info.phone, info.pwd, false); 42 | await signCheckIn(cookie); 43 | } else { 44 | $.log(`签到失败: ${pRes.message}`, 'error'); 45 | console.error(pRes); 46 | } 47 | } 48 | 49 | export async function login(phone: string, password: string, useCache = true) { 50 | const cacheKey = `ipzan_token_${phone}`; 51 | const d = $.storage.getItem(cacheKey); 52 | 53 | if (useCache && d?.token && Date.now() - d.t < 24 * 60 * 60 * 1000) return d.token; 54 | 55 | let e = c.encode(''.concat(phone, 'QWERIPZAN1290QWER').concat(password)); 56 | let t = ''; 57 | for (let o = 0; o < 80; o++) t += Math.random().toString(16).slice(2); 58 | e = '' 59 | .concat(t.slice(0, 100)) 60 | .concat(e.slice(0, 8)) 61 | .concat(t.slice(100, 200)) 62 | .concat(e.slice(8, 20)) 63 | .concat(t.slice(200, 300)) 64 | .concat(e.slice(20)) 65 | .concat(t.slice(300, 400)); 66 | 67 | const params = { 68 | account: e, 69 | source: 'ipzan-home-one', 70 | }; 71 | const { data } = await $.req.post<{ code: number; message: string; data: { token: string } }>( 72 | 'https://service.ipzan.com/users-login', 73 | params 74 | ); 75 | 76 | if (data.code !== 0) $.log(`登录失败: ${data.message}`, 'error'); 77 | else { 78 | $.storage.setItem(cacheKey, { token: data.data.token, t: Date.now() }); 79 | return data.data.token; 80 | } 81 | 82 | return ''; 83 | } 84 | 85 | const c = { 86 | // prettier-ignore 87 | table: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"], 88 | UTF16ToUTF8: function (e: string) { 89 | for (var t = [], n = e.length, i = 0; i < n; i++) { 90 | var r, 91 | o, 92 | s = e.charCodeAt(i); 93 | 0 < s && s <= 127 94 | ? t.push(e.charAt(i)) 95 | : 128 <= s && s <= 2047 96 | ? ((r = 192 | ((s >> 6) & 31)), (o = 128 | (63 & s)), t.push(String.fromCharCode(r), String.fromCharCode(o))) 97 | : 2048 <= s && 98 | s <= 65535 && 99 | ((r = 224 | ((s >> 12) & 15)), 100 | (o = 128 | ((s >> 6) & 63)), 101 | (s = 128 | (63 & s)), 102 | t.push(String.fromCharCode(r), String.fromCharCode(o), String.fromCharCode(s))); 103 | } 104 | return t.join(''); 105 | }, 106 | UTF8ToUTF16: function (e: string) { 107 | for (var t = [], n = e.length, i = 0, i = 0; i < n; i++) { 108 | var r, 109 | o, 110 | s = e.charCodeAt(i); 111 | 0 == ((s >> 7) & 255) 112 | ? t.push(e.charAt(i)) 113 | : 6 == ((s >> 5) & 255) 114 | ? ((o = ((31 & s) << 6) | (63 & (r = e.charCodeAt(++i)))), t.push(String.fromCharCode(o))) 115 | : 14 == ((s >> 4) & 255) && 116 | ((o = ((255 & ((s << 4) | (((r = e.charCodeAt(++i)) >> 2) & 15))) << 8) | (((3 & r) << 6) | (63 & e.charCodeAt(++i)))), 117 | t.push(String.fromCharCode(o))); 118 | } 119 | return t.join(''); 120 | }, 121 | encode: function (e: string) { 122 | if (!e) return ''; 123 | for (var t = this.UTF16ToUTF8(e), n = 0, i = t.length, r = []; n < i; ) { 124 | var o = 255 & t.charCodeAt(n++); 125 | if ((r.push(this.table[o >> 2]), n == i)) { 126 | r.push(this.table[(3 & o) << 4]), r.push('=='); 127 | break; 128 | } 129 | var s = t.charCodeAt(n++); 130 | if (n == i) { 131 | r.push(this.table[((3 & o) << 4) | ((s >> 4) & 15)]), r.push(this.table[(15 & s) << 2]), r.push('='); 132 | break; 133 | } 134 | var a = t.charCodeAt(n++); 135 | r.push(this.table[((3 & o) << 4) | ((s >> 4) & 15)]), 136 | r.push(this.table[((15 & s) << 2) | ((192 & a) >> 6)]), 137 | r.push(this.table[63 & a]); 138 | } 139 | return r.join(''); 140 | }, 141 | decode: function (e: string) { 142 | if (!e) return ''; 143 | for (var t = e.length, n = 0, i = []; n < t; ) { 144 | let code1 = this.table.indexOf(e.charAt(n++)); 145 | let code2 = this.table.indexOf(e.charAt(n++)); 146 | let code3 = this.table.indexOf(e.charAt(n++)); 147 | let code4 = this.table.indexOf(e.charAt(n++)); 148 | let c1 = (code1 << 2) | (code2 >> 4); 149 | let c2, c3; 150 | 151 | i.push(String.fromCharCode(c1)); 152 | -1 != code3 && ((c2 = ((15 & code2) << 4) | (code3 >> 2)), i.push(String.fromCharCode(c2))); 153 | -1 != code4 && ((c3 = ((3 & code3) << 6) | code4), i.push(String.fromCharCode(c3))); 154 | } 155 | return this.UTF8ToUTF16(i.join('')); 156 | }, 157 | }; 158 | 159 | // process.env.IPZAN_ACCOUNT = ''; 160 | if (require.main === module) $.init(signCheckIn, 'IPZAN_ACCOUNT').then(() => $.done()); 161 | -------------------------------------------------------------------------------- /ql_iqiyi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-03-12 23:52:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-05-30 09:21:02 6 | * @See {https://ghpr.cc/https://raw.githubusercontent.com/NobyDa/Script/master/iQIYI-DailyBonus/iQIYI.js | 参考} 7 | cron: 25 8 * * * 8 | new Env('爱奇艺签到') 9 | 环境变量: IQIYI_COOKIE 抓取请求中的 cookie 。多账户用 & 或换行分割 10 | */ 11 | 12 | import { AnyObject, md5, sleep, toQueryString, cookieParse } from '@lzwme/fe-utils'; 13 | import { Env } from './utils'; 14 | 15 | const $ = new Env('爱奇艺签到'); 16 | $.init(start, 'IQIYI_COOKIE').then(() => $.done()); 17 | 18 | let P00001 = ''; 19 | let P00003 = ''; 20 | let dfp = ''; 21 | 22 | async function start(cookie: string) { 23 | const ckObj = cookieParse(cookie); 24 | P00001 = ckObj.P00001; 25 | P00003 = ckObj.P00003 || ckObj.P00010; 26 | dfp = ckObj.dfp || ''; 27 | 28 | if (P00001 && P00003) { 29 | await login(); 30 | await Checkin(); 31 | sleep(1000); 32 | await webCheckin(); 33 | sleep(1000); 34 | await webtask(); 35 | 36 | for (let i = 0; i < 3; i++) { 37 | const run = await Lottery(); 38 | if (run) await sleep(1000); 39 | else break; 40 | } 41 | 42 | const tasks = await getTaskList(); 43 | for (const task of tasks) { 44 | if (task.status === 4) { 45 | $.log(`💓任务[${task.name}]进行中,需手动完成`); 46 | console.log(`--------------------`); 47 | } else if (task.status !== 1) { 48 | //0:待领取 1:已完成 2:未开始 4:进行中 49 | await joinTask(task); 50 | await notifyTask(task); 51 | await sleep(1000); 52 | await getTaskRewards(task); 53 | console.log(`--------------------`); 54 | } 55 | } 56 | } else { 57 | console.log(`Cookie缺少关键值,需重新获取`); 58 | } 59 | } 60 | 61 | async function login() { 62 | const url = 63 | `https://cards.iqiyi.com/views_category/3.0/vip_home?secure_p=iPhone&scrn_scale=0&dev_os=0&ouid=0&layout_v=6&psp_cki=${P00001}` + 64 | '&page_st=suggest&app_k=8e48946f144759d86a50075555fd5862&dev_ua=iPhone8%2C2&net_sts=1&cupid_uid=0&xas=1&init_type=6&app_v=11.4.5&idfa=0&app_t=0&platform_id=0&layout_name=0&req_sn=0&api_v=0&psp_status=0&psp_uid=451953037415627&qyid=0&secure_v=0&req_times=0'; 65 | const headers = { sign: '7fd8aadd90f4cfc99a858a4b087bcc3a', t: '479112291' }; 66 | const { data } = await $.req.get(url, void 0, headers); 67 | if (data.code == 0) $.log(`爱奇艺查询成功!VIP会员${data.base?.exp_time || '-'}天后到期`); 68 | else $.log(`爱奇艺查询失败!$${data.message || JSON.stringify(data)}`, 'error'); 69 | } 70 | 71 | async function Checkin() { 72 | const sign_date = { 73 | task_code: 'natural_month_sign', 74 | timestamp: Date.now(), 75 | appKey: 'lequ_rn', 76 | userId: P00003, 77 | authCookie: P00001, 78 | agenttype: 20, 79 | agentversion: '15.4.6', 80 | srcplatform: 20, 81 | appver: '15.4.6', 82 | qyid: md5(stringRandom(16)), 83 | // agentType: '1', 84 | // typeCode: 'point', 85 | }; 86 | const post_date = { 87 | natural_month_sign: { 88 | verticalCode: 'iQIYI', 89 | agentVersion: '15.4.6', 90 | authCookie: P00001, 91 | taskCode: 'iQIYI_mofhr', 92 | dfp: dfp, 93 | qyid: md5(stringRandom(16)), 94 | agentType: 20, 95 | signFrom: 1, 96 | }, 97 | }; 98 | const sign = k('cRcFakm9KSPSjFEufg3W', sign_date, { split: '|', sort: true, splitSecretKey: true }); 99 | const url = `https://community.iqiyi.com/openApi/task/execute?${toQueryString(sign_date)}&sign=${sign}`; 100 | const { data: obj } = await $.req.post(url, post_date); 101 | 102 | if (obj.code === 'A00000') { 103 | if (obj.data.code === 'A0000') { 104 | $.log(`签到成功!累计签到 ${obj.data?.data?.signDays} 天`); 105 | for (let i = 0; i < obj.data.data.rewards.length; i++) { 106 | if (obj.data.data.rewards[i].rewardType == 1) { 107 | $.log(` 成长值+${obj.data.data.rewards[i].rewardCount}`); 108 | } else if (obj.data.data.rewards[i].rewardType == 2) { 109 | $.log(` VIP天+${obj.data.data.rewards[i].rewardCount}`); 110 | } else if (obj.data.data.rewards[i].rewardType == 3) { 111 | $.log(` 积分+${obj.data.data.rewards[i].rewardCount}`); 112 | } 113 | } 114 | } else { 115 | $.log(`应用签到: ${obj.data.msg} ⚠️`); 116 | } 117 | } else { 118 | $.log(`应用签到: [${obj.msg || obj.message || 'Cookie无效'}]⚠️`, 'error'); 119 | } 120 | } 121 | 122 | /** 网页版签到 */ 123 | async function webCheckin() { 124 | if (!dfp) return; // $.log('网页版签到:未发现 dfp 参数,任务忽略'); 125 | 126 | const sign_date = { 127 | agenttype: '1', 128 | agentversion: '0', 129 | appKey: 'basic_pca', 130 | appver: '0', 131 | authCookie: P00001, 132 | channelCode: 'sign_pcw', 133 | dfp: dfp, 134 | scoreType: '1', 135 | srcplatform: '1', 136 | typeCode: 'point', 137 | userId: P00003, 138 | user_agent: 139 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0', 140 | verticalCode: 'iQIYI', 141 | }; 142 | const sign = k('UKobMjDMsDoScuWOfp6F', sign_date, { split: '|', sort: true, splitSecretKey: true }); 143 | const url = `https://community.iqiyi.com/openApi/score/add?${toQueryString(sign_date)}&sign=${sign}`; 144 | const { data: obj } = await $.req.get(url); 145 | 146 | if (obj.code === 'A00000') { 147 | if (obj.data?.[0]?.code === 'A0000') { 148 | $.log(`网页端签到成功: 获得积分${obj.data[0].score}, 累计签到 ${obj.data[0].continuousValue} 天`); 149 | } else { 150 | $.log(`网页端签到: ${obj.data?.[0].msg} ⚠️`); 151 | } 152 | } else { 153 | $.log(`网页端签到失败: [${obj.msg || obj.message || 'Cookie无效'}]⚠️`, 'error'); 154 | } 155 | } 156 | /** 网页端访问热点首页任务 */ 157 | async function webtask() { 158 | if (!dfp) return; // $.log('网页版热点任务:未发现 dfp 参数,任务忽略'); 159 | 160 | const sign_date = { 161 | agenttype: '1', 162 | agentversion: '0', 163 | appKey: 'basic_pca', 164 | appver: '0', 165 | authCookie: P00001, 166 | channelCode: 'paopao_pcw', 167 | dfp, 168 | scoreType: '1', 169 | srcplatform: '1', 170 | typeCode: 'point', 171 | userId: P00003, 172 | user_agent: 173 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0', 174 | verticalCode: 'iQIYI', 175 | }; 176 | const sign = k('UKobMjDMsDoScuWOfp6F', sign_date, { split: '|', sort: true, splitSecretKey: true }); 177 | const paramStr = toQueryString(sign_date); 178 | const url = `https://community.iqiyi.com/openApi/task/complete?${paramStr}&sign=${sign}`; 179 | 180 | try { 181 | const { data: res } = await $.req.get(url); 182 | 183 | if (res.code === 'A00000') { 184 | $.log(`网页端访问任务成功:${res.message}`); 185 | 186 | // 领取奖励 187 | const rewardUrl = `https://community.iqiyi.com/openApi/score/getReward?${paramStr}&sign=${sign}`; 188 | const { data: rewardRes } = await $.req.get(rewardUrl); 189 | // console.log('rewardRes', rewardRes); 190 | if (rewardRes.code === 'A00000') { 191 | $.log(`网页端访问任务成功:获得${rewardRes.data.score}积分`); 192 | } else { 193 | $.log(`网页端访问任务失败:${rewardRes.message}`); 194 | } 195 | } else { 196 | $.log(`网页端访问任务失败:${res.message}`); 197 | } 198 | } catch (error) { 199 | console.error('请求失败:', error); 200 | } 201 | } 202 | // 抽奖 203 | async function Lottery() { 204 | const url = 205 | 'https://iface2.iqiyi.com/aggregate/3.0/lottery_activity?app_k=0&app_v=0&platform_id=0&dev_os=0&dev_ua=0&net_sts=0&qyid=0&psp_uid=0&psp_cki=' + 206 | P00001 + 207 | '&psp_status=0&secure_p=0&secure_v=0&req_sn=0'; 208 | const { data: obj } = await $.req.get(url); 209 | 210 | if (obj.code != 0) { 211 | $.log(`[${obj.title || '应用抽奖'}]${obj.errorReason}`); 212 | return false; 213 | } else if (obj.title) { 214 | $.log(`应用抽奖: ${(obj.title != '影片推荐' && obj.awardName) || '未中奖'} 🎉`); 215 | if (obj.kv.code == 'Q00702') { 216 | $.log(`应用抽奖: 您的抽奖次数已经用完 ⚠️`); 217 | return false; 218 | } 219 | } else if (obj.kv.code == 'Q00304') { 220 | $.log(`应用抽奖: Cookie无效 ⚠️`, 'error'); 221 | return false; 222 | } else { 223 | $.log(`应用抽奖: 未知错误 ⚠️`, 'error'); 224 | return false; 225 | } 226 | 227 | return true; 228 | } 229 | 230 | async function getTaskList() { 231 | const { data: obj } = await $.req.get(`https://tc.vip.iqiyi.com/taskCenter/task/queryUserTask?P00001=${P00001}`); 232 | if (obj.code == 'A00000' && obj.data && obj.data.tasks) { 233 | const taskList: TaskItem[] = []; 234 | Object.values(obj.data.tasks).forEach((d: any) => { 235 | if (!Array.isArray(d)) return; 236 | d.forEach(item => 237 | taskList.push({ 238 | name: item.taskTitle || item.name, 239 | taskCode: item.taskCode || item.code, 240 | status: item.status, 241 | }) 242 | ); 243 | }); 244 | 245 | $.log(`获取任务列表成功!共有任务 ${obj.data.count} 个,已完成 ${obj.data.finishedCount} 个`); 246 | return taskList; 247 | } else { 248 | $.log(`获取任务列表失败!`); 249 | } 250 | return []; 251 | } 252 | 253 | async function joinTask(task: TaskItem) { 254 | const { data } = await $.req.get( 255 | `https://tc.vip.iqiyi.com/taskCenter/task/joinTask?taskCode=${task.taskCode}&lang=zh_CN&platform=0000000000000000&P00001=${P00001}` 256 | ); 257 | $.log(`领取任务:${task.name} => ${data.msg || data.code || JSON.stringify(data)}`); 258 | } 259 | 260 | async function notifyTask(task: TaskItem) { 261 | const { data } = await $.req.get( 262 | `https://tc.vip.iqiyi.com/taskCenter/task/notify?taskCode=${task.taskCode}&lang=zh_CN&platform=0000000000000000&P00001=${P00001}` 263 | ); 264 | 265 | $.log(`爱奇艺-开始任务: ${task.name} => ${data.msg || data.code || JSON.stringify(data)}`); 266 | } 267 | 268 | async function getTaskRewards(task: TaskItem) { 269 | const { data } = await $.req.get( 270 | `https://tc.vip.iqiyi.com/taskCenter/task/getTaskRewards?taskCode=${task.taskCode}` + 271 | `&lang=zh_CN&platform=0000000000000000&P00001=${P00001}` 272 | ); 273 | 274 | if (data.msg === '成功' && data.code === 'A00000' && data.dataNew[0] !== undefined) { 275 | $.log(`任务奖励: ${task.name} => ${data.dataNew[0].name + data.dataNew[0].value} 🎉`); 276 | } else { 277 | $.log(`任务奖励: ${task.name} => ${(data.msg !== `成功` && data.msg) || `未完成`} ⚠️`); 278 | } 279 | } 280 | 281 | function k(e: string, t: AnyObject, a: { split: string; sort: boolean; splitSecretKey: boolean }) { 282 | let c = void 0 == a.split ? '|' : a.split, 283 | s = void 0 === a.sort || a.sort, 284 | o = a.splitSecretKey, 285 | i = void 0 !== o && o, 286 | l = s ? Object.keys(t).sort() : Object.keys(t), 287 | u = l.map(k => `${k}=${t[k]}`).join(c) + (i ? c : '') + e; 288 | return md5(u); 289 | } 290 | function stringRandom(length: number) { 291 | let rdm62 = 0; 292 | let ret = ''; 293 | while (length--) { 294 | rdm62 = 0 | (Math.random() * 62); 295 | ret += String.fromCharCode(rdm62 + (rdm62 < 10 ? 48 : rdm62 < 36 ? 55 : 61)); 296 | } 297 | return ret; 298 | } 299 | 300 | type TaskItem = { 301 | name: string; 302 | taskCode: string; 303 | /** 任务状态。 0:待领取 1:已完成 2:未开始 4:进行中 */ 304 | status: 1 | 2 | 3 | 4; 305 | }; 306 | -------------------------------------------------------------------------------- /ql_jsbaxfls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | cron: 40 8 * * * 3 | new Env('杰士邦安心福利社-小程序') 4 | 环境变量: jsbaxfls 抓取 https://xh-vip-api.a-touchin.com/mp/sign/applyV2 请求头 Headers 中 access-token 的值 多账户 & 或换行分割,或新建同名变量 5 | */ 6 | 7 | import { Env } from './utils'; 8 | const $ = new Env('杰士邦安心福利社-小程序'); 9 | 10 | class UserInfo { 11 | private nick_name = ''; 12 | constructor(token: string, private index: number) { 13 | console.log(token); 14 | $.req.setHeaders({ 15 | 'access-token': token, 16 | referer: 'https://servicewechat.com/wx9a2dc52c95994011/98/page-frame.html', 17 | 'user-agent': 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090b19)XWEB/11253', 19 | platform: 'MP-WEIXIN', 20 | connection: 'keep-alive', 21 | charset: 'utf-8', 22 | 'content-type': 'application/json;charset=utf-8', 23 | 'accept-encoding': 'gzip,compress,br,deflate', 24 | sid: '10009', 25 | }); 26 | } 27 | async start() { 28 | if (!(await this.userInfo())) return false; 29 | await this.signInInfo(); 30 | await this.taskShare(); 31 | 32 | return true; 33 | } 34 | async sign() { 35 | // todo: 获取限时任务 36 | // 'https://member.alipan.com/v2/activity/sign_in_info' data.result.rewards[] 37 | 38 | const { data: res } = await $.req.post( 39 | `https://member.aliyundrive.com/v1/activity/sign_in_list`, 40 | { isReward: false }, 41 | { authorization: `Bearer ${this.access_token}` } 42 | ); 43 | 44 | if (res.success == true) { 45 | this.signInDay = res.result.signInCount; 46 | const o = this.signInDay - 1; 47 | $.log(`账号 [${this.nick_name} ] 签到成功 ${res.result.signInLogs[o].calendarChinese} \n ${res.result.signInLogs[o].reward.notice}`); 48 | await this.reward(); 49 | } else { 50 | $.log(`❌账号[${this.index}] 签到失败`); 51 | console.log(res); 52 | } 53 | 54 | await this.Sendtg_bot(); 55 | } 56 | async userInfo() { 57 | try { 58 | const { data: result } = await $.req.get>(`https://xh-vip-api.a-touchin.com/mp/user/info`); 59 | if (result.status == 200) { 60 | this.nick_name = result.data.userInfo.nick_name; 61 | $.log(`✅账号[${this.index}][${this.nick_name}] 当前积分[${result.data.userInfo.points}]🎉`); 62 | return true; 63 | } else { 64 | $.log(`❌账号[${this.index}] 获取用户信息失败[${result.message}]`); 65 | console.log(result); 66 | return false; 67 | } 68 | } catch (e) { 69 | console.log(e); 70 | $.log(`❌${(e as Error).message}`); 71 | return false; 72 | } 73 | } 74 | async signInInfo() { 75 | try { 76 | const { data: result } = await $.req.get(`https://xh-vip-api.a-touchin.com/mp/sign/infoV2`); 77 | if (result.status == 200) { 78 | $.log(`✅账号[${this.nick_name}] 当天签到状态[${result.data.today_is_signed}]🎉`); 79 | if (!result.data.today_is_signed) { 80 | await this.taskSignIn(); 81 | } 82 | } else { 83 | $.log(`❌账号[${this.nick_name}] 当天签到状态[${result.message}]`); 84 | console.log(result); 85 | } 86 | } catch (e) { 87 | console.log(e); 88 | } 89 | } 90 | async taskSignIn() { 91 | try { 92 | const { data: result } = await $.req.get(`https://xh-vip-api.a-touchin.com/mp/sign/applyV2`); 93 | if (result.status == 200 || String(result.message).includes('ok')) { 94 | $.log(`✅账号[${this.nick_name}] 签到执行状态[${result.message}]🎉`); 95 | } else { 96 | $.log(`❌账号[${this.nick_name}] 签到执行状态[${result.message}]`); 97 | console.log(result); 98 | } 99 | } catch (e) { 100 | console.log(e); 101 | } 102 | } 103 | async taskShare() { 104 | try { 105 | const { data: result } = await $.req.get( 106 | `https://xh-vip-api.a-touchin.com/mp/guess.home/share?project_id=pages%2Fguess%2Findex%3Fproject_id%3D333480658633344` 107 | ); 108 | if (result.status == 200) { 109 | $.log(`✅账号[${this.nick_name}] 分享执行状态[${result.message || result.msg}]🎉`); 110 | } else { 111 | $.log(`❌账号[${this.nick_name}] 分享执行状态[${result.message || result.msg}]`); 112 | console.log(result); 113 | } 114 | } catch (e) { 115 | console.log(e); 116 | } 117 | } 118 | } 119 | // process.env.jsbaxfls = ''; 120 | $.init(UserInfo, 'jsbaxfls').then(() => $.done()); 121 | 122 | interface Res { 123 | status: number; 124 | message: string; 125 | msg?: string; 126 | data: T; 127 | } 128 | -------------------------------------------------------------------------------- /ql_ssone.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-05-16 10:50:54 6 | * @Description: ssone机场签到。注册: https://m.ssonecloud.com/register?aff=vap2VlUi 7 | 8 | cron: 21 9 * * * 9 | 环节变量: V2BOARD_HOST 可选。可以指定任何基于 V2Board 搭建的机场用于签到 10 | 环境变量: SSONE 必填。格式: 邮箱#密码#HOST,HOST 可选。也可以是 cookie(有效期一个星期)。多个账户以 & 或 \n 换行分割 11 | 示例:process.env.SSONE=邮箱1#密码1&邮箱2#密码2#HOST 12 | 或:process.env.SSONE=cookie1&cookie2 13 | */ 14 | import { Env } from './utils'; 15 | 16 | const $ = new Env('ssone机场签到'); 17 | 18 | async function getHost() { 19 | return process.env.V2BOARD_HOST || 'https://m.ssonecloud.com/'; 20 | } 21 | 22 | export async function signCheckIn(cfg: string) { 23 | const [email, passwd, HOST = await getHost()] = cfg.split('#'); 24 | const url = { 25 | login: `${HOST}/api/?action=login`, 26 | checkin: `${HOST}/skyapi?action=checkin`, 27 | }; 28 | let cookie = passwd ? '' : email; 29 | // @ts-ignore 30 | Object.entries(url).forEach(([key, value]) => (url[key] = value.replaceAll('//', '/'))); 31 | 32 | if (!cookie) { 33 | const { data, headers } = await $.req.get(url.login, { email, password: passwd }); 34 | if (data.data) { 35 | $.req.setHeaders({ cookie: `auth=${data.data}` }); 36 | cookie = headers['set-cookie']!.map(d => d.split(';')[0]).join(';'); 37 | $.log(data.msg || `登录成功!`); 38 | } else { 39 | console.log(data); 40 | $.log(data.msg || `登录失败!`, 'error'); 41 | return; 42 | } 43 | } 44 | 45 | $.req.setHeaders({ cookie }); 46 | 47 | const { data } = await $.req.get(url.checkin); 48 | if (data.ret === 1 || String(data.message).includes('Already')) { 49 | $.log(`签到成功!${data.message}`); 50 | } else { 51 | $.log(`❌签到失败:${data.message}`, 'error'); 52 | } 53 | } 54 | 55 | // process.env.SSONE = ''; 56 | if (require.main === module) $.init(signCheckIn, 'SSONE').then(() => $.done()); 57 | -------------------------------------------------------------------------------- /ql_thsSignIn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-05-07 10:20:31 6 | * @Description: 同花顺签到。奖励:积分换优惠券、积分抽奖(只有虚拟奖品,接口数据里没有实物) 7 | 8 | cron: 55 8 * * * 9 | 环境变量: ths_cookie 抓包 https://eq.10jqka.com.cn 请求 header 里面的 cookie。多账号用 & 或换行分割 10 | 示例:export ths_cookie="" 11 | */ 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('同花顺签到'); 15 | 16 | /** 签到 */ 17 | export async function signCheckIn(cookie: string) { 18 | const signUrl = 'https://eq.10jqka.com.cn/integralV2/signIn'; 19 | const userId = cookie.match(/userid=(\d+)/)?.[1] || ''; 20 | $.req.setHeaders({ 21 | cookie, 22 | accept: 'application/json', 23 | referer: 'https://eq.10jqka.com.cn/frontend/integralCenter/index.html', 24 | 'User-Agent': 25 | `Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 hxFont/normal getHXAPPAdaptOldSetting/0 Language/zh-Hans getHXAPPAccessibilityMode/0 hxnoimage/0 getHXAPPFontSetting/normal VASdkVersion/1.0.0 VoiceAssistantVer/0 hxtheme/0 IHexin/11.50.41 (Royal Flush) userid/${userId} innerversion/I037.08.517 build/11.50.41 surveyVer/0 isVip/0`, 26 | }); 27 | const { data: signRes } = await $.req.get(signUrl); 28 | 29 | if (signRes.status_code == 0) { 30 | if (signRes.data?.integral) { 31 | $.log(`[${userId}]签到成功,获取积分: ${signRes.data?.integral}。已连续签到 ${signRes.data.continuousSignInDays} 天`); 32 | } else { 33 | const item = signRes.data?.signInRecordList?.[0]; 34 | if (item?.status) $.log(`[${userId}]今日已签到,获取积分:${item.integral}。已连续签到 ${signRes.data.continuousSignInDays} 天`); 35 | } 36 | return true; 37 | } else { 38 | console.error(signRes); 39 | $.log(`[${userId}]签到失败:${signRes.status_msg || JSON.stringify(signRes)}`, 'error'); 40 | } 41 | 42 | return false; 43 | } 44 | 45 | /** 做任务赚积分 */ 46 | async function doTasks() { 47 | const { data: tasks } = await $.req.get(`https://eq.10jqka.com.cn/integralV2/taskManagementAll`); 48 | 49 | if (Array.isArray(tasks.data)) { 50 | console.log(`查询到了 ${tasks.data?.length} 个任务,开始执行任务`); 51 | if (tasks.data.every(d => d.taskStatus === 2)) return console.log(`所有任务均已完成`); 52 | 53 | $.req.setHeaders({ referer: 'https://eq.10jqka.com.cn/frontend/integralCenter/gotPoints.html' }); 54 | 55 | for (const task of tasks.data) { 56 | if (task.taskStatus === 0) { 57 | $.log(`开始做任务:${task.taskName}`); 58 | const { data } = await $.req.get(`https://eq.10jqka.com.cn/integralV2/completeTask?id=${task.id}`); 59 | if (data.status_code !== 0) $.log(`任务[${task.taskName}]完成失败!${data.status_msg || JSON.stringify(data)}`); 60 | else task.taskStatus = 1; 61 | await $.wait(1000, 500); 62 | } 63 | 64 | // 任务已完成,领取积分 65 | if (task.taskStatus === 1) { 66 | const { data } = await $.req.get(`https://eq.10jqka.com.cn/integralV2/receiveTaskReward?id=${task.id}`); 67 | if (data.status_code === 0) { 68 | $.log(`任务[${task.taskName}]领取了 ${task.rewardIntegral} 积分。当前总积分 ${data.data}`); 69 | } else { 70 | $.log(`任务[${task.taskName}]领取积分失败!${data.status_msg || JSON.stringify(data)}`); 71 | } 72 | await $.wait(1000, 500); 73 | } 74 | } 75 | } else { 76 | console.log(tasks); 77 | $.log(`获取任务列表失败!${tasks.status_msg || tasks.status_code}`); 78 | } 79 | } 80 | 81 | async function start(cookie: string) { 82 | if (false === await signCheckIn(cookie)) return; 83 | await doTasks(); 84 | } 85 | 86 | // process.env.ths_cookie = ``; 87 | if (require.main === module) $.init(start, 'ths_cookie').then(() => $.done()); 88 | -------------------------------------------------------------------------------- /ql_videoqq.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-23 13:52:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-03-18 18:18:48 6 | * @see https://raw.githubusercontent.com/ClydeTime/Quantumult/main/Script/Task/videoqq.js 7 | cron: 36 7 * * * 8 | new Env('腾讯视频VIP会员签到') 9 | 注意,非会员无法签到。环境变量: 10 | export videoqq_cookie='' : 抓取 APP 请求中的 Cookie 11 | 下面两个不抓取也没关系,但可能时效性会比较短,容易频繁过期 12 | export videoqq_cookie_pc='' : 抓取 PC 网页请求中的 Cookie 13 | export videoqq_ref_url='' : 抓取 PC 网页请求的 ref_url: 获取教程: https://cdn.jsdelivr.net/gh/BlueskyClouds/Script/img/2020/11/1/img/v_1.jpg 14 | */ 15 | 16 | import { Env } from './utils'; 17 | 18 | const $ = new Env('腾讯视频VIP会员签到', { sep: ['\n', '@'] }); 19 | // process.env.videoqq_cookie = ''; 20 | // process.env.videoqq_cookie_pc = ''; 21 | // process.env.process.env.videoqq_ref_url = ''; 22 | $.init(start, 'videoqq_cookie').then(() => $.done()); 23 | 24 | let videoqq_cookie_pc = ''; 25 | // ref_url获取教程: 「https://cdn.jsdelivr.net/gh/BlueskyClouds/Script/img/2020/11/1/img/v_1.jpg」 26 | let videoqq_ref_url = process.env.videoqq_ref_url || ''; 27 | let auth: Record = {}; 28 | 29 | let headers = { 30 | Referer: `https://film.video.qq.com/`, 31 | 'User-Agent': 32 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Mobile/11A465 QQLiveBrowser/8.7.45 AppType/UN WebKitCore/WKWebView iOS GDTTangramMobSDK/4.370.6 GDTMobSDK/4.370.6 cellPhone/iPhone 12 pro max', 33 | }; 34 | 35 | async function start(cookie: string) { 36 | $.req.setHeaders({ ...headers, cookie }); 37 | auth = getAuth(cookie); 38 | if (videoqq_ref_url) await refCookie(videoqq_ref_url); 39 | 40 | let sign_flag = await txVideoSignIn(); 41 | await txVideoDownTasks(); 42 | 43 | if (sign_flag) { 44 | let message = `🟢【恭喜】签到状态:签到成功 \n`; 45 | $.log(message); 46 | } else { 47 | let message = `🔴【抱歉】签到状态:签到失败 \n` + '请重新获取cookie'; 48 | $.log(message); 49 | } 50 | } 51 | 52 | const parseSet = (cookie: string) => { 53 | const obj: Record = {}; 54 | cookie = cookie.replace(/\GMT, /g, 'GMT;'); 55 | const arr = cookie.split(';'); 56 | arr.forEach(function (val) { 57 | const brr = val.split('='); 58 | obj[brr[0]] = brr[1]; 59 | }); 60 | return obj; 61 | }; 62 | 63 | function getAuth(cookie: string) { 64 | let needParams = ['']; 65 | const obj: Record = {}; 66 | 67 | //适配微信登录 68 | if (cookie) { 69 | if (cookie.includes('main_login=wx')) { 70 | needParams = [ 71 | 'vdevice_qimei36', 72 | 'video_platform', 73 | 'pgv_pvid', 74 | 'pgv_info', 75 | 'video_omgid', 76 | 'main_login', 77 | 'access_token', 78 | 'appid', 79 | 'openid', 80 | 'vuserid', 81 | 'vusession', 82 | ]; 83 | } else if (cookie.includes('main_login=qq')) { 84 | needParams = [ 85 | 'vdevice_qimei36', 86 | 'video_platform', 87 | 'pgv_pvid', 88 | 'video_omgid', 89 | 'main_login', 90 | 'vqq_access_token', 91 | 'vqq_appid', 92 | 'vqq_openid', 93 | 'vqq_vuserid', 94 | 'vqq_vusession', 95 | ]; 96 | } else { 97 | $.log('getAuth - 无法提取有效cookie参数', 'error'); 98 | } 99 | 100 | cookie.split('; ').forEach(t => { 101 | const [key, val] = t.split(/\=(.*)$/, 2); 102 | if (needParams.includes(key)) obj[key] = val; 103 | }); 104 | } 105 | 106 | return obj; 107 | } 108 | 109 | async function refCookie(url: string) { 110 | const { headers, data } = await $.req.get(url, {}, { 'content-type': 'text/html', cookie: videoqq_cookie_pc }); 111 | const { vusession, vqq_vusession, access_token } = parseSet(headers['set-cookie']?.join(';') as string); 112 | //微信多一个 access_token 113 | if (typeof vusession != 'undefined') { 114 | auth['vusession'] = vusession; 115 | auth['access_token'] = access_token; 116 | } else { 117 | auth['vqq_vusession'] = vqq_vusession; 118 | } 119 | 120 | const cookie = Object.keys(auth) 121 | .map(i => i + '=' + auth[i]) 122 | .join('; '); 123 | $.req.setHeaders({ 124 | ...headers, 125 | cookie, 126 | }); 127 | 128 | if (data.match(/nick/)) { 129 | //通过验证获取QQ昵称参数来判断是否正确 130 | $.log('验证成功,执行主程序'); 131 | } else { 132 | $.log('验证ref_url失败,无法获取个人资料 ref_url 或 Cookie 失效 ‼️‼️'); 133 | console.error(data); 134 | } 135 | } 136 | 137 | /** 手机端签到 */ 138 | async function txVideoSignIn() { 139 | const { data } = await $.req.get(`https://vip.video.qq.com/rpc/trpc.new_task_system.task_system.TaskSystem/CheckIn?rpc_data=%7B%7D`); 140 | if (data != null) { 141 | const { ret: code, check_in_score } = data; 142 | if (code === 0 && check_in_score != undefined) { 143 | $.log('腾讯视频会员手机端签到成功:签到分数:' + check_in_score + '分 🎉'); 144 | } else if (code === -2002) { 145 | $.log('腾讯视频会员手机端签到失败:重复签到 ‼️‼️'); 146 | } else if (code === -2007) { 147 | $.log('腾讯视频会员签到:非会员无法签到'); 148 | } else { 149 | $.log('腾讯视频会员手机端签到失败:未知错误请查看控制台输出 ‼️‼️\n' + data); 150 | } 151 | } else { 152 | $.log('腾讯视频会员签到:签到失败, Cookie失效 ‼️‼️'); 153 | } 154 | 155 | return data?.ret == 0; 156 | } 157 | 158 | /** 观看60分钟任务签到请求 */ 159 | async function txVideoDownTasks() { 160 | const { data } = await $.req.get( 161 | `https://vip.video.qq.com/rpc/trpc.new_task_system.task_system.TaskSystem/ProvideAward?rpc_data=%7B%22task_id%22:1%7D` 162 | ); 163 | const { ret: code, check_in_score } = data; 164 | 165 | if (code === 0 && check_in_score != undefined) { 166 | $.log('腾讯视频会员观看任务签到成功:签到分数:' + check_in_score + '分 🎉'); 167 | } else if (code === -2002) { 168 | $.log('腾讯视频会员观看任务签到成功:重复签到 ‼️‼️'); 169 | } else if (code === -2003) { 170 | $.log('腾讯视频会员观看任务签到失败:任务未完成 ‼️‼️'); 171 | } else if (code === -2007) { 172 | $.log('腾讯视频会员签到:非会员无法签到'); 173 | } else { 174 | $.log('腾讯视频会员观看任务签到成功:未知错误请查看控制台输出 ‼️‼️\n'); 175 | console.error(data); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /ql_weather.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2025-04-17 20:50:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-04-21 09:07:01 6 | * 每日天气推送。API 参考: https://www.sojson.com/api/weather.html 7 | cron: 30 7 1 1 1 8 | new Env('每日天气') 9 | 10 | 环境变量: 11 | export city_code='101280101' # 城市code,可访问这个网址搜索查找: https://fastly.jsdelivr.net/gh/Oreomeow/checkinpanel@master/city.json 12 | */ 13 | 14 | import { sendNotify } from './utils'; 15 | 16 | interface WeatherData { 17 | cityInfo: { 18 | city: string; 19 | }; 20 | data: { 21 | forecast: Array<{ 22 | ymd: string; 23 | week: string; 24 | type: string; 25 | high: string; 26 | low: string; 27 | fx: string; 28 | fl: string; 29 | notice: string; 30 | }>; 31 | shidu: string; 32 | quality: string; 33 | pm25: string; 34 | pm10: string; 35 | ganmao: string; 36 | }; 37 | time: string; 38 | } 39 | 40 | async function start(city_code?: string) { 41 | if (!city_code) city_code = process.env.city_code; // 北京: 101010100 广州 101280101 42 | if (!city_code) { 43 | console.log( 44 | 'city_code 环境变量未配置。可访问该网址查找你的城市:https://fastly.jsdelivr.net/gh/Oreomeow/checkinpanel@master/city.json' 45 | ); 46 | return; 47 | } 48 | 49 | const data: WeatherData = await fetch(`http://t.weather.itboy.net/api/weather/city/${city_code}`).then(d => d.json()); 50 | // console.log(data); 51 | 52 | // 当天天气信息 53 | const today = data.data.forecast[0]; 54 | const msg = [ 55 | `城市:${data.cityInfo.city}`, 56 | `日期:${today.ymd} ${today.week}`, 57 | `天气:${today.type}`, 58 | `温度:${today.high} ${today.low}`, 59 | `湿度:${data.data.shidu}`, 60 | `空气质量:${data.data.quality}`, 61 | `PM2.5:${data.data.pm25}`, 62 | `PM10:${data.data.pm10}`, 63 | `风力风向:${today.fx} ${today.fl}`, 64 | `感冒指数:${data.data.ganmao}`, 65 | `[💌]温馨提示:${today.notice}`, 66 | `更新时间:${data.time}`, 67 | ].join('\n'); 68 | 69 | // 未来 N 天天气预报 70 | const sevenDaysWeather = data.data.forecast.map(day => [ 71 | day.ymd.replaceAll('-', '').slice(4), 72 | day.week.replace('星期', ''), 73 | `${day.low}~${day.high}`.replace('低温 ', '').replace('高温 ', ''), 74 | day.type, 75 | // day.notice, 76 | ]); 77 | const formattedSevenDays = sevenDaysWeather.map(day => day.join(' ')).join('\n'); 78 | const body = `${msg}\n\n${formattedSevenDays}`; 79 | 80 | await sendNotify(`${data.cityInfo.city}今日天气`, body, { notifyType: 2, isPrint: true }); 81 | } 82 | 83 | start() 84 | .catch(error => console.error('程序运行失败:', error)) 85 | .finally(() => process.exit()); 86 | -------------------------------------------------------------------------------- /ql_xmlySign.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-23 13:52:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-05-16 10:44:35 6 | * @see https://github.com/ClydeTime/Quantumult/blob/main/Script/Task/xmlySign.js 7 | cron: 35 7 * * * 8 | new Env('喜马拉雅签到') 9 | 环境变量: xmly_cookie 抓取请求中的 Cookie 。多账户用 @ 或换行分割 10 | */ 11 | 12 | import { assign } from '@lzwme/fe-utils'; 13 | import { Env } from './utils'; 14 | 15 | const $ = new Env('喜马拉雅签到', { sep: ['\n', '@'] }); 16 | $.init(start, 'xmly_cookie').then(() => $.storage.setItem(configKey, config)).then(() => $.done()); 17 | 18 | let configKey = 'xmlyConfig'; 19 | let startTime = Date.now(); 20 | let xm_cookie = ''; 21 | const config = { 22 | watch: { 23 | num: 0, 24 | time: 0, 25 | }, 26 | spec: { 27 | num: 0, 28 | time: 0, 29 | }, 30 | }; 31 | 32 | async function start(cookie: string) { 33 | await $.storage.ready(); 34 | assign(config, $.storage.getItem(configKey)); 35 | 36 | xm_cookie = cookie = cookie.replaceAll('%26', '&'); 37 | $.req.setHeaders({ 'user-agent': 'ting/9.2.24.3 CFNetwork/1494.0.7 Darwin/23.4.0', cookie }); 38 | startTime = Date.now(); 39 | 40 | const sign_flag = await xmlySign(); 41 | 42 | if (sign_flag) { 43 | let watch_message = ''; 44 | let spec_message = ''; 45 | 46 | if (check('watch', 5)) { 47 | let exec_times = 6 - config.watch.num; 48 | $.log('### 看广告任务进行中'); 49 | for (let i = 0; i < exec_times; i++) { 50 | let token = await adVideoGetToken(); 51 | if (token != 'null') { 52 | await adVideoFinish(token); 53 | } else { 54 | $.log('- 获取token失败,无法完成观看任务'); 55 | } 56 | } 57 | if (config.watch.num == 6) { 58 | watch_message = `🟢 今日视频任务已全部完成`; 59 | } else { 60 | watch_message = `🟡 今日视频任务尚未完成`; 61 | } 62 | } else { 63 | watch_message = `🟢 今日视频任务已全部完成`; 64 | } 65 | $.log(watch_message); 66 | 67 | if (check('spec', 5)) { 68 | await share(); 69 | await voiceAdd(); 70 | await voiceDelete(); 71 | await giveDynamicsLike(); 72 | await cancelDynamicsLike(); 73 | await giveVoiceLike(); 74 | await cancelVoiceLike(); 75 | await userAdd(); 76 | await userDelete(); 77 | /* let actCode = await jumpDzdp() 78 | if (actCode != "") { 79 | await dzdpComplete(actCode) 80 | } */ 81 | let uid = await getUid(); 82 | let content = urlencode(await wyy()); 83 | let commentId = await createComment(uid, content); 84 | if (commentId != 0) { 85 | await deleteComment(commentId); 86 | } else { 87 | $.log('- 评论失败,无法删除评论'); 88 | $.log('- 遇到此种情况,没有很好的解决办法,建议手动评论并交还任务'); 89 | } 90 | 91 | await flushTaskRecords(); 92 | $.log('### 特殊任务统一交还中'); 93 | config.spec.num = 0; 94 | config.spec.time = Date.now(); 95 | $.storage.setItem(configKey, config); 96 | 97 | let listset = [96, 168, 169, 170, 171, 336]; //任务列表分别为「分享声音, 收藏声音, 动态点赞, 声音点赞, 关注用户, 声音评论(172变更336), 大众点评(217已失效)」 98 | for (let i = 0; i < listset.length; i++) { 99 | await handInGeneralTask(listset[i]); 100 | } 101 | 102 | if (config.spec.num == 6) { 103 | spec_message = `🟢 今日特殊任务已全部完成`; 104 | } else { 105 | spec_message = `🟡 今日特殊任务尚未全部完成,请查看日志`; 106 | } 107 | } else { 108 | spec_message = `🟢 今日特殊任务已全部完成`; 109 | } 110 | $.log(spec_message); 111 | let message = `🟢【恭喜】签到状态:签到成功 \n` + `${watch_message}\n` + `${spec_message}\n` + '- 其中特殊任务完成进度以app内完成度为准'; 112 | $.log(message); 113 | } else { 114 | let message = `🔴【抱歉】签到状态:签到失败 \n` + '请重新获取cookie'; 115 | $.log(message); 116 | } 117 | } 118 | 119 | const check = (key: keyof typeof config, num: number) => 120 | !config.hasOwnProperty(key) || !config[key].hasOwnProperty('time') || !(config[key]['num'] > num) || Date.now() > config[key].time; 121 | 122 | const urlencode = (str: string) => { 123 | str = (str + '').toString(); 124 | return encodeURIComponent(str) 125 | .replace(/!/g, '%21') 126 | .replace(/'/g, '%27') 127 | .replace(/\(/g, '%28') 128 | .replace(/\)/g, '%29') 129 | .replace(/\*/g, '%2A') 130 | .replace(/%20/g, '+'); 131 | }; 132 | 133 | async function xmlySign() { 134 | $.log('### 签到任务进行中'); 135 | const { data: body } = await $.req.post('https://hybrid.ximalaya.com/web-activity/signIn/v2/signIn?v=new', { aid: 87 }); 136 | 137 | if (body.ret == 0) { 138 | $.log(`- 签到成功: ${body.msg || body.data?.msg}`); 139 | return await getPointInfo(); 140 | } else { 141 | $.log(`- 签到失败![${body.ret}]${body.msg || '请重新获取 cookie'}`, 'error'); 142 | console.log(body); 143 | return false; 144 | } 145 | } 146 | 147 | async function getPointInfo() { 148 | const { data: body } = await $.req.get('https://m.ximalaya.com/web-hybrid-server/api/point/info', { aid: 87 }); 149 | 150 | if (body.ret == 0) { 151 | configKey = `xmlyConfig_${body.context?.currentUser?.uid}`; 152 | $.log(`- 当前积分:${body.data.point}`); 153 | return true; 154 | } else { 155 | $.log(`- 积分查询失败![${body.ret}]${body.msg}`, 'error'); 156 | console.log(body); 157 | return false; 158 | } 159 | } 160 | 161 | async function flushTaskRecords() { 162 | const { data: body } = await $.req.post(`https://m.ximalaya.com/web-activity/task/v2/taskRecords?tag=pc`, { aid: 112 }); 163 | 164 | if (body.ret == 0) { 165 | $.log('- 刷新列表成功'); 166 | return true; 167 | } else { 168 | $.log('- !!!刷新列表失败', 'error'); 169 | console.log(body); 170 | return false; 171 | } 172 | } 173 | 174 | async function share() { 175 | const { data: body } = await $.req.get( 176 | `https://mobile.ximalaya.com/thirdparty-share/share/content?srcId=422711158&srcType=7&subType=1098&tpName=weixin` 177 | ); 178 | if (body.ret == 0) { 179 | $.log('- 分享成功'); 180 | return true; 181 | } else { 182 | $.log('- !!!分享失败'); 183 | return false; 184 | } 185 | } 186 | 187 | async function getUid() { 188 | let uid = 0; 189 | const { data: body } = await $.req.get(`https://passport.ximalaya.com/user-http-app/v1/nickname/info`); 190 | if (body.ret == 0) { 191 | uid = body.data.uid; 192 | $.log('- 获取uid成功'); 193 | return uid; 194 | } else { 195 | $.log('- !!!获取uid失败', 'error'); 196 | return uid; 197 | } 198 | } 199 | 200 | async function wyy() { 201 | return $.req 202 | .get('https://keai.icu/apiwyy/api') 203 | .then((d) => d.data.contetnt as string) 204 | .catch((error) => { 205 | $.log('- 获取评论失败:' + (error as Error).message); 206 | return '真不错呀'; 207 | }); 208 | } 209 | 210 | async function voiceAdd() { 211 | let params = { relatedId: 423641159, businessType: 100 }; 212 | const { data: body } = await $.req.post(`https://mobile.ximalaya.com/general-relation-service/track/collect/add/1667873518984`, params); 213 | if (body.ret == 0) { 214 | $.log('- 收藏声音成功'); 215 | return true; 216 | } else if (body.ret == 103) { 217 | $.log('- !!!此声音已收藏, 无法再次收藏'); 218 | return false; 219 | } else { 220 | $.log('- !!!未知收藏状况'); 221 | return false; 222 | } 223 | } 224 | 225 | async function voiceDelete() { 226 | let params = { relatedId: 423641159, businessType: 100 }; 227 | const { data: body } = await $.req.post( 228 | `https://mobile.ximalaya.com/general-relation-service/track/collect/delete/1667873518984`, 229 | params 230 | ); 231 | 232 | if (body.ret == 0) { 233 | $.log('- 删除收藏声音成功'); 234 | return true; 235 | } else if (body.ret == 112) { 236 | $.log('- !!!此声音未收藏, 无法删除'); 237 | return false; 238 | } else { 239 | $.log('- !!!未知收藏状况'); 240 | return false; 241 | } 242 | } 243 | 244 | async function userAdd() { 245 | let p = { bizType: 11, isFollow: 1, toUid: 2342717 }; 246 | let url = `https://mobile.ximalaya.com/mobile/follow`; 247 | const { data: body } = await $.req.post(url, p, { 'Content-Type': `application/x-www-form-urlencoded` }); 248 | if (body.ret == 0) { 249 | $.log('- 关注用户成功'); 250 | return true; 251 | } else if (body.ret == 3002) { 252 | $.log('- !!!此用户已关注过'); 253 | return false; 254 | } else if (body.ret == 3001) { 255 | $.log('- !!!关注频率过高,无法关注'); 256 | $.log('- 遇到此种情况,没有很好的解决办法,建议手动关注并交还任务'); 257 | return false; 258 | } else { 259 | $.log('- !!!未知关注状况'); 260 | $.log(JSON.stringify(body)); 261 | return false; 262 | } 263 | } 264 | 265 | async function userDelete() { 266 | let headers = { 267 | Cookie: xm_cookie, 268 | 'Content-Type': `application/x-www-form-urlencoded`, 269 | }; 270 | let body = `bizType=13&isFollow=0&toUid=2342717`; 271 | return await fetch(`https://mobile.ximalaya.com/mobile/follow`, { 272 | method: 'post', 273 | headers, 274 | body, 275 | }) 276 | .then((d) => d.json()) 277 | .then((body) => { 278 | if (body.ret == 0) { 279 | $.log('- 取关用户成功'); 280 | return true; 281 | } else { 282 | $.log('- !!!未知关注状况'); 283 | return false; 284 | } 285 | }) 286 | .catch(() => { 287 | $.log('- !!!取关用户失败'); 288 | return false; 289 | }); 290 | } 291 | 292 | async function giveVoiceLike() { 293 | let headers = { 294 | Cookie: xm_cookie, 295 | 'Content-Type': `application/x-www-form-urlencoded`, 296 | }; 297 | let body = `favorite=1&trackId=423641159`; 298 | let url = `https://mobile.ximalaya.com/favourite-business/favorite/track`; 299 | return await fetch(url, { method: 'post', headers, body }) 300 | .then((d) => d.json()) 301 | .then((body) => { 302 | if (body.ret == 0) { 303 | $.log('- 点赞声音成功'); 304 | return true; 305 | } else if (body.ret == 111) { 306 | $.log('- !!!此声音已点赞过'); 307 | return false; 308 | } else { 309 | $.log('- !!!未知声音点赞状况'); 310 | return false; 311 | } 312 | }) 313 | .catch(() => { 314 | $.log('- !!!点赞声音失败'); 315 | return false; 316 | }); 317 | } 318 | 319 | async function cancelVoiceLike() { 320 | let headers = { 321 | Cookie: xm_cookie, 322 | 'Content-Type': `application/x-www-form-urlencoded`, 323 | }; 324 | let body = `favorite=0&trackId=423641159`; 325 | let url = `https://mobile.ximalaya.com/favourite-business/favorite/track`; 326 | return await fetch(url, { method: 'post', headers, body }) 327 | .then((d) => d.json()) 328 | .then((body) => { 329 | if (body.ret == 0) { 330 | $.log('- 取消声音点赞成功'); 331 | return true; 332 | } else if (body.ret == -1) { 333 | $.log('- !!!此声音尚未点赞, 无法取消'); 334 | return false; 335 | } else { 336 | $.log('- !!!未知声音点赞状况'); 337 | return false; 338 | } 339 | }) 340 | .catch(() => { 341 | $.log('- !!!取消声音点赞失败'); 342 | return false; 343 | }); 344 | } 345 | 346 | async function giveDynamicsLike() { 347 | let headers = { 348 | Cookie: xm_cookie, 349 | 'Content-Type': `application/json`, 350 | }; 351 | let body = `{"feedId":217014623}`; 352 | let url = `https://mobile.ximalaya.com/chaos/v2/feed/praise/create`; 353 | 354 | return await fetch(url, { method: 'post', headers, body }) 355 | .then((d) => d.json()) 356 | .then((body) => { 357 | if (body.ret == 0) { 358 | $.log('- 点赞动态成功'); 359 | return true; 360 | } else { 361 | $.log('- !!!未知动态点赞状况'); 362 | return false; 363 | } 364 | }); 365 | } 366 | 367 | async function cancelDynamicsLike() { 368 | let headers = { 369 | Cookie: xm_cookie, 370 | 'Content-Type': `application/json`, 371 | }; 372 | let body = `{"feedId":217014623}`; 373 | let url = `https://mobile.ximalaya.com/chaos/v2/feed/praise/delete`; 374 | return await fetch(url, { method: 'post', headers, body }) 375 | .then((d) => d.json()) 376 | .then((body) => { 377 | if (body.ret == 0) { 378 | $.log('- 取消动态点赞成功'); 379 | return true; 380 | } else { 381 | $.log('- !!!未知动态点赞状况'); 382 | return false; 383 | } 384 | }); 385 | } 386 | 387 | async function createComment(uid: number | string, content: string) { 388 | let headers = { 389 | Cookie: xm_cookie, 390 | 'Content-Type': `application/x-www-form-urlencoded`, 391 | }; 392 | let body = `content=${content}&source=0&synchaos=1&timeStampType=1&trackId=424771991&uid=${uid}`; 393 | let url = 'https://mobile.ximalaya.com/comment-mobile/v1/create'; 394 | let commentId = 0; 395 | return await fetch(url, { method: 'post', headers, body }) 396 | .then((d) => d.json()) 397 | .then((body) => { 398 | if (body.ret == 0) { 399 | $.log('- 评论成功'); 400 | commentId = body.id; 401 | } else if (body.ret == 801) { 402 | $.log('- !!!请勿发送相同内容'); 403 | } else if (body.ret == 805) { 404 | $.log('- !!!发送内容频繁'); 405 | } else { 406 | $.log('- !!!评论失败'); 407 | } 408 | return commentId; 409 | }); 410 | } 411 | 412 | async function deleteComment(commentId: number) { 413 | let headers = { 414 | Cookie: xm_cookie, 415 | 'Content-Type': `application/x-www-form-urlencoded`, 416 | }; 417 | let body = `commentId=${commentId}&trackId=424771991`; 418 | let url = 'https://mobile.ximalaya.com/comment-mobile/delete'; 419 | return await fetch(url, { method: 'post', headers, body }) 420 | .then((d) => d.json()) 421 | .then((body) => { 422 | if (body.ret == 0) { 423 | $.log('- 删除评论成功'); 424 | return true; 425 | } else { 426 | $.log('- !!!未知评论状态'); 427 | return false; 428 | } 429 | }); 430 | } 431 | 432 | async function adVideoGetToken() { 433 | let headers = { 434 | Cookie: xm_cookie, 435 | 'Content-Type': `application/json`, 436 | }; 437 | let body = `{"aid":112,"taskId":252}`; 438 | let myRequest = { 439 | url: `https://m.ximalaya.com/web-activity/task/v2/genTaskToken`, 440 | headers: headers, 441 | body: body, 442 | }; 443 | return await fetch(myRequest.url, { method: 'post', headers, body }) 444 | .then((d) => d.json()) 445 | .then((body) => { 446 | if (body.ret == 0) { 447 | let token = body.data.token; 448 | return token; 449 | } else { 450 | $.log('- !!!token获取失败'); 451 | let token = 'null'; 452 | return token; 453 | } 454 | }); 455 | } 456 | 457 | async function adVideoFinish(token: string) { 458 | let headers = { 459 | Cookie: xm_cookie, 460 | 'Content-Type': `application/json`, 461 | }; 462 | let body = `{"aid":112,"taskId":252,"token":"${token}","progress":1}`; 463 | let myRequest = { 464 | url: `https://m.ximalaya.com/web-activity/task/v2/incrTaskProgress`, 465 | headers: headers, 466 | body: body, 467 | }; 468 | return await fetch(myRequest.url, { method: 'post', headers, body }) 469 | .then((d) => d.json()) 470 | .then((body) => { 471 | if (body.ret == 0) { 472 | if (body.data.status == 0) { 473 | $.log('- 本条视频广告观看已完成, 获得50点奖励'); 474 | config.watch.num += 1; 475 | config.watch.time = startTime; 476 | return true; 477 | } else if (body.data.status == -1) { 478 | $.log('### 今日观看广告任务已全部完成 ✅ '); 479 | config.watch.num = 6; 480 | config.watch.time = startTime; 481 | $.storage.setItem(configKey, config); 482 | return true; 483 | } else { 484 | $.log('- !!!未知完成状态'); 485 | $.log(JSON.stringify(body.data)); 486 | return false; 487 | } 488 | } else { 489 | $.log('- !!!观看广告任务交还失败'); 490 | return false; 491 | } 492 | }); 493 | } 494 | 495 | async function handInGeneralTask(taskId: number) { 496 | let headers = { 497 | Cookie: xm_cookie, 498 | 'Content-Type': `application/json`, 499 | }; 500 | let body = `{"aid":112,"taskId":${taskId}}`; 501 | let myRequest = { 502 | url: `https://m.ximalaya.com/web-activity/task/v2/drawTaskAward`, 503 | headers: headers, 504 | body: body, 505 | }; 506 | return await fetch(myRequest.url, { method: 'post', headers, body }) 507 | .then((d) => d.json()) 508 | .then((body) => { 509 | if (body.ret == 0) { 510 | if (body.data.status == 0) { 511 | if ((taskId > 167 && taskId < 173) || taskId == 96 || taskId == 336) { 512 | config.spec.num += 1; 513 | config.spec.time = startTime; 514 | $.log('- 交还特殊任务成功, 获得奖励点数'); 515 | } /* else { 516 | config.gene.num += 1 517 | config.gene.time = format(startTime) 518 | $.setdata(JSON.stringify(config.gene), name + "_gene") 519 | $.log("- 交还通用任务成功, 获得10点奖励") 520 | } */ 521 | return true; 522 | } else if (body.data.status == 1) { 523 | if ((taskId > 167 && taskId < 173) || taskId == 96 || taskId == 336) { 524 | config.spec.num += 1; 525 | config.spec.time = startTime; 526 | $.log('- 此项特殊任务今日已交还'); 527 | } /* else { 528 | config.gene.num += 1 529 | config.gene.time = format(startTime) 530 | $.setdata(JSON.stringify(config.gene), name + "_gene") 531 | $.log("- 此项通用任务今日已交还") 532 | } */ 533 | return true; 534 | } else if (body.data.status == -1) { 535 | $.log('--- !!!此任务尚未完成,不能交还'); 536 | return false; 537 | } else { 538 | $.log('--- !!!未知交还状态'); 539 | $.log(JSON.stringify(body.data)); 540 | return false; 541 | } 542 | } else { 543 | $.log('--- !!!交还任务失败'); 544 | return false; 545 | } 546 | }); 547 | } 548 | -------------------------------------------------------------------------------- /ql_youzan-liteapp.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-04-01 09:34:40 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-04-08 11:59:08 6 | * @Description: 有赞小程序签到 7 | * @see https://github.com/dreamtonight/js/blob/main/youzan.js 8 | * 9 | * new Env('有赞小程序签到'); 10 | * cron: 55 8 * * * 11 | * 环境变量: youzan_le_data 。格式:checkinId:sessionId##desc,多账号 @、& 或换行 分割。 12 | * checkinId 为 url 中的参数 13 | * sessionId 为 Cookie 中 KDTWEAPPSESSIONID 的值 14 | * desc 可选。 15 | */ 16 | 17 | import { Env } from './utils'; 18 | 19 | const $ = new Env('有赞小程序签到', { sep: ['@', '&', '\n'] }); 20 | const pd_map = { 21 | '1479428': 'ffit8', 22 | '2187565': '蜜蜂惊喜社', 23 | '2050884': '伯喜线上商城', 24 | '1631': '云南白药', 25 | '3262': 'TOIs朵茜情调生活馆', 26 | '9332': '三只松鼠旗舰店', 27 | '12307': 'colorkey珂拉琪旗舰店', 28 | '16453': 'PMPM', 29 | '17666': '爱依服商城-总店', 30 | '1465878': '隅田川旗舰店', 31 | '1595664': '参半口腔护理', 32 | '1597464': 'Xbox俱乐部', 33 | '1876007': 'FLORTTE花洛莉亚', 34 | '1903120': 'KIMTRUE且初', 35 | '1985507': '肤漾FORYON', 36 | '2176467': 'chillmore且悠', 37 | '2299510': '燕京啤酒电商旗舰店', 38 | '2386563': 'HBN品牌店', 39 | '2646845': '海贽医疗科技', 40 | '2910869': 'ficcecode菲诗蔻官方旗舰店', 41 | '2923467': '红之旗舰店', 42 | '3014060': 'LAN蘭', 43 | '3347128': '松鲜鲜官方旗舰店', 44 | '8249': '贝因美贝家商城', 45 | '18415': '得宝Tempo', 46 | '2713880': '莱克旗舰店', 47 | '2905214': '百事可乐', 48 | '13968': '圣牧有机官方商城', 49 | '3124': '东鹏特饮微店', 50 | '1380': '幸福西饼', 51 | '8': '韩都严选', 52 | '1220': '良品铺子官方商城', // 连续签到领优惠券 53 | '1700': '中粮-健康生活甄选', 54 | '2983020': '盘龙云海健康家', // 连续签到60天送 50 券 55 | '379': '燕之坊五谷为养官方商城', // 连续签到 10、20、30 天送券 56 | '1123': '有间全球购', // 积分+现金购物。不是很划算 57 | }; 58 | // 黑名单,已不再支持签到 59 | const pd_black = new Set([ 60 | 1985111, // 'INTOYOU心慕与你', // 已无签到 61 | 3520910, // 'a chock官方', // 打不开 62 | 1579, // '等蜂来天然蜂蜜旗舰店', // 积分换满减优惠券。不划算 63 | '央广甄选购物', 64 | '云南白药生活', 65 | '奈雪的茶商城', 66 | '东鹏特饮官方微店', // 可签到但无礼物兑换 67 | ]); 68 | 69 | type Res = { 70 | code: number; 71 | msg: string; 72 | data: T; 73 | }; 74 | 75 | class Task { 76 | private checkinId: string = ''; 77 | private sessionId: string = ''; 78 | constructor(str: string, _idx: number, private desc: string = '') { 79 | [this.checkinId, this.sessionId] = str.split(':'); 80 | $.req.setHeaders({ 81 | 'extra-data': `{"is_weapp": 1, sid: "${this.sessionId}"}`, 82 | cookie: this.sessionId.includes('KDTWEAPPSESSIONID') ? this.sessionId : `KDTWEAPPSESSIONID=${this.sessionId}`, 83 | 'user-agent': 84 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x6309092b) XWEB/9079', 85 | }); 86 | } 87 | isInBlackList() { 88 | return [...pd_black].some(d => d === this.checkinId || String(this.desc).includes(String(d))); 89 | } 90 | async start() { 91 | try { 92 | $.log(`开始执行签到任务:[${pd_map[this.checkinId as keyof typeof pd_map] || this.checkinId}]${this.desc}`, 'D'); 93 | if (await this.signin()) await this.getCustomerPoints(); 94 | await $.wait(2000, 1000); 95 | } catch (error) { 96 | $.log(`❌ 发生错误:${(error as Error).message}`, 'error'); 97 | } 98 | } 99 | async signin() { 100 | const url = `https://h5.youzan.com/wscump/checkin/checkinV2.json?checkinId=${this.checkinId}`; 101 | const { data: result } = await $.req.get>(url, {}, {}, { rejectUnauthorized: false }); 102 | // console.log(result); 103 | if (result?.code == 0) { 104 | $.log(`签到成功!获得${result?.data?.list[0]?.infos?.title}`, 'D'); 105 | } else { 106 | if (result?.msg.includes('无法参与')) $.log(`已签到过了:${result.msg}`, 'D'); 107 | else { 108 | // 仅在白名单内的执行通知 109 | if (!this.isInBlackList()) $.log(`签到失败!${result?.msg || JSON.stringify(result)}`, 'error'); 110 | return false; 111 | } 112 | } 113 | 114 | return true; 115 | } 116 | async getCustomerPoints() { 117 | const { data: result } = await $.req.get>( 118 | `https://h5.youzan.com/wscump/pointstore/getCustomerPoints.json`, 119 | {}, 120 | {}, 121 | { rejectUnauthorized: false } 122 | ); 123 | // console.log(result); 124 | if (result.code == 0) $.log(`当前积分: ${result.data.currentAmount}`, 'D'); 125 | else $.log(`查询积分失败!${result.msg}`); 126 | } 127 | } 128 | 129 | // 读取自定义的黑名单 130 | if (process.env.youzan_le_id_blacklist) { 131 | process.env.youzan_le_id_blacklist.split(',').forEach(d => { 132 | const [id, _desc] = d.split(':'); 133 | if (+id && !pd_black.has(+id)) pd_black.add(+id); 134 | }); 135 | } 136 | 137 | // process.env.youzan_le_data = ''; 138 | if (require.main === module) $.init(Task, 'youzan_le_data').then(() => $.done()); 139 | -------------------------------------------------------------------------------- /ql_ysfqd.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-23 13:52:46 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-09-10 11:59:45 6 | * 7 | cron: 15 7 * * * 8 | new Env('云闪付签到') 9 | 环境变量: ysfqd_data, 多账户用 @ 或换行分割。抓取 https://youhui.95516.com/newsign/api 请求 headers 中 Authorization 10 | */ 11 | 12 | import { Env } from './utils'; 13 | 14 | const $ = new Env('云闪付签到', { sep: ['@', '\n'] }); 15 | $.init(signIn, 'ysfqd_data').then(() => $.done()); 16 | 17 | async function signIn(auth: string) { 18 | const { data: result } = await $.req.post( 19 | 'https://youhui.95516.com/newsign/api/daily_sign_in', 20 | {}, 21 | { 22 | Authorization: `Bearer ${auth.replace('Bearer ', '')}`, 23 | 'User-Agent': 24 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 /sa-sdk-ios/sensors-verify/analytics.95516.com?production (com.unionpay.chsp) (cordova 4.5.4) (updebug 0) (version 929) (UnionPay/1.0 CloudPay) (clientVersion 189) (language zh_CN) (upHtml) (walletMode 00) ', 25 | } 26 | ); 27 | 28 | if ('signedIn' in result) { 29 | $.log(`今天是第${result['signInDays']['current']['days']}天签到 今日已签到成功,目前已连续签到${result['signInDays']['days']}天🎉`); 30 | } else { 31 | $.log(`用户查询:失败 ❌ 了呢,原因未知!`); 32 | console.log(result); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample/lzwme_ql_config.json5: -------------------------------------------------------------------------------- 1 | { 2 | version: '0.0.0', 3 | data: { 4 | I茅台预约: { 5 | appVersion: '1.5.6', 6 | user: [ 7 | { 8 | phone: '13800138000', 9 | itemCodes: [], 10 | province: 'xx省', 11 | city: 'xx市', 12 | lng: '', 13 | lat: '', 14 | deviceId: '', 15 | token: '', 16 | tokenWap: '', 17 | }, 18 | ], 19 | type: '预约类型,默认为最近距离。设置为 max 时查找当前城市最大投放量的店铺预约', 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /todo/.gitkeep: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /todo/ql_v2free.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-22 17:05:00 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2024-02-23 11:05:36 6 | * @Description: v2free机场签到。注册地址: https://w1.v2free.top/auth/register?code=cJLs 7 | 8 | cron: 21 10 * * * 9 | 环境变量 V2freeCookie: 抓包获取 header 中的 cookie,多个账户以 & 或 \n 换行分割 10 | 环境变量 YESCAPTCHA_KEY: https://yescaptcha.com/i/9fyOAJ 平台的 ClientKey,新用户联系客服可得 1500 测试积分 11 | */ 12 | import { sleep } from '@lzwme/fe-utils'; 13 | import { Env, logger } from '../utils'; 14 | 15 | const $ = new Env('v2free机场签到'); 16 | $.init(signCheckIn, 'v2freeCookie').then(() => $.done()); 17 | 18 | async function reCaptchaV3({ type = 'RecaptchaV3TaskProxyless', websiteURL = '', websiteKey = '', pageAction = '', clientKey = '' }) { 19 | const result = { token: '', msg: '' }; 20 | 21 | if (!clientKey) { 22 | result.msg = 'clientKey 必填'; 23 | return result; 24 | } 25 | 26 | const r = await fetch('https://cn.yescaptcha.com/createTask', { 27 | method: 'POST', 28 | body: JSON.stringify({ clientKey, task: { type, websiteURL, websiteKey, pageAction } }), 29 | }).then(d => d.json()); 30 | logger.debug('[createTask]', r); 31 | 32 | if (!r.taskId) { 33 | logger.debug('[createTask]创建获取验证码任务失败', r); 34 | result.msg = r.errorDescription || JSON.stringify(r); 35 | return result; 36 | } 37 | 38 | const getTaskResult: () => Promise = async () => { 39 | const r1 = await fetch('https://api.yescaptcha.com/getTaskResult', { 40 | method: 'POST', 41 | body: JSON.stringify({ taskId: r.taskId, clientKey }), 42 | }).then(d => d.json()); 43 | 44 | logger.debug('getTaskResult', r1); 45 | 46 | if (r1.errorId !== 0) { 47 | logger.debug('获取验证码结果失败', r1); 48 | result.msg = r1.errorDescription || JSON.stringify(r1); 49 | return result; 50 | } 51 | 52 | if (r1.status === 'processing') { 53 | await sleep(1000); 54 | return getTaskResult(); 55 | } 56 | 57 | result.token = r1.solution?.gRecaptchaResponse as string; 58 | if (!result.token) result.msg = r1.errorDescription || JSON.stringify(r1); 59 | return result; 60 | }; 61 | 62 | await sleep(10_000); 63 | return getTaskResult(); 64 | } 65 | 66 | async function signCheckIn(Cookie: string) { 67 | const r = await reCaptchaV3({ 68 | type: 'RecaptchaV3TaskProxyless', 69 | websiteURL: 'https://w1.v2free.top/user', 70 | websiteKey: '6LdKhuYnAAAAAEshg5Sa2jKL_HqNOpqfrmp', 71 | pageAction: '', 72 | clientKey: process.env.YESCAPTCHA_KEY, 73 | }); 74 | if (!r.token) return $.log(`人机验证码识别失败: ${r.msg}`, 'error'); 75 | 76 | const data = await fetch('https://w1.v2free.top/user/checkin', { 77 | method: 'POST', 78 | headers: { 79 | Cookie, 80 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 81 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 82 | Host: 'w1.v2free.top', 83 | Origin: 'https://w1.v2free.top', 84 | Referer: 'https://w1.v2free.top/user', 85 | 'User-Agent': 86 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', 87 | }, 88 | body: `recaptcha=${r.token}`, 89 | }).then(d => d.text()); 90 | 91 | const res = JSON.parse(data as never as string); 92 | if (res.trafficInfo) $.log(`签到成功!${res.msg}。未使用流量:${res.trafficInfo.unUsedTraffic}`); 93 | else $.log(`❌签到失败:${res.msg}`, 'error'); 94 | } 95 | -------------------------------------------------------------------------------- /todo/update-readme.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-05-21 10:20:11 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-01-09 10:22:26 6 | * @Description: 7 | */ 8 | import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; 9 | import { resolve } from 'node:path'; 10 | import { getLogger } from '@lzwme/fe-utils'; 11 | 12 | const logger = new getLogger('[QL-SCRIPTS]'); 13 | const rootDir = process.cwd(); 14 | 15 | 16 | function getScriptsList() { 17 | const list = readdirSync(rootDir).filter(d => /^ql_.+\.(ts|js)$/.test(d)); 18 | const mdContent = list.map(filename => { 19 | const content = readFileSync(resolve(rootDir, filename), 'utf8'); 20 | const title = /Env\(["' ]+([^'"]+)["' ]+\)/.exec(content)?.[1].trim() || ''; 21 | 22 | return `- [${title}](./${filename})`; 23 | // return `- [${title}](https://ghpr.cc/github.com/lzwme/ql-scripts/raw/main/${filename})`; 24 | }).join('\n'); 25 | 26 | return { list, mdContent }; 27 | } 28 | 29 | function updateReadme() { 30 | const rdFile = resolve(rootDir, 'README.md'); 31 | const { list, mdContent } = getScriptsList(); 32 | const content = readFileSync(rdFile, 'utf8'); 33 | const noticeStr = `> 注意:本仓库脚本不支持单独订阅。可订阅仓库并禁用不需要的脚本。\n\n`; 34 | const updated = content.replace(/## 脚本列表\([\s\S]+\n## /g, `## 脚本列表(${list.length}):\n${noticeStr}${mdContent}\n\n## `); 35 | 36 | if (updated !== content) { 37 | writeFileSync(rdFile, updated, 'utf8'); 38 | logger.info('已更新', list.length); 39 | } else logger.info('[UPDATE-READE] No Chagned'); 40 | return list.length; 41 | } 42 | 43 | function start() { 44 | updateReadme(); 45 | } 46 | 47 | start(); 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "skipLibCheck": true, 6 | "target": "ESNext", 7 | "module": "commonjs", 8 | "outDir": "cjs", 9 | "moduleResolution": "node", 10 | "inlineSourceMap": false, 11 | "esModuleInterop": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | /* Debugging Options */ 20 | "traceResolution": false, 21 | "listEmittedFiles": false, 22 | "listFiles": false, 23 | "pretty": true, 24 | 25 | "lib": ["DOM", "esnext"], 26 | "typeRoots": ["./node_modules/@types"] 27 | }, 28 | "exclude": ["node_modules/**"], 29 | "include": ["**/*.ts"], 30 | "compileOnSave": false 31 | } 32 | -------------------------------------------------------------------------------- /utils/Env.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2024-02-20 10:31:21 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-05-16 10:46:17 6 | * @Description: 7 | */ 8 | import { type AnyObject, Request, color } from '@lzwme/fe-utils'; 9 | import { getCacheStorage, sendNotify } from './common'; 10 | 11 | const { redBright, strip } = color; 12 | 13 | interface EnvOptions { 14 | /** 多账号分隔符。默认为 &、\n */ 15 | sep?: string[]; 16 | /** 是否开启消息通知。默认为 true */ 17 | notifyFlag?: boolean; 18 | } 19 | 20 | export class Env { 21 | public index = 0; 22 | private startTime = Date.now(); 23 | private msgs: string[] = []; 24 | private options: EnvOptions = { 25 | sep: ['&', '\n'], 26 | }; 27 | public hasError: boolean | number = 0; 28 | public req = new Request(undefined, { 'content-type': 'application/json' }); 29 | public storage: ReturnType>; 30 | public color = color; 31 | constructor(public name: string, options?: EnvOptions) { 32 | this.log(`[${this.name}]开始运行\n`, 'debug'); 33 | this.storage = getCacheStorage(name); 34 | if (options) Object.assign(this.options, options); 35 | } 36 | public async init(Task?: any, envName?: string, envValue?: string) { 37 | await this.storage.ready(); 38 | 39 | if (Task) { 40 | if (!envValue && envName) envName.split('|').some((eName) => (envValue = process.env[eName])); 41 | if (envValue) { 42 | const users = this.parse(envValue, this.options.sep); 43 | await this.runTask(Task, users); 44 | } else { 45 | this.log(`环境变量 ${redBright(envName)} 未定义`, 'error'); 46 | } 47 | } 48 | return this; 49 | } 50 | public async runTask(Task: any, usersConfig: any[]) { 51 | try { 52 | for (let [idx, userConfig] of Object.entries(usersConfig)) { 53 | try { 54 | this.index = +idx + 1; 55 | let desc = ''; 56 | if (typeof userConfig === 'string') [userConfig, desc = ''] = userConfig.split('##'); // 支持以 ## 隔离描述,可主要用于唯一 uid 标记 57 | this.log(`🆔账号${this.index}:${desc || ''}`); 58 | if (typeof Task.prototype?.start === 'function') { 59 | const t = new Task(userConfig, this.index, desc); 60 | await t.start(); 61 | } else await Task(userConfig, this.index, desc); 62 | } catch (error) { 63 | console.error(error); 64 | this.log(`❌账号 ${this.index} 运行异常:${(error as Error).message}`, 'error'); 65 | } 66 | } 67 | } catch (e) { 68 | const error = e as Error; 69 | console.error(error); 70 | this.log(`❌运行异常:${error.message}`, 'error'); 71 | } 72 | this.done(); 73 | } 74 | public parse(envValue: string, mutiAccountSeps = this.options.sep!) { 75 | if (!envValue) return []; 76 | 77 | const sep = mutiAccountSeps.find((d) => envValue.includes(d)) || mutiAccountSeps[0]; 78 | const arr = envValue.split(sep).filter(Boolean); 79 | if (arr.length > 1) this.log(`共找到了 ${arr.length} 个账号`); 80 | return arr; 81 | } 82 | public log(msg: string, type: 'error' | 'info' | 'warn' | 'log' | 'debug' | 'D' = 'info') { 83 | if (type === 'D') type = 'debug'; 84 | if (type === 'error') { 85 | this.hasError = true; 86 | if (!msg.startsWith('❌') && !/^[\ud800-\udbff][\udc00-\udfff]/.test(msg)) msg = `❌ ${msg}`; 87 | } 88 | if (type !== 'debug') this.msgs.push(strip(msg)); 89 | console[type](msg); 90 | } 91 | uuid() { 92 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 93 | const r = (Math.random() * 16) | 0; 94 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); 95 | }); 96 | } 97 | wait(delay: number, gap = 0, showTip = true) { 98 | if (gap > 0) delay += Math.floor(Math.random() * gap); 99 | if (showTip) this.log(`等待 ${delay}ms 后继续...`, 'debug'); 100 | return new Promise((rs) => setTimeout(rs, delay)); 101 | } 102 | public getMsgs() { 103 | return this.msgs.join('\n'); 104 | } 105 | private end = false; 106 | public async done() { 107 | if (this.end) return; 108 | this.end = true; 109 | if (this.options.notifyFlag !== false && this.msgs.length) { 110 | await sendNotify(this.name, this.getMsgs(), { hasError: this.hasError, isPrint: false, exit: false }); 111 | } 112 | this.log(`运行结束,共运行了 ${Math.ceil((Date.now() - this.startTime) / 1000)} 秒`); 113 | process.exit(this.hasError ? 1 : 0); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /utils/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: renxia 3 | * @Date: 2023-11-28 11:09:04 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2025-04-17 17:08:33 6 | * @Description: 7 | */ 8 | 9 | import { LiteStorage, Request, color, NLogger } from '@lzwme/fe-utils'; 10 | import { existsSync } from 'node:fs'; 11 | import { homedir } from 'node:os'; 12 | import { resolve, sep } from 'node:path'; 13 | 14 | export const logger = new NLogger('[ql-scripts]'); 15 | 16 | export function findFile(filename: string, dirs = [process.cwd(), __dirname]) { 17 | const dirList = new Set([...dirs, process.cwd(), __dirname, homedir()]); 18 | 19 | for (let dir of dirList) { 20 | while (dir.length > 3 && dir.includes(sep)) { 21 | const fullpath = resolve(dir, filename); 22 | if (existsSync(fullpath)) return fullpath; 23 | dir = dir.substring(0, dir.lastIndexOf(sep)); 24 | } 25 | } 26 | 27 | return ''; 28 | } 29 | 30 | /** 获取本地持久缓存对象 */ 31 | export function getCacheStorage>(uuid: string) { 32 | return getConfigStorage(uuid, resolve(process.cwd(), 'cache/lzwme_ql_cache.json')); 33 | } 34 | 35 | /** 获取配置持久化对象 */ 36 | export function getConfigStorage>(uuid: string, filepath = process.env.LZWME_QL_CONFIG_FILE) { 37 | if (!uuid) throw Error('请指定 uuid'); 38 | 39 | if (!filepath) { 40 | filepath = findFile('lzwme_ql_config.json5') || findFile('lzwme_ql_config.json') || 'lzwme_ql_config.json'; 41 | } 42 | 43 | // if (!existsSync(filepath) && !filepath.includes('cache.json')) console.warn(`未发现配置文件。请创建:${color.cyan(filepath)}`); 44 | 45 | return new LiteStorage({ filepath: resolve(process.cwd(), filepath), uuid }); 46 | } 47 | 48 | interface SendNotifyParams extends Record { 49 | hasError?: boolean | number; 50 | notifyType?: 0 | 1 | 2; 51 | isPrint?: boolean; 52 | exit?: boolean; 53 | } 54 | 55 | export async function sendNotify(title: string, body: string, params: SendNotifyParams = {}, author = '\n本通知 By:lzwme/ql-scripts') { 56 | const notifyFilePath = findFile('sendNotify.js'); 57 | const { hasError, notifyType = Number(process.env.LZWME_QL_NOTIFY_TYPE) || 1, isPrint, exit = true } = params; 58 | let needNotify = true; 59 | 60 | if (notifyType === 0) needNotify = false; // 0 - 禁用通知 61 | else if (notifyType === 1) { 62 | // 1 - 有异常才通知 63 | needNotify = hasError === true; 64 | } else if (notifyType === 2) { 65 | // 2 - 全通知 66 | needNotify = true; 67 | } else needNotify = process.env.LZWME_QL_NOTIFY !== 'false'; 68 | 69 | if (isPrint !== false && (isPrint || !notifyFilePath || !needNotify)) console.log(`[needNotify=${needNotify}][${title}]\n${body}`); 70 | 71 | if (!notifyFilePath) { 72 | console.warn('未发现 sendNotify.js 文件!'); 73 | } else if (needNotify) { 74 | await require(notifyFilePath).sendNotify(title, body, params, author); 75 | } 76 | 77 | exit && process.exit(hasError ? 1 : 0); 78 | } 79 | 80 | /** 根据指定的位置返回附近位置及经纬度列表 */ 81 | export async function getGeoByGD(address: string, AMAP_KEY: string) { 82 | const req = new Request(); 83 | const { data } = await req.get<{ 84 | status: string; 85 | geocodes: { 86 | province: string; 87 | city: string; 88 | formatted_address: string; 89 | location: string; 90 | }[]; 91 | }>(`https://restapi.amap.com/v3/geocode/geo?key=${AMAP_KEY}&output=json&address=${address.trim()}`); 92 | 93 | return data.geocodes; 94 | } 95 | 96 | export async function getLocationByIp(ip = '') { 97 | const url = `https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`; 98 | const req = new Request(); 99 | const { data } = await req.get<{ code: number; data: { address: string; ip: string } }>(url); 100 | 101 | if (data.code === 200 && data.data.address) { 102 | let [_area, province, city] = data.data.address.split(' '); 103 | 104 | if (['北京', '上海', '天津', '重庆'].includes(province)) { 105 | province += '市'; 106 | city = province; 107 | } else { 108 | if (!province.includes('省')) province += '省'; 109 | if (!city.includes('市')) city += '市'; 110 | } 111 | 112 | return { province, city }; 113 | } else { 114 | console.error('[err][getLocationByIp', ip, data); 115 | } 116 | 117 | return; 118 | } 119 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export { Env } from './Env'; 3 | --------------------------------------------------------------------------------