巴哈姆特自动签到脚本(适配/开发实例)

巴哈姆特自动签到脚本(适配/开发实例)

简介

本文主要记录一次从巴哈姆特自动签到脚本(油猴)适配至可供Surge、QuantumultX运行的开发过程。

背景

其实这个脚本很久之前就想适配Surge了,之前从Greasyfork搜到这个油猴签到脚本,但是该脚本只能在PC中打开巴哈姆特网页时才能被运行,如果不是每天都逛的话,很容易忘记签到,所以决定适配一下Surge。

当时脚本写了一半,前期测试时发现用户鉴权的Cookie有效期只有1天,这对于大多数自动化签到脚本来说都是致命的,意味着需要频繁去抓Cookie造成毫无使用体验。发现这一情况后并没有深入去研究,而且那段时间也比较忙,所以这个脚本一直处于停滞的状态。

最近在整理脚本时偶然发现这个未完成的脚本,想着可不能半途而废,于是决定深入研究一下巴哈姆特的鉴权方式,顺便分享一下整个过程。

过程

签到脚本最重要的就是用户鉴权方式,一般来说大多数可以被写成脚本的签到都是请求头带上Cookie字段完成用户鉴权的过程,但该方法弊端很明显,例如有效期过短造成需要频繁的去抓取新Cookie。

而且除了上述情况之外,也不是所有APP/网页签到都可以写成脚本,例如相关请求被加密或者需要算法二次验证;这类情况也很难被写成自动化脚本。

幸运的是本文所描述的巴哈姆特签到并没有被加密,待解决的主要问题是每次签到开始时如何拿到最新的Cookie以避免签到过程中鉴权失败。

鉴权分析

因为抓取的请求国内无法直连,使用Thor抓包的话需要路由级翻墙,多数用户并没有这个条件。本文为了方便读者实践,将使用Surge进行抓包,抓到以后导入Thor分析(因为有完善的关键字过滤且查看方便)最后使用Anubis进行重放演示。

既然我们无法使用已经生成好的Cookie,那么我们可以尝试抓取生成Cookie的接口,让脚本每一次运行时都去查询新Cookie,自然就解决了有效期问题。

抓包过程:

  1. 进入巴哈姆特动画疯APP后,退出登录
  2. 打开Surge,MITM主机名输入*:0表示解密所有域名以及所有端口,并开启抓取流量
  3. 进入巴哈姆特动画疯APP,输入账号密码登录
  4. 返回Surge,关闭抓取流量,并禁用*:0解密

抓到请求可以在Surge - 工具 - 已保存的截取数据中找到。

导入Thor:

我们导入Thor后,看到请求并不是很多,所以可以简单搜索一下请求URL关键字,例如我们想抓取登录请求,则尝试搜索login

搜索后有两条符合的请求,可能有的小伙伴就要问了,你怎么知道搜索这个关键字呢?

答案是经验,抓的包多了,很自然就会知道相关请求的关键字,比如现在抓取的是登录请求,login即为登录的意思。如果关键字搜索不到,则只能凭经验逐条分析,没有什么特别好的办法。

回到正题,我们把搜索到的请求进一步查看请求体以及响应体。

吐槽:写这APP的程序员不行阿,怎么说用户密码也得加密一下。

登录请求体中,uid=为用户账号,passwd=为用户密码,以及各种验证码均以明文显示,响应体则是用户基本信息。

我们的需求是获取新Cookie,然后再看一下这个请求的响应头是否返回了Set-Cookie字段

可以看到响应头是有返回Set-Cookie的,但缺少了关键值,猜测可能是请求头中的Cookie已包含所以不返回。

我们右滑该请求,使用Anubis重放,把请求头中Cookie字段里的一些信息删掉,看看是否会返回完整的Set-Cookie

可以看到,删除请求头Cookie中的部分字段进行重放后,响应头的Set-Cookie返回了关键字段,该字段可用于签到的用户鉴权。

然后再进行重放,精简请求头、请求体的参数,看看哪些是必须的。

经过多次重放调试,可以得知:

  1. 请求头中Cookie里的ckBahamutCsrfToken字段需要与请求体中的bahamutCsrfToken字段对应
  2. 请求头中Cookie里的ckAPP_VCODE字段需要与请求体中的vcode字段对应
  3. 除账号密码和以上两个参数不可精简以外,其他都可删除

进一步重放后发现请求体中的vcode字段可以自定义,而bahamutCsrfToken字段不行,

vcode字段我们暂且不管,bahamutCsrfToken字段的话,由于该脚本针对大众,如果多个用户使用同一个内容,可能会出现未知问题,所以得想个办法精简掉(虽然不精简也可以)

经过多次尝试无果后,灵光一现,想到了去Github搜索一下这个字段的关键字:

不得不说Github真是大佬聚集地,点进这个项目后可以看到是一个基于python的巴哈姆特签到项目(瞬间感觉上面抓包抓了个寂寞)

我们进一步查看源码后发现,登录拿Cookie的请求逻辑跟我调试时基本一致,但有几点不同

在他的源码中可以看到请求体并没有携带bahamutCsrfToken等参数,并且发现请求URL用的是v3接口,而我抓的是v4

瞬间豁然开朗,v3接口可以省略掉一些参数,经过重放调试后也可以正常返回Set-Cookie。

至此已经解决登录Cookie问题。

适配脚本

脚本原型为适用于PC浏览器的巴哈姆特签到脚本 (油猴),项目地址:bahamut-auto-sign-script

该脚本业务逻辑包括巴哈签到、巴哈公会签到、动画疯答题等。

得益于该项目,省去了自行抓包分析签到等各种过程,在此表示感谢。

应用兼容

脚本将适配多平台,脚本的写法需要同时兼容多个客户端,那么我们就需要一个兼容函数让它在不同环境下也能被正确执行.

本文将使用chavy大佬的兼容函数。项目地址:https://github.com/chavyleung/scripts/blob/master/Env.js

该兼容函数兼容大多数Surge、QuantumultX、Loon、Shadowrocket的内部API。

兼容函数 >folded
1
2
3

//太长了,略,可点击以上项目地址查看

全局变量

写好兼容函数后我们先定义一些全局变量,供所有函数调用.

全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 以下全局变量中的持久化接口为BoxJs预留, 以便修改
// 把兼容函数定义到$中, 以便统一调用
const $ = new Env('巴哈姆特');

// 用户名
$.uid = $.getdata('@ND_BAHA.ID') || 'YourUserName';

// 用户密码
$.pwd = $.getdata('@ND_BAHA.PW') || 'YourUserPassword';

// 是否自动签到公会,true/false,默认开启
$.needSignGuild = $.getdata('@ND_BAHA.GUILD') || true;

// 是否自动答题动画疯,true/false,默认开启 (不保证100%答题正确)
$.needAnswer = $.getdata('@ND_BAHA.ANSWER') || true;

// 为通知准备的空数组
$.notifyMsg = [];

登录函数

根据前面的分析,我们只需写一个登录请求即可拿到响应头中的Set-Cookie,请求成功后,后续客户端会自动设置其他同类域名请求的Cookie头字段,无需手动设置。如果登录失败,则终止执行脚本。

具体实现:

登录函数
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
function BahamutLogin() { //登录函数,拿到Set-Cookie

//登录成功: {"success":true,"userid":"DGIE","nickname":"coco","gold":152769,"gp":0,"avatar":"https:\/\/avatar2.bahamut.com.tw\/avataruserpic\/dgie.png","avatar_s":"https:\/\/avatar2.bahamut.com.tw\/avataruserpic\/dgie_s.png","lv":6}
//账号错误: {"code":0,"message":"查無此人:SDFOUGB"}
//密码错误: {"code":0,"message":"帳號、密碼或驗證碼錯誤!"}
//验证码错误: {"code":0,"message":"驗證碼錯誤"}

return $.http.post({ //使用post请求查询 (兼容函数实际上返回Promise实例对象,以便后续调用时可以实现顺序执行异步函数)
url: 'https://api.gamer.com.tw/mobile_app/user/v3/do_login.php', //登录接口
headers: { //请求头
'Cookie': 'ckAPP_VCODE=6666' //Cookie中的ckAPP_VCODE为必须
},
//请求体放入用户名和密码,并把它uri编码
body: `uid=${encodeURIComponent($.uid)}&passwd=${encodeURIComponent($.pwd)}&vcode=6666`
})
.then((resp) => { //请求成功的处理
const body = JSON.parse(resp.body); //解析响应体json为对象
if (body.userid) { //如果成功返回用户信息
$.log('', `✅巴哈姆特登录成功`); // 打印日志
} else { //否则登录失败 (例如密码错误)
const failMsg = body.error ? body.error.message : null; //判断签到失败原因
throw new Error(`❌登录失败\n❌${body.message||failMsg||'未知'}`); //带上原因抛出异常, 脚本结束
}
}) //未写catch,如果登录失败,例如无法联网、密码错误等, 则被调用该函数时的catch捕获,脚本结束
}

签到函数(巴哈)

原油猴脚本的业务逻辑为先查询签到Token,再把拿到的Token去请求签到。

造好的轮子适配起来就是舒服,省了抓包;我们直接按照该逻辑去适配。

具体实现:

巴哈姆特签到函数
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
function BahamutSign() { //查询巴哈姆特签到Token
return $.http.get({ //使用get方法 (Promise实例对象) 查询签到Token
url: 'https://www.gamer.com.tw/ajax/get_csrf_token.php', // 查询Token接口
headers: {} //请求头, 客户端将自动设置Cookie字段
}).then(async (resp) => { //网络请求成功的处理, 实例函数带有async关键字, 表示里面有异步操作
if (resp.body) { //如果签到Token获取成功
$.log('', '✅获取签到令牌成功'); //打印日志
const sign = await StartSignBahamut(resp.body); //带上Token开始签到
$.notifyMsg.push(`主页签到: 成功, 已连续签到${sign}天`); //添加到全局变量备用 (通知)
} else { //否则抛出异常
throw new Error('获取签到令牌失败'); //带上原因被下面catch捕获
}
})
.catch(err => {
$.notifyMsg.push(`主页签到: ${err.message||err}`); //添加到全局变量备用 (通知)
$.log('', `❌巴哈姆特签到失败`, `❌${err.message||err}`);
}); // 捕获异常, 打印日志
}

function StartSignBahamut(token) { //巴哈姆特签到

//签到成功: {"data":{"days":1,"dialog":"","prjSigninDays":0}}
//已签过: {"error":{"code":0,"message":"今天您已經簽到過了喔","status":"","details":[]}}
//未登录: {"error":{"code":401,"message":"尚未登入","status":"NO_LOGIN","details":[]}}
//令牌过期: {"error":{"code":403,"message":"網頁已過期","status":"CSRF_TOKEN_ERROR","details":[]}}

return $.http.post({ //使用post方法 (Promise实例对象) 进行签到
url: 'https://www.gamer.com.tw/ajax/signin.php', //巴哈姆特签到接口
headers: {}, //请求头, 客户端将自动设置Cookie字段
body: `action=1&token=${token}` //请求体带上查询到的签到Token
})
.then(res => { // 网络请求成功的处理
const body = JSON.parse(res.body); //解析响应体json为对象
if (body.data) { // 如果签到成功 (判断预期响应格式)
$.log('', '✅巴哈姆特签到成功', `✅已连续签到${body.data.days}天`); //打印日志
return body.data.days; //返回签到天数
} else { //否则签到失败
const failMsg = body.error ? body.error.message : null; //判断签到失败原因
throw new Error(failMsg || body.message || '未知'); //带上原因抛出异常
}
}); //未写catch,如果签到失败或其他错误,则被调用该函数时的catch捕获
}

第一个函数被调用后,查询签到Token成功后则调用第二个函数;如果这俩任何一环出现错误,则直接跳过该签到,继续运行其他任务。

签到函数(公会)

原油猴脚本的业务逻辑为先查询已加入公会的ID,再进行签到操作。

我们直接按照该逻辑去适配。

具体实现:

公会签到函数
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
function BahamutGuildSign() { //巴哈姆特查询公会列表
if ($.needSignGuild === false || $.needSignGuild === 'false') { //如果用户选择不签到公会
return; //退出公会签到函数
}
return $.http.get({ //使用get请求查询公会列表 (Promise实例对象)
url: 'https://api.gamer.com.tw/ajax/common/topBar.php?type=forum', // 查询公会列表接口
headers: {} //请求头, 客户端将自动设置Cookie字段
})
.then(async (resp) => { //网络请求成功的处理, 实例函数带有async关键字, 表示里面有异步操作
const list = (resp.body.replace(/\n/g, '').match(/guild\.php\?g?sn=\d.+?<\/p>/g) || []) //正则过滤公会列表大致内容
.map(n => { //使用map遍历每个大致内容
return { //返回包含公会ID和公会名称的对象
sn: n.split(/guild\.php\?g?sn=(\d+)/)[1], //正则进一步提取公会ID
name: n.split(/<p>(.+?)<\/p>/)[1] //正则进一步提取公会名称
}
});
if (list.length) { //过滤后, 如果包含公会列表
$.log('', `✅获取公会列表成功`); //打印日志
//按照公会数量进行并发签到, map结合Promise.all后可以实现并发签到并且都完成后才进行下一行操作
const sign = await Promise.all(list.map(StartSignGuild));
const sucs = sign.filter(n => n === 1).length; //过滤后得到成功数量
const fail = sign.filter(n => n === 0).length; //过滤后得到失败数量
//添加到全局变量备用 (通知)
$.notifyMsg.push(`公会签到: ${sucs?`成功${sucs}个`:``}${sucs&&fail?`, `:``}${fail?`失败${fail}个`:``}`);
} else {
throw new Error('公会列表为空'); //无公会列表则抛出异常
}
})
.catch(err => { //捕获异常, 打印日志
$.notifyMsg.push(`公会签到: ${err.message || err}`); //添加到全局变量备用 (通知)
$.log('', `❌巴哈姆特公会签到失败`, `❌${err.message || err}`); //打印日志
});
}

function StartSignGuild(v) { //巴哈姆特公会签到

//签到成功: {"ok":1,"msg":"本日簽到成功!獲得5貢獻度"}
//已签过: {"error":1,"msg":"您今天已經簽到過了!"}
//公会ID错误: {"error":1,"msg":"此公會社團不存在。"}
//未加入公会: {"error":1,"msg":"你還不是成員,歡迎加入!"}
//未登录: {"error":1,"msg":"請先登入"}

return $.http.post({ //使用post方法签到公会 (Promise实例对象)
url: 'https://guild.gamer.com.tw/ajax/guildSign.php', //公会签到接口
headers: {}, //请求头, 客户端将自动设置Cookie字段
body: `sn=${v.sn}` //把查询到的公会ID放进请求体
})
.then((res) => { //网络请求成功后的处理
const body = JSON.parse(res.body); //解析响应体json为对象
$.log('', `🔷<${v.name}>`, `${body.ok?`✅`:`❌`}${body.msg}`); //打印日志, 包含签到结果
if (body.ok) { //如果签到成功
return 1; //返回1表示成功
} else {
return 0; //返回0表示失败
}
})
.catch(e => { //捕获异常, 打印日志
$.log('', `🔷<${v.name}>`, `❌签到失败: ${e.message||e}`);
return 0; //返回0表示失败
});
}

第一个函数被调用后,首先判断用户是否开启公会签到,如果开启则先查询已加入公会的ID,查询成功后再调用第二个函数并发签到。

如果用户选择关闭公会签到或者查询公会ID失败,则直接退出该签到,继续运行其他任务。

答题函数(动画疯)

原油猴脚本的答题逻辑稍微有点复杂,简单概括就是默认手动答题 (浏览器),如果开启自动答题,则先查询题目,查询成功后:

  1. blackxblue的小屋获取今日答案的文章ID
  2. 如果找到文章ID,则查询该文章内容找到答案
  3. 如果以上获取失败,则调用moontai0724的题库寻找答案(耗时过长)

所以我们适配时将直接从blackxblue的小屋获取答案,获取失败则跳过该任务

具体实现:

动画疯答题函数
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
86
87
88
89
90
91
92
93
94
function BahamutAnswer() { //动画疯答题

//未答题: {"game":"灌籃高手","question":"流川楓的號碼是下列何者?","a1":"7","a2":"11","a3":"23","a4":"59","userid":"GN32964174","token":"01092fe463ab36ab47cb298e229c4f8fb298e229cc260fa7baf"}
//已答题: {"error":1,"msg":"今日已經答過題目了,一天僅限一次機會"}
//未登录: {"error":1,"nologin":1,"msg":"請先登入"}

if ($.needAnswer === false || $.needAnswer === 'false') { //如果用户关闭动画疯答题
return; //退出答题函数
}
return $.http.get({ //使用get方获取题目 (Promise实例对象)
url: 'https://ani.gamer.com.tw/ajax/animeGetQuestion.php?t=' + Date.now(), //获取题目接口
headers: {} //请求头, 客户端将自动设置Cookie字段
})
.then(async (res) => { //网络请求成功的处理, 实例函数带有async关键字, 表示里面有异步操作
const r = JSON.parse(res.body); //解析响应体json为对象
if (r.token) { //如果有题目
$.log('', `✅获取动画疯题目成功`, ``, `🔶<${r.game}> ${r.question}`,
`1️⃣${r.a1}`, `2️⃣${r.a2}`, `3️⃣${r.a3}`, `4️⃣${r.a4}`); //打印日志
const article = await GetAanswerArticles(); //获取答案文章ID
const getAnswer = await StartSearchAnswers(article); //传入文章ID, 再从文章内获取答案
const sendAnswer = await StartBahamutAnswer(getAnswer, r.token); //传入答案和题目令牌, 开始答题
$.notifyMsg.push(`动画答题: ${sendAnswer}`); //答题后的结果添加到全局变量备用 (通知)
} else { //未获取到题目
throw new Error(r.msg || `获取题目失败`); //带上原因抛出异常
}
})
.catch(e => { //捕获异常, 打印日志
$.notifyMsg.push(`动画答题: ${e.message||e||`失败`}`); //添加到全局变量备用 (通知)
$.log('', `❌动画疯答题失败`, `❌${e.message||e}`); //打印日志
});
}

function GetAanswerArticles() { // 从blackxblue的小屋查询含答案的文章ID
$.log('', `🔶开始获取文章`); //打印日志
return $.http.get({ //使用get方法获取文章ID (Promise实例对象)
url: 'https://api.gamer.com.tw/mobile_app/bahamut/v1/home.php?owner=blackXblue&page=1', //获取文章ID接口
headers: {}
})
.then((res) => { //网络请求成功后的处理
const body = JSON.parse(res.body); //解析响应体json为对象
const tDate = $.time('MM/dd'); //返回今日日期
const title = (body.creation || []).filter(t => t.title.includes(tDate)); //过滤后返回今日答案文章
if (title.length && title[0].sn) { //如果有答案文章
$.log('', `✅获取文章成功 (${title[0].sn})`); //打印日志
return title[0].sn; //返回文章ID
} else { //否则带上原因抛出异常, 被调用该函数时的catch捕获
throw new Error('今日答案未发表');
}
})
}

function StartSearchAnswers(id) { //获取文章内答案
$.log('', `🔶开始获取答案`); //打印日志
return $.http.get({ //使用get方法获取答案 (Promise实例对象)
url: 'https://api.gamer.com.tw/mobile_app/bahamut/v1/home_creation_detail.php?sn=' + id, //获取答案接口
headers: {}
})
.then((res) => { //网络请求成功后的处理
const body = JSON.parse(res.body); //解析响应体json为对象
const answers = body.content.split(/A:(\d)/)[1]; //正则提取答案
if (answers) { //如果成功提取答案
$.log('', `✅获取答案成功 (${answers})`); //打印日志
return answers; //返回答案
} else { //否则带上原因抛出异常, 被调用该函数时的catch捕获
throw new Error('提取答案失败');
}
})
}

function StartBahamutAnswer(answer, token) { //动画疯答题

//答题正确: {"ok":1,"gift":"恭喜您得到:300 巴幣"}
//答题错误: {"error":1,"msg":"答題錯誤"}
//令牌过期: {"error":1,"msg":"很抱歉!本題目已超過時效!"}
//已答题: {"error":1,"msg":"今日已經答過題目了,一天僅限一次機會"}
//未登录: {"error":1,"nologin":1,"msg":"請先登入"}

$.log('', `🔶开始答题`); //打印日志
return $.http.post({ //使用post方法提交答案 (Promise实例对象)
url: 'https://ani.gamer.com.tw/ajax/animeAnsQuestion.php', //提交答案接口
headers: {}, //请求头, 客户端将自动设置Cookie字段
body: `token=${token}&ans=${answer}&t=${Date.now()}`, //请求体带上答案和答案令牌
})
.then((res) => { //网络请求成功后的处理
const body = JSON.parse(res.body); //解析响应体json为对象
if (body.ok) { //如果答题成功
$.log('', `✅${body.gift}`); //打印奖励日志
return body.gift; //返回奖励内容
} else { //否则答题失败
const failMsg = body.error ? body.error.message : null; //提取签到失败原因
throw new Error(body.msg || failMsg || '未知'); //否则带上原因抛出异常, 被调用该函数时的catch捕获
}
})
}

共有四个函数,第一个函数被调用后首先查询题目,如果查询成功:

  1. 调用第二个函数从blackxblue的小屋查询今日答案的文章ID
  2. 调用第三个函数查询文章内的答案
  3. 调用第四个函数则进行答题

如果以上任一环节查询失败,则不会再继续执行其余函数,跳过动画疯答题。

统一调用

写好函数后,如果不进行调用,那么代码是无法被运行的.

在javascript中,代码的执行顺序很重要,我们的需求是先完成一个任务后再进行下一个任务,最后退出脚本.

以下匿名函数将按我们预期的顺序执行.

匿名异步函数
1
2
3
4
5
6
7
8
9
10
11
12
13
(async function() { // 立即运行的匿名异步函数
await BahamutLogin(); // 登录
await BahamutGuildSign(); //签到巴哈公会
await BahamutSign(); //签到巴哈
await BahamutAnswer(); //动画疯答题
})().catch((e) => $.notifyMsg.push(e.message || e)) //捕获登录函数等抛出的异常, 并把原因添加到全局变量(通知)
.finally(() => { //finally在catch之后无论有无异常都会执行
$.msg(`巴哈姆特`, ``, $.notifyMsg.join('\n'), {
'open-url': 'crazyanime://', //动画疯url scheme
'media-url': 'https://cdn.jsdelivr.net/gh/NobyDa/mini@master/Color/bahamutClear.png' //通知图片
}); //带上总结推送通知
$.done(); //调用Surge、QX内部特有的函数, 用于退出脚本执行
});

至此脚本适配已完成。

配置任务

我们写好脚本后,就可以在Surge或QuantumultX里配置定时任务让它在规定时间内执行该脚本.

以下将用Surge进行演示

编辑Surge配置文件,在[Script]段落放入以下脚本

1
巴哈姆特签到 = type=cron,cronexp="0 8 * * *",wake-system=1,timeout=30,script-path=https://raw.githubusercontent.com/NobyDa/Script/master/Bahamut/BahamutDailyBonus.js

以上配置将在每天的早上8:00执行。

有几点需要注意:

  1. 需要进入BoxJs填写账号密码,或者手动填入脚本(全局变量)
  2. 脚本不建议在凌晨执行(需要获取blackxblue发布的动画疯答案)
  3. 脚本兼容Surge、QuantumultX、Loon、Shadowrocket 以及 Node.js (需安装got、tough-cookie模块)

后记

虽然油猴版巴哈姆特签到并非鄙人原创,但二次适配起来并不亚于重构整个脚本,脚本的逻辑也需要重新设计。

教学类型文章不可避免会有一些专业性,抓包部分对于初学者来说并不难,脚本部分也包含大量的注释供读者理解执行逻辑。

俗话说:授人以鱼不如授人以渔,授之以渔可解一生之需;

祝大家学成归来。

巴哈姆特自动签到脚本(适配/开发实例)

https://nobyda.github.io/2021/07/24/Bahamut_daily_bonus_js_example/

作者

NobyDa

发布于

2021-07-24

更新于

2021-07-24

许可协议

评论