├── assets ├── appicon.png ├── task-1.PNG ├── task-2.PNG ├── task-3.PNG ├── task-4.PNG ├── task-5.PNG ├── rewrite-1.PNG ├── rewrite-2.PNG ├── rewrite-3.PNG └── gallery-icon.png ├── .gitignore ├── task ├── gallery.json └── mihoyobbs.conf ├── LICENSE ├── README.md └── src ├── mihoyobbs.cookie.js ├── mihoyobbs.config.js └── mihoyobbs-auto-helper.js /assets/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/appicon.png -------------------------------------------------------------------------------- /assets/task-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/task-1.PNG -------------------------------------------------------------------------------- /assets/task-2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/task-2.PNG -------------------------------------------------------------------------------- /assets/task-3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/task-3.PNG -------------------------------------------------------------------------------- /assets/task-4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/task-4.PNG -------------------------------------------------------------------------------- /assets/task-5.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/task-5.PNG -------------------------------------------------------------------------------- /assets/rewrite-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/rewrite-1.PNG -------------------------------------------------------------------------------- /assets/rewrite-2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/rewrite-2.PNG -------------------------------------------------------------------------------- /assets/rewrite-3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/rewrite-3.PNG -------------------------------------------------------------------------------- /assets/gallery-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/HEAD/assets/gallery-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # misc 7 | .DS_Store 8 | 9 | # vscode 10 | .vscode -------------------------------------------------------------------------------- /task/gallery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "米游社小助手", 3 | "description": "米游社小助手的任务仓库 by kayanouriko", 4 | "task": [ 5 | { 6 | "config": "2 0 * * * https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/src/mihoyobbs-auto-helper.js, tag=米游社小助手, img-url=https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/assets/gallery-icon.png, enabled=false", 7 | "addons": "https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/task/mihoyobbs.conf, tag=配置米游社小助手" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /task/mihoyobbs.conf: -------------------------------------------------------------------------------- 1 | # @name 米游社相关 cookie 获取 2 | # @version v1.0.0 3 | # @description 用于 quantumultx 获取米游币任务和签到任务所需 cookie 的 rewrite 文件 4 | # @author kayanouriko 5 | # @homepage https://github.com/kayanouriko/quantumultx-mihoyobbs-auto-helper 6 | # @license MIT 7 | 8 | # mitm 9 | hostname = bbs-api.mihoyo.com, api-takumi.mihoyo.com, example.com 10 | 11 | # 获取米游币任务功能需要的 cookie 12 | ^https://bbs-api.mihoyo.com/apihub/api/getGameList url script-request-header https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/src/mihoyobbs.cookie.js 13 | # 获取签到功能需要的 cookie 14 | ^https://api-takumi.mihoyo.com/binding/api/getUserGameRoles url script-request-header https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/src/mihoyobbs.cookie.js 15 | # 脚本设置自定义 16 | ^https://example.com/? url script-request-header https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/src/mihoyobbs.config.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 kayanouriko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./assets/appicon.png) 2 | # 米游社小助手 3 | 4 | ![platform](https://img.shields.io/badge/platform-quantumultx-lightgrey.svg) [![](https://img.shields.io/github/v/release/kayanouriko/quantumultx-genshin-autosign-helper)](https://github.com/kayanouriko/quantumultx-genshin-autosign-helper/releases) 5 | 6 | 一个 quantumultx 脚本, 主要用于米游社米游币任务和游戏(原神和崩坏3rd)签到的自动运行. 7 | 8 | ## 暂时不维护 9 | 10 | 鉴于 mihoyo 对于该类脚本加强了风控后台防护. 11 | 目前上游项目也没法解决, 因为目前风控解除的方案是需要付费的, 所以我一直没有修, 所以该脚本目前暂时处于不维护状态. 12 | 推荐使用一个拥有复合型功能的应用(eg. 胡桃工具箱)来管理米游社额外的功能. 13 | 14 | ## 前言 15 | 16 | 1. 该项目 v2 版本的关键业务参数均来源于: [@AutoMihoyoBBS](https://github.com/Womsxd/AutoMihoyoBBS), 感谢逆向接口参数可以直接抄作业. 17 | 18 | 2. 当使用该项目的前提下, 一个手机对应一个账号, 所以不支持多账号的功能是理所应当的. 当你有多账号需求时, 请使用上述等其他项目. 19 | 20 | 3. 该脚本只适配了国服账号. 21 | 22 | 4. 使用该项目之前, 你需要提前了解如何使用 quantumultx 拉取远程仓库和执行脚本. 23 | 24 | > 虽然是参照 quantumultx 编写, 但是使用的模块封装应该也适配 shadowrocket, loon, surge, stash. 25 | 26 | > 别家应用的用户可以自行测试一下, 行就行, 不行也别找我了Orz 27 | 28 | 5. 目前 quantumultx 并不会自动升级远程配置的脚本, 当你发现脚本功能不可用时, 可先看该项目是否已经更新修复了, 再尝试在 quantumultx 内更新脚本以获取最新的修复. 29 | 30 | > 推荐点击项目右上角的 watch 按钮, 选择 custom 里面 Releases, 这样当脚本释出新版本时你能第一时间获取邮件通知以便在 quantumultx 内更新脚本. 31 | 32 | ## 更新日志 33 | 34 | * v2.4.1 35 | 1. 同步上游 salt 参数 36 | 2. 去除调试打印 37 | 38 | 39 |
40 | 历史更新日志 41 | 42 | * v2.4.0 43 | 1. 修复原神签到风控问题 44 | > 注意需要同时更新配套的重写规则并重新获取 headers. 更加详细说明参照下文 *关于原神签到的说明* 部分 45 | * v2.3.1 46 | 1. 原神签到添加风控验证码的判断 47 | 2. 优化 headers 代码逻辑 48 | 3. 优化请求出错的报错提示 49 | * v2.3.0 50 | 1. 更新米游币任务相关接口相关参数 51 | > 推荐更新一下自己的 cookie 防止出现问题 52 | * v2.2.0 53 | 1. 更新 header 相关参数适配米游币新接口 54 | 2. 更新图片资源 55 | 3. 更新了仓库内容, 将重写规则也加入仓库, 现在无需另外手动添加重写链接了.(有需要的童鞋可以删除原来的仓库链接和重写链接, 添加并使用新的仓库来下载重写规则, 方便以后更新) 56 | * v2.1.0 57 | 1. 适配 崩坏3rd 签到新接口. [@接口改版抓包讨论](https://github.com/Womsxd/AutoMihoyoBBS/issues/151) 58 | * v2.0.3 59 | 1. 修复分享任务代码造成的米游币任务完成提示文本错误 60 | * v2.0.2 61 | 1. 修复分享任务成功判断问题 62 | * v2.0.1 63 | 1. 缩短米游币任务报告文本 64 | 2. 修复分享任务报告文本无法显示的问题 65 | * v2.0.0 66 | 1. 支持米游币任务和崩坏3rd签到 67 | 2. 自动获取 cookie 68 | 3. 可自定义任务执行配置 69 | * v1.1.1 70 | 1. 优化代码逻辑 71 | * v1.1.0 72 | 1. 新增签到奖励信息 73 | 2. 优化代码逻辑 74 | * v1.0.0 75 | 1. 初版 76 | 77 |
78 | 79 | ## 如何使用 80 | 81 | v2.0.0开始, 在 quantumultx 中全面转为远程更新和执行, 配置一次, 自动更新, 永久运行. 82 | 83 | ### 引入资源 84 | 85 | | ![](./assets/task-1.PNG) | 86 | | :------------------------------: | 87 | | 打开应用底部最右侧图标
task-1 | 88 | 89 | | ![](./assets/task-2.PNG) | 90 | | :----------------------: | 91 | | 点击红框图标
task-2 | 92 | 93 | | ![](./assets/task-3.PNG) | 94 | | :------------------------------------: | 95 | | 点击右上角加号, 输入仓库地址
task-3 | 96 | 97 | ``` 98 | // 仓库地址连接 99 | https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/task/gallery.json 100 | ``` 101 | 102 | | ![](./assets/task-4.PNG) | 103 | | :---------------------------------------------------------------------: | 104 | | 点击米游社小助手, 在弹出的 sheet 中分别选择添加和添加附加组件
task-4 | 105 | 106 | | ![](./assets/task-5.PNG)
![](./assets/rewrite-1.PNG) | 107 | | :--------------------------------------------------------: | 108 | | 请求列表和重写列表如图所示即为添加成功
task-5 rewrite-1 | 109 | 110 | ### 获取 cookie 111 | 112 | | ![](./assets/rewrite-1.PNG) | 113 | | :-----------------------------------: | 114 | | 保证重写列表的为打开状态
rewrite-1 | 115 | 116 | > 重写需要配合 MitM 使用, 确保你的 MitM 也是开启状态 117 | 118 | 打开米游社 app, 此时会弹出第一条获取成功的通知, 再随便打开一个游戏的签到页面, 会收到第二条获取成功的通知. 如下所示 119 | 120 | | ![](./assets/rewrite-3.PNG) | 121 | | :-------------------------------------: | 122 | | 获取 cookie 成功的两条通知
rewrite-3 | 123 | 124 | > 如果之前你的米游社 app 是处于后台开启状态, 可能需要清除后台, 重新打开米游社 app 才能收到米游币任务所需 cookie 获取成功的通知. 125 | 126 | 关闭重写列表, 以后 cookie 失效了再重新打开重复以上步骤重新获取 cookie 即可. 127 | 128 | | ![](./assets/rewrite-2.PNG) | 129 | | :-------------------------: | 130 | | 取消打勾操作
rewrite-2 | 131 | 132 | ### 至此, 脚本可以运行了. 133 | 134 | ### 进阶用法(自定义配置) 135 | 136 | | 参数名 | 说明 | 值 | 137 | | :------: | :------------------------: | :---------------------------------------------------------------------------------------------------------------------------: | 138 | | tasks | 需要自动执行的任务 | 1. 米游币任务 2. 原神签到 3. 崩坏 3rd 签到.
默认为 1,2,3 执行米游币, 原神, 崩坏3rd 3个任务 | 139 | | scetions | 需要执行米游币任务的讨论区 | 1. 崩坏3, 26. 原神 30. 崩坏学园2 37. 未定事件簿 34. 大别野 52. 崩坏:星穹铁道
默认为 34, 即在大别野帖子列表执行米游币任务 | 140 | | actions | 需要执行的米游币任务 | 58. 讨论区签到 59. 浏览 3 个帖子 60. 完成 5 次点赞 61. 分享帖子
默认为 58,59,60,61 执行米游社的全部任务 | 141 | 142 | #### 自定义配置使用方法 143 | 144 | | ![](./assets/rewrite-1.PNG) | 145 | | :-----------------------------------: | 146 | | 保证重写列表的为打开状态
rewrite-1 | 147 | 148 | 打开 safari 浏览器, 访问 `https://example.com/?参数名=值` 即可, 弹出设置成功的通知即为成功. 149 | 150 | * 注1: 这里的 `https://example.com/?` 是固定的, 必须是这个网址才能设置成功. 151 | 152 | * 注2: 关于链接 GET 请求传参的相关知识请参考: [Query String](https://en.wikipedia.org/wiki/Query_string) 153 | 154 | 例如: `https://example.com/?tasks=1,2&actions=58,59` 表示脚本执行 米游币任务 原神签到任务, 并且米游币任务中执行讨论区签到, 浏览 3 个帖子两个任务. 155 | 156 | 关闭重写列表, 以后想要重新自定义配置项重复上述步骤即可. 157 | 158 | | ![](./assets/rewrite-2.PNG) | 159 | | :-------------------------: | 160 | | 取消打勾操作
rewrite-2 | 161 | 162 | ## 关于原神签到的说明 163 | 164 | 米哈游在 2.33.1 版本左右单独为原神的签到加入了 CAPTCHA 测试. 165 | 166 | 在 [#179](https://github.com/Womsxd/AutoMihoyoBBS/issues/179) 的讨论中基本确定是针对 UserAgent 进行识别, 感谢大佬们的分析. 所以从 v2.4.0 版本起, 该脚本将获取用户整个请求的 headers 作为数据存储用以解决该问题. 167 | 168 | 而对于需要原神签到的用户, 之前出现过风控问题需要到 app 手动签到直至风控验证码消失, 再将本脚本更新到 v2.4.0 版本以及配套重写规则更新到 v1.1.0, 并打开重写规则重新获取 headers 数据, 即可解决风控问题. 169 | 170 | 在以后手机系统和米游社 app 升级后, 都必须及时打开重写规则更新自己的 headers 防止风控问题. 171 | 172 | ## 感谢 173 | 174 | * [@chavyleung/Env.js](https://github.com/chavyleung/scripts): 各家应用环境的统一封装 175 | * [@NobyDa](https://github.com/NobyDa/Script): 一些原生算法解决方案参考 176 | * [@AutoMihoyoBBS](https://github.com/Womsxd/AutoMihoyoBBS): v2版本业务逻辑部分基本来自该仓库 177 | * [@genshin-sign-helper](https://github.com/daye99/genshin-sign-helper): v1版本业务逻辑部分基本来自该仓库 178 | * [@GenshinPlayerQuery](https://github.com/Azure99/GenshinPlayerQuery/issues/20): 关键算法逻辑部分的来源 179 | -------------------------------------------------------------------------------- /src/mihoyobbs.cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 米游社小助手-headers 3 | * @version v1.1.0 4 | * @description 用于获取米游币任务和签到任务的 headers 5 | * @author kayanouriko 6 | * @homepage https://github.com/kayanouriko/quantumultx-mihoyobbs-auto-helper 7 | * @thanks chavyleung, 各家应用环境的统一封装 8 | * @thanks NobyDa, 一些原生算法解决方案参考 9 | * @license MIT 10 | */ 11 | 12 | const $ = new Env('米游社小助手-headers') 13 | /** URL */ 14 | const BBS_URL = 'https://bbs-api.mihoyo.com/apihub/api/getGameList' 15 | const SIGN_URL = 'https://api-takumi.mihoyo.com/binding/api/getUserGameRoles' 16 | /** 存储的 key */ 17 | const BBS_HEADERS_KEY = 'kayanouriko_mihoyobbs_headers_bbs' 18 | const SIGN_HEADERS_KEY = 'kayanouriko_mihoyobbs_headers_sign' 19 | 20 | /** 主入口 */ 21 | main() 22 | 23 | /** 24 | * 1.1.0 版本开始直接存储整个 headers, 用于处理原神签到风控问题 25 | */ 26 | function main() { 27 | const url = $request.url 28 | const result = url.split('?')?.[0] 29 | const headers = $request?.headers 30 | if (headers) { 31 | if (result === BBS_URL) { 32 | // 米游币任务的 url 33 | if ($.setdata(JSON.stringify(headers), BBS_HEADERS_KEY)) { 34 | $.msg('米游社小助手-headers', '米游币任务所需的 headers 获取成功!') 35 | } 36 | } else if (result === SIGN_URL) { 37 | // 签到任务的 url 38 | if ($.setdata(JSON.stringify(headers), SIGN_HEADERS_KEY)) { 39 | $.msg('米游社小助手-headers', '签到任务所需的 headers 获取成功!') 40 | } 41 | } 42 | } 43 | // 传入空对象不改变原来的请求 44 | $.done({}) 45 | } 46 | 47 | /** 48 | * Env 各家应用环境适配 49 | * @see https://github.com/chavyleung/scripts/blob/master/Env.min.js 50 | */ 51 | function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise((e,i)=>{s.call(this,t,(t,s,r)=>{t?i(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}isNode(){return"undefined"!=typeof module&&!!module.exports}isQuanX(){return"undefined"!=typeof $task}isSurge(){return"undefined"!=typeof $httpClient&&"undefined"==typeof $loon}isLoon(){return"undefined"!=typeof $loon}isShadowrocket(){return"undefined"!=typeof $rocket}isStash(){return"undefined"!=typeof $environment&&$environment["stash-version"]}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const i=this.getdata(t);if(i)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,i)=>e(i))})}runScript(t,e){return new Promise(s=>{let i=this.getdata("@chavy_boxjs_userCfgs.httpapi");i=i?i.replace(/\n/g,"").trim():i;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[o,h]=i.split("@"),n={url:`http://${h}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":o,Accept:"*/*"}};this.post(n,(t,e,i)=>s(i))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):i?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const i=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of i)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,i,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,r]=/^@(.*?)\.(.*?)$/.exec(e),o=this.getval(i),h=i?"null"===o?null:o||"{}":"{}";try{const e=JSON.parse(h);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),i)}catch(e){const o={};this.lodash_set(o,r,t),s=this.setval(JSON.stringify(o),i)}}else s=this.setval(t,e);return s}getval(t){return this.isSurge()||this.isLoon()?$persistentStore.read(t):this.isQuanX()?$prefs.valueForKey(t):this.isNode()?(this.data=this.loaddata(),this.data[t]):this.data&&this.data[t]||null}setval(t,e){return this.isSurge()||this.isLoon()?$persistentStore.write(t,e):this.isQuanX()?$prefs.setValueForKey(t,e):this.isNode()?(this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0):this.data&&this.data[e]||null}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){if(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"]),this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:i,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:i,statusCode:r,headers:o,rawBody:h},s.decode(h,this.encoding))},t=>{const{message:i,response:r}=t;e(i,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";if(t.body&&t.headers&&!t.headers["Content-Type"]&&(t.headers["Content-Type"]="application/x-www-form-urlencoded"),t.headers&&delete t.headers["Content-Length"],this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let i=require("iconv-lite");this.initGotEnv(t);const{url:r,...o}=t;this.got[s](r,o).then(t=>{const{statusCode:s,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:s,statusCode:r,headers:o,rawBody:h},i.decode(h,this.encoding))},t=>{const{message:s,response:r}=t;e(s,r,r&&i.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let i={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in i)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?i[e]:("00"+i[e]).substr((""+i[e]).length)));return t}msg(e=t,s="",i="",r){const o=t=>{if(!t)return t;if("string"==typeof t)return this.isLoon()?t:this.isQuanX()?{"open-url":t}:this.isSurge()?{url:t}:void 0;if("object"==typeof t){if(this.isLoon()){let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}if(this.isQuanX()){let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,i=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":i}}if(this.isSurge()){let e=t.url||t.openUrl||t["open-url"];return{url:e}}}};if(this.isMute||(this.isSurge()||this.isLoon()?$notification.post(e,s,i,o(r)):this.isQuanX()&&$notify(e,s,i,o(r))),!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),i&&t.push(i),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){const s=!this.isSurge()&&!this.isQuanX()&&!this.isLoon();s?this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack):this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t)}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),(this.isSurge()||this.isQuanX()||this.isLoon())&&$done(t)}}(t,e)} -------------------------------------------------------------------------------- /src/mihoyobbs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 米游社小助手-设置 3 | * @version v1.0.0 4 | * @description 用于获取米游币任务和签到任务的配置 5 | * @author kayanouriko 6 | * @homepage https://github.com/kayanouriko/quantumultx-mihoyobbs-auto-helper 7 | * @thanks chavyleung, 各家应用环境的统一封装 8 | * @thanks NobyDa, 一些原生算法解决方案参考 9 | * @license MIT 10 | */ 11 | 12 | const $ = new Env('米游社小助手-cookie') 13 | 14 | const CONFIG_KEY = 'kayanouriko_mihoyobbs_config' 15 | 16 | const tasksDic = { 17 | 1: '米游币任务', 18 | 2: '原神签到任务', 19 | 3: '崩坏3rd签到任务' 20 | } 21 | 22 | const actionsDic = { 23 | 58: '讨论区签到任务', 24 | 59: '浏览 3 个帖子任务', 25 | 60: '完成 5 次点赞任务', 26 | 61: '分享帖子任务', 27 | } 28 | 29 | /** 主入口 */ 30 | main() 31 | 32 | function main() { 33 | const url = new URL($request.url) 34 | const params = url.searchParams 35 | const tasks = parse(params, 'tasks', '1,2,3') 36 | const sections = parse(params, 'sections', '34') 37 | const actions = parse(params, 'actions', '58,59,60,61') 38 | 39 | const config = { 40 | tasks, 41 | micoin: { 42 | sections, 43 | actions 44 | } 45 | } 46 | // 整理通知文本 47 | let tasksResult = '执行' 48 | for (const id of tasks) { 49 | tasksResult += `"${tasksDic[id]}"` 50 | } 51 | 52 | const micoinID = tasks.find(e => e === 1) 53 | if (micoinID) { 54 | tasksResult += `, 其中"米游币任务"将执行` 55 | for (const id of actions) { 56 | tasksResult += `"${actionsDic[id]}"` 57 | } 58 | } 59 | tasksResult += '.' 60 | 61 | const result = `米游社小助手自定义设置成功! 长按通知展开配置或者点击通知在应用内查看配置.\n\n${tasksResult}` 62 | 63 | if ($.setdata(JSON.stringify(config), CONFIG_KEY)) { 64 | $.msg('米游社小助手-设置', result) 65 | } 66 | // 传入空对象不改变原来的请求 67 | $.done({}) 68 | } 69 | 70 | function parse(params, key, placeholder) { 71 | return (params.get(key) ?? placeholder).split(',').map(e => parseInt(e)) 72 | } 73 | 74 | /** 75 | * Env 各家应用环境适配 76 | * @see https://github.com/chavyleung/scripts/blob/master/Env.min.js 77 | */ 78 | function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise((e,i)=>{s.call(this,t,(t,s,r)=>{t?i(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}isNode(){return"undefined"!=typeof module&&!!module.exports}isQuanX(){return"undefined"!=typeof $task}isSurge(){return"undefined"!=typeof $httpClient&&"undefined"==typeof $loon}isLoon(){return"undefined"!=typeof $loon}isShadowrocket(){return"undefined"!=typeof $rocket}isStash(){return"undefined"!=typeof $environment&&$environment["stash-version"]}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const i=this.getdata(t);if(i)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,i)=>e(i))})}runScript(t,e){return new Promise(s=>{let i=this.getdata("@chavy_boxjs_userCfgs.httpapi");i=i?i.replace(/\n/g,"").trim():i;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[o,h]=i.split("@"),n={url:`http://${h}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":o,Accept:"*/*"}};this.post(n,(t,e,i)=>s(i))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):i?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const i=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of i)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,i,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,r]=/^@(.*?)\.(.*?)$/.exec(e),o=this.getval(i),h=i?"null"===o?null:o||"{}":"{}";try{const e=JSON.parse(h);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),i)}catch(e){const o={};this.lodash_set(o,r,t),s=this.setval(JSON.stringify(o),i)}}else s=this.setval(t,e);return s}getval(t){return this.isSurge()||this.isLoon()?$persistentStore.read(t):this.isQuanX()?$prefs.valueForKey(t):this.isNode()?(this.data=this.loaddata(),this.data[t]):this.data&&this.data[t]||null}setval(t,e){return this.isSurge()||this.isLoon()?$persistentStore.write(t,e):this.isQuanX()?$prefs.setValueForKey(t,e):this.isNode()?(this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0):this.data&&this.data[e]||null}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){if(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"]),this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:i,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:i,statusCode:r,headers:o,rawBody:h},s.decode(h,this.encoding))},t=>{const{message:i,response:r}=t;e(i,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";if(t.body&&t.headers&&!t.headers["Content-Type"]&&(t.headers["Content-Type"]="application/x-www-form-urlencoded"),t.headers&&delete t.headers["Content-Length"],this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let i=require("iconv-lite");this.initGotEnv(t);const{url:r,...o}=t;this.got[s](r,o).then(t=>{const{statusCode:s,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:s,statusCode:r,headers:o,rawBody:h},i.decode(h,this.encoding))},t=>{const{message:s,response:r}=t;e(s,r,r&&i.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let i={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in i)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?i[e]:("00"+i[e]).substr((""+i[e]).length)));return t}msg(e=t,s="",i="",r){const o=t=>{if(!t)return t;if("string"==typeof t)return this.isLoon()?t:this.isQuanX()?{"open-url":t}:this.isSurge()?{url:t}:void 0;if("object"==typeof t){if(this.isLoon()){let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}if(this.isQuanX()){let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,i=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":i}}if(this.isSurge()){let e=t.url||t.openUrl||t["open-url"];return{url:e}}}};if(this.isMute||(this.isSurge()||this.isLoon()?$notification.post(e,s,i,o(r)):this.isQuanX()&&$notify(e,s,i,o(r))),!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),i&&t.push(i),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){const s=!this.isSurge()&&!this.isQuanX()&&!this.isLoon();s?this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack):this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t)}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),(this.isSurge()||this.isQuanX()||this.isLoon())&&$done(t)}}(t,e)} -------------------------------------------------------------------------------- /src/mihoyobbs-auto-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 米游社小助手 3 | * @version v2.4.1 4 | * @description 摆脱米游社 每天定时自动执行相关任务. 5 | * @author kayanouriko 6 | * @homepage https://github.com/kayanouriko/quantumultx-mihoyobbs-auto-helper 7 | * @thanks chavyleung, 各家应用环境的统一封装 8 | * @thanks NobyDa, 一些原生算法解决方案参考 9 | * @thanks https://github.com/Womsxd/AutoMihoyoBBS, v2版本业务逻辑部分基本来自该仓库 10 | * @thanks https://github.com/daye99/genshin-sign-helper, v1版本业务逻辑部分基本来自该仓库 11 | * @thanks https://github.com/Azure99/GenshinPlayerQuery/issues/20 关键算法逻辑部分的来源 12 | * @license MIT 13 | */ 14 | 15 | /** env.js 全局 */ 16 | const $ = new Env('米游社小助手') 17 | 18 | /** 通知相关 */ 19 | 20 | // 通知的 option 21 | const msgOpt = { 22 | cookie: { 23 | 'open-url': 'https://github.com/kayanouriko/quantumultx-mihoyobbs-auto-helper' 24 | }, 25 | normal: {}, 26 | } 27 | // 文本信息 28 | const msgText = { 29 | noti: { 30 | title: '米游社小助手', 31 | resultsTitle: '脚本执行完成, 长按通知展开报告或者点击通知在应用内查看报告.\n\n', 32 | resultsEmpty: '脚本执行完成, 不过貌似没有任务执行了Orz', 33 | resultsEnd: '报告结果结束!' 34 | }, 35 | // cookie 相关 36 | cookie: { 37 | empty: '请先打开该脚本配套重写规则更新后获取 headers, 再重新运行该脚本. 点击该通知将跳转获取 headers 的教程页面.', 38 | }, 39 | common: { 40 | user: '获取账号信息有误, 错误信息: {0}.', 41 | uid: '无法正确获取账号信息关键参数.', 42 | sign: '获取账号签到信息有误, 错误信息: {0}.', 43 | today: '无法正确获取账号签到信息关键参数.', 44 | awards: '获取签到奖励信息有误, 错误信息: {0}.', 45 | award: '无法正确获取签到奖励信息关键参数.', 46 | error: '错误信息: {0}.' 47 | }, 48 | // 米游币相关 49 | micoin: { 50 | cookie: 'cookie 已过期, 请重新运行 cookie 获取脚本一次.', 51 | finished: '今日可以获取的米游币已达上限.', 52 | empty: '查询可执行的米游币任务出错.', 53 | state: '获取米游社账号米游币任务完成状态出错, 错误信息: {0}.', 54 | forumid: '配置中的 sections 出错, 请参照脚本配置说明重新配置.', 55 | list: '在{0}讨论区执行米游币任务:\n', 56 | listError: '获取帖子列表有误, 错误信息: {0}.', 57 | listEmpty: '获取到的帖子列表为空.', 58 | signError: '讨论区签到任务执行失败, 错误信息: {0}.\n', 59 | sign: '讨论区签到任务完成(米游币+30).\n', 60 | post: '浏览 3 个帖子任务完成(米游币+20).\n', 61 | postFail: '浏览 3 个帖子任务未完成, 只成功浏览了 {0} 个帖子.\n', 62 | vote: '5 次点赞任务完成(米游币+30).\n', 63 | voteFail: '5 次点赞任务未完成, 只成功点赞了 {0} 个帖子.\n', 64 | shared: '分享帖子任务完成(米游币+10).\n', 65 | sharedFail: '分享帖子任务未完成.\n', 66 | taskEmpty: '不过貌似没有任何米游币任务执行了Orz\n', 67 | success: '米游币任务操作完成!\n{0}\n', 68 | error: '米游币任务操作未完成!\n{0}\n\n' 69 | }, 70 | // 原神签到相关 71 | genshin: { 72 | bind: '请先前往米游社 App 手动签到一次!', 73 | signed: '旅行者"{0}"今日已领取过奖励.', 74 | success: '原神签到操作完成!\n旅行者"{0}"领取了奖励({1}x{2}).\n\n', 75 | error: '原神签到操作未完成!\n{0}\n\n', 76 | riskCode: '触发了风控验证码, 请前往米游社 app 手动签到.' 77 | }, 78 | // 崩坏3rd签到相关 79 | honkai3rd: { 80 | signed: '舰长"{0}"今日已领取过奖励.', 81 | success: '崩坏3rd签到操作完成!\n舰长"{0}"领取了奖励({1}x{2}).\n\n', 82 | error: '崩坏3rd签到操作未完成!\n{0}\n\n' 83 | }, 84 | // 根据类型获取对应的数据 85 | getMsg(type, key) { 86 | return this?.[type]?.[key] 87 | } 88 | } 89 | 90 | /** 米游社 api 相关 */ 91 | 92 | // 米游社的版块 93 | const boards = { 94 | honkai3rd: { 95 | forumid: 1, 96 | key: 'honkai3rd', 97 | biz: 'bh3_cn', 98 | actid: 'e202207181446311', 99 | name: '崩坏3rd', 100 | url: "https://bbs.mihoyo.com/bh3/", 101 | getReferer() { 102 | return `https://webstatic.mihoyo.com/bbs/event/signin/bh3/index.html?bbs_auth_required=true&act_id=${this.actid}&bbs_presentation_style=fullscreen&utm_source=bbs&utm_medium=mys&utm_campaign=icon` 103 | } 104 | }, 105 | genshin: { 106 | forumid: 26, 107 | key: 'genshin', 108 | biz: 'hk4e_cn', 109 | actid: 'e202009291139501', 110 | name: '原神', 111 | url: "https://bbs.mihoyo.com/ys/", 112 | getReferer() { 113 | return `https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?bbs_auth_required=true&act_id=${this.actid}&utm_source=bbs&utm_medium=mys&utm_campaign=icon` 114 | } 115 | }, 116 | honkai2: { 117 | forumid: 30, 118 | biz: 'bh2_cn', 119 | actid: 'e202203291431091', 120 | name: '崩坏学园2', 121 | url: "https://bbs.mihoyo.com/bh2/" 122 | }, 123 | tears: { 124 | forumid: 37, 125 | biz: 'nxx_cn', 126 | name: '未定事件簿', 127 | url: "https://bbs.mihoyo.com/wd/" 128 | }, 129 | house: { 130 | forumid: 34, 131 | name: '大别野', 132 | url: "https://bbs.mihoyo.com/dby/" 133 | }, 134 | honkaisr: { 135 | forumid: 52, 136 | name: '崩坏: 星穹铁道', 137 | url: "https://bbs.mihoyo.com/sr/" 138 | } 139 | } 140 | 141 | /** 请求 url 相关 */ 142 | const api = { 143 | // 获取用户信息(所有游戏通用, 通过不同的游戏 biz 获取绑定的账号信息) 144 | getUserInfo: 'https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz={0}', 145 | // bbs 论坛 146 | micoin: { 147 | // 获取用户任务完成状态 148 | getUserMissionState: 'https://bbs-api.mihoyo.com/apihub/sapi/getUserMissionsState', 149 | // 获取对应版块的帖子列表 150 | getForumPostList: 'https://bbs-api.mihoyo.com/post/api/getForumPostList?forum_id={0}&is_good=false&is_hot=false&page_size=20&sort_type=1', 151 | // 讨论区签到 152 | postSignIn: 'https://bbs-api.mihoyo.com/apihub/app/api/signIn', 153 | // 浏览帖子 154 | getPostFull: 'https://bbs-api.mihoyo.com/post/api/getPostFull?post_id={0}', 155 | // 点赞 156 | postUpVotePost: 'https://bbs-api.mihoyo.com/apihub/sapi/upvotePost', 157 | // 分享 158 | getShareConf: 'https://bbs-api.mihoyo.com/apihub/api/getShareConf?entity_id={0}&entity_type=1' 159 | }, 160 | // 原神签到 161 | genshin: { 162 | // 签到状态 163 | getSignInfo: 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/info?region={0}&act_id={1}&uid={2}', 164 | // 签到奖励 165 | getSignAwards: 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/home?act_id={0}', 166 | // 签到操作 167 | postSign: 'https://api-takumi.mihoyo.com/event/bbs_sign_reward/sign' 168 | }, 169 | honkai3rd: { 170 | // 签到状态 171 | getSignInfo: 'https://api-takumi.mihoyo.com/event/luna/info?lang=zh-cn®ion={0}&act_id={1}&uid={2}', 172 | // 奖励信息 173 | getSignAwards: 'https://api-takumi.mihoyo.com/event/luna/home?lang=zh-cn&act_id={0}', 174 | // 签到操作 175 | postSign: 'https://api-takumi.mihoyo.com/event/luna/sign' 176 | }, 177 | getApi(type) { 178 | return this[type] 179 | } 180 | } 181 | 182 | /** headers */ 183 | // 米游币相关的 headers 184 | const bbsHeadersString = $.getdata('kayanouriko_mihoyobbs_headers_bbs') 185 | // 签到相关的 headers 186 | const signHeadersString = $.getdata('kayanouriko_mihoyobbs_headers_sign') 187 | 188 | /** 189 | * 脚本的配置文件 190 | * 用户可以自定义配置, 每项设置均有说明, 脚本默认不做修改就能运行. 191 | * @param {array} tasks 需要自动执行的任务, 填入数组即可 192 | * 1. 米游币任务 2. 原神签到 3. 崩坏 3rd 签到 193 | * 默认为 [1, 2, 3], 执行米游币, 原神, 崩坏3rd 3 个任务 194 | * @param {object} micoin 米游币任务的配置项, 只有 tasks 项存在 1 时, 该配置项的内容才会生效 195 | * @param {array} scetions 需要执行米游币任务的讨论区, 填入数字数组即可 196 | * 1. 崩坏3, 26. 原神 30. 崩坏学园2 37. 未定事件簿 34. 大别野 52. 崩坏:星穹铁道 197 | * 默认为 [34], 即在大别野帖子列表执行米游币任务(看帖子, 点赞和分享帖子) 198 | * 后续可能会支持自动执行分区的经验任务, 所以这里用数组, 填写多个id也是没问题的(例如: [34, 26]), 但是暂时没什么, 脚本只会使用到数组里面的第一个id 199 | * @param {array} actions 需要执行的米游币任务, 填入数字数组即可 200 | * 58. 讨论区签到 59. 浏览 3 个帖子 60. 完成 5 次点赞 61. 分享帖子 201 | * 默认为 [58, 59, 60, 61], 执行米游社的全部任务 202 | */ 203 | 204 | const defaultConfig = { 205 | tasks: [1, 2, 3], 206 | micoin: { 207 | sections: [34], 208 | actions: [58, 59, 60, 61] 209 | } 210 | } 211 | 212 | let config = $.getdata('kayanouriko_mihoyobbs_config') 213 | 214 | //==== 主入口 ==== 215 | main() 216 | 217 | async function main() { 218 | try { 219 | // 如果用户没用运行过 config 设置脚本, 则采用默认的, 否则使用用户自定义的 220 | if (!config) { 221 | config = defaultConfig 222 | } else { 223 | config = JSON.parse(config) 224 | } 225 | // 执行任务流程 226 | let results = msgText.noti.resultsTitle 227 | for (const id of config.tasks) { 228 | switch (id) { 229 | case 1: 230 | await checkBBSHeaders() 231 | const micoinResult = await micoinTask() 232 | results += micoinResult 233 | break 234 | case 2: 235 | await checkSignHeaders() 236 | const genshinResult = await genshinSignTask() 237 | results += genshinResult 238 | break 239 | case 3: 240 | await checkSignHeaders() 241 | const honkai3rdResult = await honkai3rdSignTask() 242 | results += honkai3rdResult 243 | break 244 | default: 245 | break 246 | } 247 | await randomSleepAsync() 248 | } 249 | if (results === msgText.noti.resultsTitle) { 250 | results = msgText.noti.resultsEmpty 251 | } else { 252 | results += msgText.noti.resultsEnd 253 | } 254 | notify(results, msgOpt.normal) 255 | } catch (error) { 256 | const option = error === msgText.cookie.empty ? msgOpt.cookie : msgOpt.normal 257 | notify(error.message || error, option) 258 | } finally { 259 | $.done() 260 | } 261 | } 262 | 263 | //==== headers 检查 ==== 264 | // v2.4.0 开始, 获取的不在是 cookie, 而是 headers 265 | function checkSignHeaders() { 266 | if (!signHeadersString) { 267 | return Promise.reject(msgText.cookie.empty) 268 | } 269 | } 270 | 271 | function checkBBSHeaders() { 272 | if (!bbsHeadersString) { 273 | return Promise.reject(msgText.cookie.empty) 274 | } 275 | } 276 | 277 | //==== 米游币任务 ==== 278 | // 这里少请求一个米游社用户信息的接口, 获取不到 cookie 的 nickname, 最后脚本提醒时无法显示用户名字 279 | // 不过无关紧要, 尽量减少请求接口的数量, 这个 todo 消除 280 | async function micoinTask() { 281 | try { 282 | // 获取执行任务的 board 283 | const forumid = config.micoin.sections?.[0] ?? 10000 284 | const board = findBoardByID(forumid) 285 | if (board === undefined) { 286 | return Promise.resolve(String.format(msgText.micoin.error, msgText.micoin.forumid)) 287 | } 288 | // 获取任务列表 289 | const tasks = await getUserMissionState() 290 | // 在执行任务之前, 先获取帖子列表 291 | const lists = await getForumPostList(forumid) 292 | await randomSleepAsync() 293 | let results = String.format(msgText.micoin.list, board.name) 294 | // 开始循环执行任务 295 | for (const task of tasks) { 296 | // 如果配置内不包含该任务, 则跳过执行 297 | if (config.micoin.actions.indexOf(task.id) === -1) { continue } 298 | // 任务已经完成的也跳过 299 | if (task.isGetAward) { continue } 300 | // 否则执行任务 301 | switch (task.id) { 302 | case 58: 303 | // 讨论区签到 304 | const signResult = await postSignIn(forumid) 305 | results += signResult 306 | await randomSleepAsync() 307 | break 308 | case 59: 309 | // 看帖子 310 | let postCount = task.times 311 | for (let i = task.times; i < 3; i++) { 312 | postCount += await getPostFull(lists?.[i]) 313 | await randomSleepAsync() 314 | } 315 | results += postCount === 3 ? msgText.micoin.post : String.format(msgText.micoin.postFail, postCount) 316 | break 317 | case 60: 318 | // 帖子点赞 319 | let voteCount = task.times 320 | for (let i = task.times; i < 5; i++) { 321 | voteCount += await postUpVotePost(lists?.[i]) 322 | await randomSleepAsync() 323 | } 324 | results += voteCount === 5 ? msgText.micoin.vote : String.format(msgText.micoin.voteFail, voteCount) 325 | break 326 | case 61: 327 | // 分享 328 | const sharedCode = await getShareConf(lists?.[0]) 329 | const sharedResult = sharedCode === 0 ? msgText.micoin.shared : msgText.micoin.sharedFail 330 | results += sharedResult 331 | await randomSleepAsync() 332 | break 333 | default: 334 | break 335 | } 336 | } 337 | if (results === String.format(msgText.micoin.list, board.name)) { 338 | results = msgText.micoin.taskEmpty 339 | } 340 | return Promise.resolve(String.format(msgText.micoin.success, results)) 341 | } catch (error) { 342 | return Promise.resolve(String.format(msgText.micoin.error, error.message || (error instanceof Object ? JSON.stringify(error) : error))) 343 | } 344 | } 345 | 346 | // 获取用户的任务状态 347 | function getUserMissionState() { 348 | const option = { 349 | url: api.micoin.getUserMissionState, 350 | headers: getBBSHeaders() 351 | } 352 | return $.http.get(option).then(res => { 353 | const { retcode, message, data } = JSON.parse(res.body) 354 | if (retcode === -100) { 355 | // cookie 失效, 需特别处理 356 | return Promise.reject(msgText.micoin.cookie) 357 | } else if (retcode === 0) { 358 | // 今日还能获取的任务米游币 359 | const getCoinsCount = data?.['can_get_points'] ?? 0 360 | if (getCoinsCount === 0) { 361 | // 已经无法通过任务获取米游币 362 | return Promise.reject(msgText.micoin.finished) 363 | } 364 | const states = data?.states ?? [] 365 | let halfTasks = [] 366 | for (const state of states) { 367 | const id = state?.['mission_id'] ?? 10000 368 | const times = state?.['happened_times'] ?? 0 369 | const isGetAward = state?.['is_get_award'] ?? true 370 | // 小于 62 的均为米游币任务 371 | if (id < 62) { 372 | halfTasks.push({ 373 | id, 374 | times, 375 | isGetAward 376 | }) 377 | } 378 | } 379 | // 创建 task 数组 380 | const tasks = [58, 59, 60, 61].map(id => { 381 | let task = halfTasks.find(e => e.id === id) 382 | if (!task) { 383 | task = { 384 | id, 385 | times: 0, 386 | isGetAward: false 387 | } 388 | } 389 | return task 390 | }) 391 | return tasks 392 | } else { 393 | // 其余情况返回接口的报错信息 394 | return Promise.reject(String.format(msgText.micoin.state, message)) 395 | } 396 | }) 397 | } 398 | 399 | // 获取帖子列表 400 | function getForumPostList(forumid) { 401 | const option = { 402 | url: String.format(api.micoin.getForumPostList, forumid), 403 | headers: getBBSHeaders() 404 | } 405 | return $.http.get(option).then(res => { 406 | const { retcode, message, data } = JSON.parse(res.body) 407 | if (retcode !== 0) { 408 | return Promise.reject(String.format(msgText.micoin.listError, message)) 409 | } 410 | const lists = data?.list ?? [] 411 | if (lists.length === 0) { 412 | return Promise.reject(msgText.micoin.listEmpty) 413 | } 414 | return lists 415 | }) 416 | } 417 | 418 | // 讨论区签到 419 | function postSignIn(forumid) { 420 | const json = { 421 | 'gids': forumid 422 | } 423 | const option = { 424 | url: api.micoin.postSignIn, 425 | headers: getBBSHeaders(JSON.stringify(json)), 426 | body: JSON.stringify(json) 427 | } 428 | return $.http.post(option).then(res => { 429 | const { retcode, message } = JSON.parse(res.body) 430 | if (retcode !== 0) { 431 | // 签到操作未完成, 但是下面的任务还需要继续, 所以返回提示文本 432 | return String.format(msgText.micoin.signError, message) 433 | } 434 | return msgText.micoin.sign 435 | }) 436 | } 437 | 438 | // 浏览帖子任务 439 | function getPostFull(post) { 440 | const postid = post?.post?.['post_id'] 441 | if (!postid) { return 0 } 442 | const option = { 443 | url: String.format(api.micoin.getPostFull, postid), 444 | headers: getBBSHeaders() 445 | } 446 | return $.http.get(option).then(res => { 447 | const { retcode } = JSON.parse(res.body) 448 | return retcode === 0 ? 1 : 0 449 | }) 450 | } 451 | 452 | // 点赞任务 453 | function postUpVotePost(post) { 454 | const postid = post?.post?.['post_id'] 455 | if (!postid) { return 0 } 456 | const json = { 457 | 'post_id': postid, 458 | 'is_cancel': false 459 | } 460 | const option = { 461 | url: api.micoin.postUpVotePost, 462 | headers: getBBSHeaders(), 463 | body: JSON.stringify(json) 464 | } 465 | return $.http.post(option).then(res => { 466 | const { retcode } = JSON.parse(res.body) 467 | return retcode === 0 ? 1 : 0 468 | }) 469 | } 470 | 471 | // 分享任务 472 | function getShareConf(post) { 473 | const postid = post?.post?.['post_id'] 474 | if (!postid) { 475 | return 0 476 | } 477 | const option = { 478 | url: String.format(api.micoin.getShareConf, postid), 479 | headers: getBBSHeaders() 480 | } 481 | return $.http.get(option).then(res => { 482 | const { retcode } = JSON.parse(res.body) 483 | return retcode 484 | }) 485 | } 486 | 487 | 488 | //==== 原神签到 ==== 489 | 490 | // 主入口 491 | async function genshinSignTask() { 492 | try { 493 | // 获取 cookie 所属的账号信息 494 | const { game_uid, region, nickname } = await getUserInfo(boards.genshin) 495 | // 获取账号签到信息 (签到次数) 496 | const total = await getGenshinSignInfo(game_uid, region, nickname) 497 | // 获取奖励列表信息 498 | const { name, count } = await getGenshinSignAwards(total) 499 | // 签到操作 500 | await postSign(boards.genshin, game_uid, region) 501 | return Promise.resolve(String.format(msgText.genshin.success, nickname, name, count)) 502 | } catch (error) { 503 | return Promise.resolve(String.format(msgText.genshin.error, error.message || (error instanceof Object ? JSON.stringify(error) : error))) 504 | } 505 | } 506 | 507 | // 获取账号签到信息 508 | function getGenshinSignInfo(game_uid, region, nickname) { 509 | const option = { 510 | url: String.format(api.genshin.getSignInfo, region, boards.genshin.actid, game_uid), 511 | headers: getHeaders(boards.genshin) 512 | } 513 | return $.http.get(option).then(res => { 514 | const { retcode, message, data } = JSON.parse(res.body) 515 | if (retcode !== 0) { 516 | return Promise.reject(String.format(msgText.common.sign, message)) 517 | } 518 | const total_sign_day = data?.['total_sign_day'] 519 | const first_bind = data?.['first_bind'] 520 | const is_sign = data?.['is_sign'] 521 | if (total_sign_day !== undefined && first_bind !== undefined && is_sign !== undefined) { 522 | // 未绑定 523 | if (first_bind) { 524 | return Promise.reject(msgText.genshin.bind) 525 | } 526 | // 已签到 527 | if (is_sign) { 528 | return Promise.reject(String.format(msgText.genshin.signed, nickname)) 529 | } 530 | // 返回总签到次数 531 | return total_sign_day 532 | } else { 533 | return Promise.reject(msgText.common.today) 534 | } 535 | }) 536 | } 537 | 538 | // 获取签到奖励信息 539 | function getGenshinSignAwards(total) { 540 | const option = { 541 | url: String.format(api.genshin.getSignAwards, boards.genshin.actid), 542 | headers: getHeaders(boards.genshin) 543 | } 544 | return $.http.get(option).then(res => { 545 | const { retcode, message, data } = JSON.parse(res.body) 546 | if (retcode !== 0) { 547 | return Promise.reject(String.format(msgText.common.awards, message)) 548 | } 549 | const name = data?.awards?.[total]?.name 550 | const cnt = data?.awards?.[total]?.cnt 551 | if (name && cnt) { 552 | return { 553 | name, 554 | count: cnt 555 | } 556 | } else { 557 | return Promise.reject(msgText.common.award) 558 | } 559 | }) 560 | } 561 | 562 | //==== 崩坏 3rd 签到 ==== 563 | 564 | // 主入口 565 | async function honkai3rdSignTask() { 566 | try { 567 | // 获取账号信息 568 | const { game_uid, region, nickname } = await getUserInfo(boards.honkai3rd) 569 | // 获取签到信息 570 | const total = await getHonkai3rdSignInfo(game_uid, region, nickname) 571 | // 获取奖励信息 572 | const { name, count } = await getHonkai3rdSignAwards(total) 573 | // 签到操作 574 | await postSign(boards.honkai3rd, game_uid, region) 575 | return Promise.resolve(String.format(msgText.honkai3rd.success, nickname, name, count)) 576 | } catch (error) { 577 | return Promise.resolve(String.format(msgText.honkai3rd.error, error.message || (error instanceof Object ? JSON.stringify(error) : error))) 578 | } 579 | } 580 | 581 | // 获取签到状态 582 | function getHonkai3rdSignInfo(game_uid, region, nickname) { 583 | const option = { 584 | url: String.format(api.honkai3rd.getSignInfo, region, boards.honkai3rd.actid, game_uid), 585 | headers: getHeaders(boards.honkai3rd) 586 | } 587 | return $.http.get(option).then(res => { 588 | const { retcode, message, data } = JSON.parse(res.body) 589 | if (retcode !== 0) { 590 | return Promise.reject(String.format(msgText.common.sign, message)) 591 | } 592 | const isSign = data?.['is_sign'] ?? false 593 | if (isSign) { 594 | // 已经签到完成 595 | return Promise.reject(String.format(msgText.honkai3rd.signed, nickname)) 596 | } 597 | const total = data?.['total_sign_day'] 598 | if (total !== undefined) { 599 | return total 600 | } else { 601 | return Promise.reject(msgText.common.today) 602 | } 603 | }) 604 | } 605 | 606 | // 获取奖励信息 607 | function getHonkai3rdSignAwards(total) { 608 | const option = { 609 | url: String.format(api.honkai3rd.getSignAwards, boards.honkai3rd.actid), 610 | headers: getHeaders(boards.honkai3rd) 611 | } 612 | return $.http.get(option).then(res => { 613 | const { retcode, message, data } = JSON.parse(res.body) 614 | if (retcode !== 0) { 615 | return Promise.reject(String.format(msgText.common.awards, message)) 616 | } 617 | const name = data?.awards?.[total]?.name 618 | const cnt = data?.awards?.[total]?.cnt 619 | if (name && cnt) { 620 | return { 621 | name, 622 | count: cnt 623 | } 624 | } else { 625 | return Promise.reject(msgText.common.award) 626 | } 627 | }) 628 | } 629 | 630 | //==== 签到任务 ==== 631 | // @todo 签到任务大概率是接口通用的, 只是部分参数不一样, 可以构造通用方法, 方便后续整合崩2, 事件簿, 铁道等 632 | 633 | // 获取账号信息 通用 634 | function getUserInfo(board) { 635 | const option = { 636 | url: String.format(api.getUserInfo, board.biz), 637 | headers: getHeaders(board) 638 | } 639 | return $.http.get(option).then(res => { 640 | const { retcode, message, data } = JSON.parse(res.body) 641 | if (retcode !== 0) { 642 | return Promise.reject(String.format(msgText.common.user, message)) 643 | } 644 | const game_uid = data?.list?.[0]?.game_uid 645 | const region = data?.list?.[0]?.region 646 | const nickname = data?.list?.[0]?.nickname 647 | // 取出必要数据 648 | if (game_uid && region && nickname) { 649 | return { 650 | game_uid, 651 | region, 652 | nickname 653 | } 654 | } else { 655 | // 无法获取到正确的 uid, region, nickname 656 | return Promise.reject(msgText.common.uid) 657 | } 658 | }) 659 | } 660 | 661 | // 游戏签到操作 逻辑通用, 根据传入的 board 构建不同的参数 662 | function postSign(board, game_uid, region) { 663 | const body = { 664 | act_id: board.actid, 665 | region, 666 | uid: game_uid 667 | } 668 | const option = { 669 | url: api.getApi(board.key).postSign, 670 | headers: getHeaders(board), 671 | body: JSON.stringify(body) 672 | } 673 | return $.http.post(option).then(res => { 674 | const { retcode, message, data } = JSON.parse(res.body) 675 | if (retcode !== 0) { 676 | return Promise.reject(String.format(msgText.common.error, message)) 677 | } 678 | if (board.forumid === 26) { 679 | // 原神游戏签到需要进一步的判断是否触发风险验证码 680 | const riskCode = data?.['risk_code'] ?? 0 681 | if (riskCode !== 0) { 682 | return Promise.reject(msgText.genshin.riskCode) 683 | } 684 | } 685 | }) 686 | } 687 | 688 | //============== 辅助函数 ========================== 689 | 690 | /** 调用系统通知 */ 691 | function notify(message, option) { 692 | $.msg(msgText.noti.title, '', message, option) 693 | } 694 | 695 | /** 随机睡眠 */ 696 | async function randomSleepAsync() { 697 | const s = random(2, 5) 698 | await sleep(s) 699 | } 700 | 701 | /** 休眠 n 秒 */ 702 | function sleep(s) { 703 | return new Promise(resolve => setTimeout(resolve, s * 1000)); 704 | } 705 | 706 | /** 获取 [n, m] 区间的某个随机数 */ 707 | function random(min, max) { 708 | return Math.round(Math.random() * (max - min)) + min; 709 | } 710 | 711 | // 通过 id 获取对应的 board 712 | function findBoardByID(forumid) { 713 | for (const key in boards) { 714 | if (Object.prototype.hasOwnProperty.call(boards, key)) { 715 | const board = boards[key] 716 | if (board.forumid === forumid) { 717 | return board 718 | } 719 | } 720 | } 721 | } 722 | 723 | /** 米游社 api headers */ 724 | 725 | // 通用参数 726 | const headers = { 727 | // 论坛米游币相关参数 728 | clientType: '2', 729 | salt: '6J1hde1Wu02eF1DFlLpMjeg2dMloAytL', 730 | saltV2: 't0qEgfub6cvueAPgR5m9aQWWVciEer7v', 731 | // 游戏签到相关, 内嵌 webview, 所以用的是 web 相关参数 732 | clientTypeWeb: '5', 733 | saltWeb: 'Qqx8cyv7kuyD8fTw11SmvXSFHp7iZD29', 734 | // 通用参数 735 | appVersion: '2.37.1', 736 | deviceId: uuidv4().replace('-', '').toLocaleUpperCase(), 737 | } 738 | 739 | // 2.4.0 改用 headers 之后, 公用参数目前只剩下这两个了. 740 | // 改用 headers 的原因是为了获取用户的 useragent, 不单独拎出来是因为只要控制需要变得比较容易 741 | function getBaseHeaders() { 742 | return { 743 | 'x-rpc-app_version': headers.appVersion, 744 | 'x-rpc-device_id': headers.deviceId 745 | } 746 | } 747 | 748 | // 游戏签到的 headers, 用的是 webview , 所以用的是 web 相关的参数 749 | function getHeaders(board) { 750 | let signHeaders = Object.assign(JSON.parse(signHeadersString), getBaseHeaders()) 751 | signHeaders['Referer'] = board.getReferer() 752 | signHeaders['DS'] = getDS(headers.saltWeb) 753 | signHeaders['x-rpc-client_type'] = headers.clientTypeWeb 754 | return signHeaders 755 | } 756 | 757 | // 米游币任务的 headers 758 | function getBBSHeaders(json) { 759 | let bbsHeaders = Object.assign(JSON.parse(bbsHeadersString), getBaseHeaders()) 760 | bbsHeaders['DS'] = json ? getDSV2(headers.saltV2, '', json) : getDS(headers.salt) 761 | bbsHeaders['x-rpc-client_type'] = headers.clientType 762 | return bbsHeaders 763 | } 764 | 765 | /** ds 获取 */ 766 | // 备注1: x-rpc-client_type 参数: 游戏签到是内嵌 webview 所以用 5 为 web mobile, 米游币为 api 请求 所以用 2 为 安卓 767 | // 备注2: salt 与 x-rpc-app_version 和 x-rpc-client_type 都是联动的 768 | function getDS(n) { 769 | const i = Math.floor(new Date().getTime() / 1000) + '' 770 | const r = getRandomString(6) 771 | const c = md5(`salt=${n}&t=${i}&r=${r}`) 772 | return `${i},${r},${c}` 773 | } 774 | 775 | // ds 的 v2 版本, 目前只有米游币任务签到接口用 776 | // n: salt 777 | // q: 目前暂时不清楚作用, 传空字符串 778 | // b: body 的 json 字符串 779 | function getDSV2(n, q, b) { 780 | const i = Math.floor(new Date().getTime() / 1000) + '' 781 | const r = `${getRandomInt(100001, 200000)}` 782 | const c = md5(`salt=${n}&t=${i}&r=${r}&b=${b}&q=${q}`) 783 | return `${i},${r},${c}` 784 | } 785 | 786 | /** 随机字符串获取 */ 787 | function getRandomString(count) { 788 | const d = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' 789 | const t = d.length 790 | let n = '' 791 | for (var i = 0; i < count; i++) n += d.charAt(Math.floor(Math.random() * t)) 792 | return n 793 | } 794 | 795 | /** 生成 [n, m] 的随机整数 */ 796 | function getRandomInt(min, max) { 797 | return Math.floor(Math.random() * (max - min + 1) + min) 798 | } 799 | 800 | //============= 类与原型上添加方法 ====================== 801 | 802 | /** 格式化字符串 */ 803 | String.format = function (string, ...args) { 804 | let formatted = string 805 | for (let i = 0; i < args.length; i++) { 806 | formatted = formatted.replace('{' + i + '}', args[i]) 807 | } 808 | return formatted 809 | } 810 | 811 | //============== 第三方辅助函数 ========================= 812 | 813 | /** 814 | * uuidv4 生成器简易版本实现 815 | * @see https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid 816 | */ 817 | function uuidv4() { 818 | const chars = '0123456789abcdef'.split('') 819 | 820 | const uuid = [] 821 | const rnd = Math.random 822 | let r = 0 823 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' 824 | uuid[14] = '4' // version 4 825 | 826 | for (var i = 0; i < 36; i++) { 827 | if (!uuid[i]) { 828 | r = 0 | (rnd() * 16) 829 | uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r & 0xf] 830 | } 831 | } 832 | return uuid.join('') 833 | } 834 | 835 | /** 836 | * 从 NobyDa 脚本里面获取到的原生 md5 函数 837 | * @see https://github.com/blueimp/JavaScript-MD5 838 | */ 839 | function md5(string){function RotateLeft(lValue,iShiftBits){return(lValue<>>(32-iShiftBits))}function AddUnsigned(lX,lY){var lX4,lY4,lX8,lY8,lResult;lX8=(lX&0x80000000);lY8=(lY&0x80000000);lX4=(lX&0x40000000);lY4=(lY&0x40000000);lResult=(lX&0x3FFFFFFF)+(lY&0x3FFFFFFF);if(lX4&lY4){return(lResult^0x80000000^lX8^lY8)}if(lX4|lY4){if(lResult&0x40000000){return(lResult^0xC0000000^lX8^lY8)}else{return(lResult^0x40000000^lX8^lY8)}}else{return(lResult^lX8^lY8)}}function F(x,y,z){return(x&y)|((~x)&z)}function G(x,y,z){return(x&z)|(y&(~z))}function H(x,y,z){return(x^y^z)}function I(x,y,z){return(y^(x|(~z)))}function FF(a,b,c,d,x,s,ac){a=AddUnsigned(a,AddUnsigned(AddUnsigned(F(b,c,d),x),ac));return AddUnsigned(RotateLeft(a,s),b)};function GG(a,b,c,d,x,s,ac){a=AddUnsigned(a,AddUnsigned(AddUnsigned(G(b,c,d),x),ac));return AddUnsigned(RotateLeft(a,s),b)};function HH(a,b,c,d,x,s,ac){a=AddUnsigned(a,AddUnsigned(AddUnsigned(H(b,c,d),x),ac));return AddUnsigned(RotateLeft(a,s),b)};function II(a,b,c,d,x,s,ac){a=AddUnsigned(a,AddUnsigned(AddUnsigned(I(b,c,d),x),ac));return AddUnsigned(RotateLeft(a,s),b)};function ConvertToWordArray(string){var lWordCount;var lMessageLength=string.length;var lNumberOfWords_temp1=lMessageLength+8;var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1%64))/64;var lNumberOfWords=(lNumberOfWords_temp2+1)*16;var lWordArray=Array(lNumberOfWords-1);var lBytePosition=0;var lByteCount=0;while(lByteCount>>29;return lWordArray};function WordToHex(lValue){var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;for(lCount=0;lCount<=3;lCount++){lByte=(lValue>>>(lCount*8))&255;WordToHexValue_temp="0"+lByte.toString(16);WordToHexValue=WordToHexValue+WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2)}return WordToHexValue};function Utf8Encode(string){string=string.replace(/\r\n/g,"\n");var utftext="";for(var n=0;n127)&&(c<2048)){utftext+=String.fromCharCode((c>>6)|192);utftext+=String.fromCharCode((c&63)|128)}else{utftext+=String.fromCharCode((c>>12)|224);utftext+=String.fromCharCode(((c>>6)&63)|128);utftext+=String.fromCharCode((c&63)|128)}}return utftext};var x=Array();var k,AA,BB,CC,DD,a,b,c,d;var S11=7,S12=12,S13=17,S14=22;var S21=5,S22=9,S23=14,S24=20;var S31=4,S32=11,S33=16,S34=23;var S41=6,S42=10,S43=15,S44=21;string=Utf8Encode(string);x=ConvertToWordArray(string);a=0x67452301;b=0xEFCDAB89;c=0x98BADCFE;d=0x10325476;for(k=0;k{s.call(this,t,(t,s,r)=>{t?i(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}isNode(){return"undefined"!=typeof module&&!!module.exports}isQuanX(){return"undefined"!=typeof $task}isSurge(){return"undefined"!=typeof $httpClient&&"undefined"==typeof $loon}isLoon(){return"undefined"!=typeof $loon}isShadowrocket(){return"undefined"!=typeof $rocket}isStash(){return"undefined"!=typeof $environment&&$environment["stash-version"]}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const i=this.getdata(t);if(i)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,i)=>e(i))})}runScript(t,e){return new Promise(s=>{let i=this.getdata("@chavy_boxjs_userCfgs.httpapi");i=i?i.replace(/\n/g,"").trim():i;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[o,h]=i.split("@"),n={url:`http://${h}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":o,Accept:"*/*"}};this.post(n,(t,e,i)=>s(i))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e);if(!s&&!i)return{};{const i=s?t:e;try{return JSON.parse(this.fs.readFileSync(i))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),i=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):i?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const i=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of i)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,i)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[i+1])>>0==+e[i+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,i]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,i,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,i,r]=/^@(.*?)\.(.*?)$/.exec(e),o=this.getval(i),h=i?"null"===o?null:o||"{}":"{}";try{const e=JSON.parse(h);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),i)}catch(e){const o={};this.lodash_set(o,r,t),s=this.setval(JSON.stringify(o),i)}}else s=this.setval(t,e);return s}getval(t){return this.isSurge()||this.isLoon()?$persistentStore.read(t):this.isQuanX()?$prefs.valueForKey(t):this.isNode()?(this.data=this.loaddata(),this.data[t]):this.data&&this.data[t]||null}setval(t,e){return this.isSurge()||this.isLoon()?$persistentStore.write(t,e):this.isQuanX()?$prefs.setValueForKey(t,e):this.isNode()?(this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0):this.data&&this.data[e]||null}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){if(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"]),this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:i,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:i,statusCode:r,headers:o,rawBody:h},s.decode(h,this.encoding))},t=>{const{message:i,response:r}=t;e(i,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";if(t.body&&t.headers&&!t.headers["Content-Type"]&&(t.headers["Content-Type"]="application/x-www-form-urlencoded"),t.headers&&delete t.headers["Content-Length"],this.isSurge()||this.isLoon())this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,i)=>{!t&&s&&(s.body=i,s.statusCode=s.status),e(t,s,i)});else if(this.isQuanX())t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:i,headers:r,body:o}=t;e(null,{status:s,statusCode:i,headers:r,body:o},o)},t=>e(t));else if(this.isNode()){let i=require("iconv-lite");this.initGotEnv(t);const{url:r,...o}=t;this.got[s](r,o).then(t=>{const{statusCode:s,statusCode:r,headers:o,rawBody:h}=t;e(null,{status:s,statusCode:r,headers:o,rawBody:h},i.decode(h,this.encoding))},t=>{const{message:s,response:r}=t;e(s,r,r&&i.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let i={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in i)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?i[e]:("00"+i[e]).substr((""+i[e]).length)));return t}msg(e=t,s="",i="",r){const o=t=>{if(!t)return t;if("string"==typeof t)return this.isLoon()?t:this.isQuanX()?{"open-url":t}:this.isSurge()?{url:t}:void 0;if("object"==typeof t){if(this.isLoon()){let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}if(this.isQuanX()){let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,i=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":i}}if(this.isSurge()){let e=t.url||t.openUrl||t["open-url"];return{url:e}}}};if(this.isMute||(this.isSurge()||this.isLoon()?$notification.post(e,s,i,o(r)):this.isQuanX()&&$notify(e,s,i,o(r))),!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),i&&t.push(i),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){const s=!this.isSurge()&&!this.isQuanX()&&!this.isLoon();s?this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack):this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t)}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),(this.isSurge()||this.isQuanX()||this.isLoon())&&$done(t)}}(t,e)} --------------------------------------------------------------------------------