Quantumult X下JS签到脚本开发全过程

Quantumult X下JS签到脚本开发全过程

Bachelor LEE Lv2

前言

近期刚开始学习JavaScript,恰巧看到 青龙面板&圈X(Quantumult X)定时任务的脚本也是用JS编写,所幸找了一个简单的签到案例分析一下,顺便检验一下自己的近期JS学习成果。

声明

以下内容中不含有任何非法修改服务器数据的行为,仅仅是本地用JS代码模拟人类点击的效果完成相关功能。

1.思路分析

在我个人理解来看,所谓的“每日签到”功能,就是当人点击某个按钮后触发了一个事件,而这个事件发起一个对远端服务器的请求,服务器记录下这个请求的时间及相关参数,以此来判定当前用户今日是否已经签到。

微信截图_20240320135823.png

2.开始实施

有了以上的思路,我们就可以通过抓包软件,在点击签到的同时抓取发起请求的连接,分析HTTP连接所携带的参数。

2.1 移动端抓包

tips: 大多数签到请求中,URL中多带有 “check_in”、 “create”、”singin”等字样。

微信图片编辑_20240320142414.jpg
请求体_20240320143514.png

通过分析抓取的请求记录,我们可以看到很多相关信息,其中请求头中携带非常见的字段,一般多为重要信息,图中携带了”KUMI-TOKEN”、”PROJECT-ID”、”PLATFORM”三个特殊字段。再查看以下请求体,里面携带了一个json数据,所以我们把它们暂且保留下来,以便于后面的调试使用。

2.2 PC端调试

1.打开PC端的postman工具,填入我们抓取的请求。

jjy_20240320144132.png
jjybody_20240320144321.png

红色区域为抓取的header字段,Body区域填入抓取到的json数据,填写完毕后,点击send后。

服务器返回的数据如下:

1
2
3
4
{
"code": "I1013",
"message": "不符合签到日期条件"
}

tips: 返回这个结果并不是我们抓取的不对,而是抓包的时候我们已经签到过了,通过postman再次发送相当于再签到一次,所以出现“不符合签到日期条件”。

到这里我们基本上已经搞清楚了整个签到流程,下面我们开始编写我们的JS代码。

3.编写JS脚本

tips: 为了简化开发流程,这里我们引入了 @Peng-YM 大佬开发的OpenAPI 跨平台脚本开发API。

3.1 定义全局变量

1
2
3
4
5
6
7
8
const $ = new  API("JIAJIAYUE签到");
const JJY_TOKEN = "JJY_TOKEN";
const Activity_ID = "2402187cvFhAhp9X"; //这里填写body中的数据
let KUMI_TOKEN = ""; //这里填写抓到的数据
let PROJECT_ID = ""; //这里填写抓到的数据
const json_body = {
activityId:Activity_ID
};

3.2 引入OpenAPI

tips: OpenAPI代码推荐始终保持在文件的末尾

1
2
3
4
5
// prettier-ignore
/*********************************** API *************************************/
function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,i="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!i,isJSBox:i,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:i,isScriptable:n,isNode:o}=ENV(),r=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/;const u={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(l=>u[l.toLowerCase()]=(u=>(function(u,l){l="string"==typeof l?{url:l}:l;const h=e.baseURL;h&&!r.test(l.url||"")&&(l.url=h?h+l.url:l.url);const a=(l={...e,...l}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...l.events};let f,d;if(c.onRequest(u,l),t)f=$task.fetch({method:u,...l});else if(s||i||o)f=new Promise((e,t)=>{(o?require("request"):$httpClient)[u.toLowerCase()](l,(s,i,n)=>{s?t(s):e({statusCode:i.status||i.statusCode,headers:i.headers,body:n})})});else if(n){const e=new Request(l.url);e.method=u,e.headers=l.headers,e.body=l.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const p=a?new Promise((e,t)=>{d=setTimeout(()=>(c.onTimeout(),t(`${u} URL: ${l.url} exceeds the timeout ${a} ms`)),a)}):null;return(p?Promise.race([p,f]).then(e=>(clearTimeout(d),e)):f).then(e=>c.onResponse(e))})(l,u))),u}function API(e="untitled",t=!1){const{isQX:s,isLoon:i,isSurge:n,isNode:o,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(o){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(i||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),o){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(i||n)&&$persistentStore.write(e,this.name),o&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||i)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);o&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||i?$persistentStore.read(e):s?$prefs.valueForKey(e):o?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||i)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);o&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",l="",h={}){const a=h["open-url"],c=h["media-url"];if(s&&$notify(e,t,l,h),n&&$notification.post(e,t,l+`${c?"\n多媒体:"+c:""}`,{url:a}),i){let s={};a&&(s.openUrl=a),c&&(s.mediaUrl=c),"{}"===JSON.stringify(s)?$notification.post(e,t,l):$notification.post(e,t,l,s)}if(o||u){const s=l+(a?`\n点击跳转: ${a}`:"")+(c?`\n多媒体: ${c}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||i||n?$done(e):o&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}
/*****************************************************************************/

3.3 编写签到函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function check_in(){
let params = $.read( JJY_TOKEN).split('@'); //圈X环境下读取存储的TOKEN
console.log(JSON.stringify(params));
KUMI_TOKEN = params[0];
PROJECT_ID = params[1];
let req = { //构造请求
url:"https://mole-mmp-scrm.jiajiayue.com/boss/boss/signin/record/create",
headers:{
'Content-Type': 'application/json',
'Accept': 'application/json',
"KUMI-TOKEN":KUMI_TOKEN,
"PROJECT-ID":PROJECT_ID,
"PLATFORM":"JIAJIAYUE"
},
body:JSON.stringify(json_body)
}
try{
await $.http.post(req).then((resp) =>{ //发起一个异步post请求
let body = JSON.parse(resp.body); //解析返回数据结果
if(body.code == "I1013"){ //参考调试过程中服务器返回的json数据
$.log(body.message);
$.notify("", `${$.name}失败❌重复签到`, "");
}else if(body.code == "1"){
let data = body.data;
if(data.continueNumber == 7 || data.continueNumber == 15){ //活动是每签到7天或15天可以免费抽奖一次
$.notify(`${$.name}签到成功🎉 `, "今天可以去小程序抽奖","");
}else{
$.notify(`${$.name}签到成功🎉 `, "","");
}
}else{
$.notify(`${$.name}未知的错误 `, "","");
}
});
}catch(error){ //如果请求过程出错则打印错误信息
console.log(error);
}
}

3.4 编写Quantumult X 下获取token函数

1
2
3
4
5
6
7
8
9
10
11
function getToken(){
let kt = $request.headers['KUMI-TOKEN']; //读取请求头中的KUMI-TOKEN
let pi = $request.headers['PROJECT-ID']; //读取请求头中的PROJECT-ID
if((kt != "") && (pi!= "")){
let token = kt +"@"+ pi; //用@拼接两个值
$.write(token, JJY_TOKEN); //写入持久化存储
if($.read(JJY_TOKEN)){
$.notify("家家悦Token获取成功~🎉", "", "");
}
}
}

3.5 统一调用

1
2
3
4
5
6
7
8
9
if (isGetCookie = typeof $request !== `undefined`) { 
getToken(); //获取token
$.done();
}else{
!(async()=>{
await check_in(); //执行签到
$.done();
})();
}

3.6 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/***
-------------- Quantumult X 配置 --------------
[MITM]
hostname = mole-mmp-scrm.jiajiayue.com
* 家家悦微信小程序
* 3月份签到活动
* 功能:自动签到
*
* 获取CK: 家家悦小程序 -> 3月份签到
*
[rewrite_local]
^https:\/\/mole-mmp-scrm\.jiajiayue\.com\/boss\/boss\/signin\/record\/list url script-request-header https://raw.githubusercontent.com/tidik/quanx/master/script/jjy.js
[task_local]
30 8 * * * https://raw.githubusercontent.com/tidik/quanx/master/script/jjy.js, tag=家家悦签到,enabled=true
*/
const $ = new API("家家悦签到");
const JJY_TOKEN = "JJY_TOKEN";
const Activity_ID = "2402187cvFhAhp9X";
let KUMI_TOKEN = null;
let PROJECT_ID = null;
const json_body = {
activityId:Activity_ID
};
async function check_in(){
let params = $.read( JJY_TOKEN).split('@');
console.log(JSON.stringify(params));
KUMI_TOKEN = params[0];
PROJECT_ID = params[1];
let req = {
url:"https://mole-mmp-scrm.jiajiayue.com/boss/boss/signin/record/create",
headers:{
'Content-Type': 'application/json',
'Accept': 'application/json',
"KUMI-TOKEN":KUMI_TOKEN,
"PROJECT-ID":PROJECT_ID,
"PLATFORM":"JIAJIAYUE"
},
body:JSON.stringify(json_body)
}
try{
await $.http.post(req).then((resp) =>{
let body = JSON.parse(resp.body);
if(body.code == "I1013"){
$.log(body.message);
$.notify("", `${$.name}失败❌重复签到`, "");
}else if(body.code == "1"){
let data = body.data;
if(data.continueNumber == 7 || data.continueNumber == 15){
$.notify(`${$.name}签到成功🎉 `, "今天可以去小程序抽奖","");
}else{
$.notify(`${$.name}签到成功🎉 `, "","");
}
}else{
$.notify(`${$.name}未知的错误 `, "","");
}
});
}catch(error){
console.log(error);
}
}
function getToken(){
let kt = $request.headers['KUMI-TOKEN'];
let pi = $request.headers['PROJECT-ID'];
if((kt != "") && (pi!= "")){
let token = kt +"@"+ pi;
$.write(token, JJY_TOKEN);
if($.read(JJY_TOKEN)){
$.notify("家家悦Token获取成功~🎉", "", "");
}
}
}
if (isGetCookie = typeof $request !== `undefined`) {
getToken();
$.done();
}else{
!(async()=>{
await check_in();
$.done();
})();

}
// prettier-ignore
/*********************************** API *************************************/
function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,i="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!i,isJSBox:i,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:i,isScriptable:n,isNode:o}=ENV(),r=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/;const u={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(l=>u[l.toLowerCase()]=(u=>(function(u,l){l="string"==typeof l?{url:l}:l;const h=e.baseURL;h&&!r.test(l.url||"")&&(l.url=h?h+l.url:l.url);const a=(l={...e,...l}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...l.events};let f,d;if(c.onRequest(u,l),t)f=$task.fetch({method:u,...l});else if(s||i||o)f=new Promise((e,t)=>{(o?require("request"):$httpClient)[u.toLowerCase()](l,(s,i,n)=>{s?t(s):e({statusCode:i.status||i.statusCode,headers:i.headers,body:n})})});else if(n){const e=new Request(l.url);e.method=u,e.headers=l.headers,e.body=l.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const p=a?new Promise((e,t)=>{d=setTimeout(()=>(c.onTimeout(),t(`${u} URL: ${l.url} exceeds the timeout ${a} ms`)),a)}):null;return(p?Promise.race([p,f]).then(e=>(clearTimeout(d),e)):f).then(e=>c.onResponse(e))})(l,u))),u}function API(e="untitled",t=!1){const{isQX:s,isLoon:i,isSurge:n,isNode:o,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(o){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(i||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),o){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(i||n)&&$persistentStore.write(e,this.name),o&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||i)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);o&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||i?$persistentStore.read(e):s?$prefs.valueForKey(e):o?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||i)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);o&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",l="",h={}){const a=h["open-url"],c=h["media-url"];if(s&&$notify(e,t,l,h),n&&$notification.post(e,t,l+`${c?"\n多媒体:"+c:""}`,{url:a}),i){let s={};a&&(s.openUrl=a),c&&(s.mediaUrl=c),"{}"===JSON.stringify(s)?$notification.post(e,t,l):$notification.post(e,t,l,s)}if(o||u){const s=l+(a?`\n点击跳转: ${a}`:"")+(c?`\n多媒体: ${c}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||i||n?$done(e):o&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}
/*****************************************************************************/

4.配置Quantumult X

4.1 配置重写

将以下代码填写到[rewrite_local]区域

1
^https:\/\/mole-mmp-scrm\.jiajiayue\.com\/boss\/boss\/signin\/record\/list url script-request-header https://raw.githubusercontent.com/tidik/quanx/master/script/jjy.js

4.2 配置定时任务

将以下代码填写到[task_local]区域

1
30 8 * * * https://raw.githubusercontent.com/tidik/quanx/master/script/jjy.js, tag=家家悦签到,enabled=true

4.3 配置MITM

将以下代码填写到[MITM]区域

1
hostname = mole-mmp-scrm.jiajiayue.com

到这一步,配置完以上内容后就可以打开MITM和重写选项,再到对应签到界面就能自动抓取token等相关参数,然后执行定时任务了。

  • 标题: Quantumult X下JS签到脚本开发全过程
  • 作者: Bachelor LEE
  • 创建于 : 2024-03-20 15:35:22
  • 更新于 : 2024-11-05 14:46:30
  • 链接: https://blog.inik.cc/2024/03/20/c471c461c92a.html
  • 版权声明: 本文章采用 CC BY-NC 4.0 进行许可。