├── 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 | 
2 | # 米游社小助手
3 |
4 |  [](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 | |  |
86 | | :------------------------------: |
87 | | 打开应用底部最右侧图标
task-1 |
88 |
89 | |  |
90 | | :----------------------: |
91 | | 点击红框图标
task-2 |
92 |
93 | |  |
94 | | :------------------------------------: |
95 | | 点击右上角加号, 输入仓库地址
task-3 |
96 |
97 | ```
98 | // 仓库地址连接
99 | https://raw.githubusercontent.com/kayanouriko/quantumultx-mihoyobbs-auto-helper/main/task/gallery.json
100 | ```
101 |
102 | |  |
103 | | :---------------------------------------------------------------------: |
104 | | 点击米游社小助手, 在弹出的 sheet 中分别选择添加和添加附加组件
task-4 |
105 |
106 | | 
 |
107 | | :--------------------------------------------------------: |
108 | | 请求列表和重写列表如图所示即为添加成功
task-5 rewrite-1 |
109 |
110 | ### 获取 cookie
111 |
112 | |  |
113 | | :-----------------------------------: |
114 | | 保证重写列表的为打开状态
rewrite-1 |
115 |
116 | > 重写需要配合 MitM 使用, 确保你的 MitM 也是开启状态
117 |
118 | 打开米游社 app, 此时会弹出第一条获取成功的通知, 再随便打开一个游戏的签到页面, 会收到第二条获取成功的通知. 如下所示
119 |
120 | |  |
121 | | :-------------------------------------: |
122 | | 获取 cookie 成功的两条通知
rewrite-3 |
123 |
124 | > 如果之前你的米游社 app 是处于后台开启状态, 可能需要清除后台, 重新打开米游社 app 才能收到米游币任务所需 cookie 获取成功的通知.
125 |
126 | 关闭重写列表, 以后 cookie 失效了再重新打开重复以上步骤重新获取 cookie 即可.
127 |
128 | |  |
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 | |  |
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 | |  |
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)}
--------------------------------------------------------------------------------