feat(login): kg 扫码/短信验证码登录、QQ 扫码登录
- kg:sdk/auth 新增 qrKey/qrCheck 扫码与 sendCaptcha/cellphoneLogin 短信登录,路由接线 - tx:新增 /login/tx/qr(/check)(移植 sansenjian/qq-music-api):ptlogin 扫码 → check_sig 换 p_skey → OAuth authorize 换 code → QQConnectLogin 换凭据; 状态码对齐 wy(800 过期/801 等待/802 已扫/803 成功),qrsig 失效 403 映射 800; 成功凭据入库(通常含 openid/refresh_key,可走自动续期)
This commit is contained in:
1 parent
96f6a7fe91
commit
0f9989b26f
4 files changed
+479
-25
No files matched your search
+156
-8
@@ -1,6 +1,6 @@
|
||||
// 酷狗登录 —— token 绑定与验证(网关 android 签名,与 KuGouMusicApi 对齐)
|
||||
// 完整的手机号/扫码登录需移植其 RSA/AES 全套协议,后续按需扩展;
|
||||
// token 可从酷狗 App 抓包获取(login.token)
|
||||
// 酷狗登录 —— token 绑定 / 扫码 / 短信验证码(与 MakcRe/KuGouMusicApi 协议对齐)
|
||||
// 扫码走 login-user.kugou.com 的 web 签名通道;短信走 android 签名 + AES/RSA;
|
||||
// token 也可从酷狗 App 抓包获取(login.token)后经 /login/kg 绑定
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import { httpFetch } from '../common/request.ts'
|
||||
@@ -12,6 +12,11 @@ const SALT_KEY = '57ae12eb6890223e355ccfcb74edf70d'
|
||||
const SALT_SIGN = 'OIlwieks28dk2k092lksi2UIkp'
|
||||
const APPID = 1005
|
||||
const CLIENTVER = 11430
|
||||
// 登录专用域按参考实现固定 clientver 20489(qr/短信接口实测通过的版本)
|
||||
const LOGIN_CLIENTVER = 20489
|
||||
const WEB_SALT = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'
|
||||
const SRC_APPID = 2919
|
||||
const QR_URL_PREFIX = 'https://h5.kugou.com/apps/loginQRCode/html/index.html'
|
||||
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
@@ -39,8 +44,14 @@ export const getDevice = () => {
|
||||
return dev
|
||||
}
|
||||
|
||||
/** 网关移动端签名请求(gateway.kugou.com + x-router) */
|
||||
const gatewayRequest = async(url, { params: extraParams, data, router, headers: extraHeaders = {} }) => {
|
||||
/** 网关移动端签名请求(默认 gateway.kugou.com + x-router,可 host 直连登录专用域) */
|
||||
const gatewayRequest = async(url, { params: extraParams, data, router, host = 'https://gateway.kugou.com', headers: extraHeaders = {} }: {
|
||||
params?: Record<string, any>
|
||||
data?: any
|
||||
router?: string
|
||||
host?: string
|
||||
headers?: Record<string, string>
|
||||
}) => {
|
||||
const dev = getDevice()
|
||||
const params = {
|
||||
dfid: dev.dfid, mid: dev.mid, uuid: '-', appid: APPID, clientver: CLIENTVER,
|
||||
@@ -49,13 +60,13 @@ const gatewayRequest = async(url, { params: extraParams, data, router, headers:
|
||||
}
|
||||
const signature = androidSign(params, data ? JSON.stringify(data) : '')
|
||||
const qs = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
|
||||
const r = await httpFetch(`https://gateway.kugou.com${url}?${qs}&signature=${signature}`, {
|
||||
const r = await httpFetch(`${host}${url}?${qs}&signature=${signature}`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
dfid: dev.dfid, mid: dev.mid, clienttime: String(params.clienttime),
|
||||
'kg-rc': '1', 'kg-thash': '5d816a0', 'kg-rec': '1', 'kg-rf': 'B9EDA08A64250DEFFBCADDEE00F8F25F',
|
||||
'x-router': router,
|
||||
'User-Agent': 'Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi',
|
||||
...(router ? { 'x-router': router } : {}),
|
||||
...extraHeaders,
|
||||
},
|
||||
body: data ?? undefined,
|
||||
@@ -98,8 +109,145 @@ export const loginByToken = async({ token, userid }) => {
|
||||
return profile
|
||||
}
|
||||
|
||||
// ---- 扫码登录(login-user.kugou.com,web 签名,盐与 H5 网页端一致)----
|
||||
|
||||
const webSign = (params) => {
|
||||
const s = Object.keys(params).map(k => `${k}=${params[k]}`).sort().join('')
|
||||
return md5(`${WEB_SALT}${s}${WEB_SALT}`)
|
||||
}
|
||||
|
||||
/** login-user.kugou.com GET 请求(web 签名,与取流共用同一设备指纹) */
|
||||
const loginUserGet = async(url, extraParams = {}) => {
|
||||
const dev = getDevice()
|
||||
const params = {
|
||||
dfid: dev.dfid, mid: dev.mid, uuid: '-',
|
||||
appid: APPID, clientver: LOGIN_CLIENTVER, clienttime: Math.floor(Date.now() / 1000),
|
||||
...extraParams,
|
||||
}
|
||||
const qs = Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k])}`).join('&')
|
||||
const r = await httpFetch(`https://login-user.kugou.com${url}?${qs}&signature=${webSign(params)}`, {
|
||||
headers: {
|
||||
dfid: dev.dfid, mid: dev.mid, clienttime: String(params.clienttime),
|
||||
'kg-rc': '1', 'kg-thash': '5d816a0', 'kg-rec': '1', 'kg-rf': 'B9EDA08A64250DEFFBCADDEE00F8F25F',
|
||||
'User-Agent': 'Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi',
|
||||
},
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
const body = r.body ?? {}
|
||||
if (body.status !== 1) {
|
||||
const err = new Error(body.error ?? '酷狗登录服务请求失败')
|
||||
;(err as any).status = 502
|
||||
throw err
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
/** 保存登录凭据并拉取账号资料(扫码/短信登录共用;资料校验失败不阻断登录) */
|
||||
const applyKgLogin = async({ token, userid }) => {
|
||||
try {
|
||||
const profile = await verifyToken({ token, userid })
|
||||
setCredentials('kg', { token, userid: profile.userid, nickname: profile.nickname })
|
||||
return profile
|
||||
} catch {
|
||||
const uid = Number(userid) || 0
|
||||
setCredentials('kg', { token, userid: uid })
|
||||
return { userid: uid, nickname: null, vip: null }
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建扫码登录 key 与二维码(qrimg 为可直接展示的 data URL) */
|
||||
export const qrKey = async() => {
|
||||
const body = await loginUserGet('/v2/qrcode', {
|
||||
appid: 1001, type: 1, plat: 4,
|
||||
qrcode_txt: `${QR_URL_PREFIX}?appid=${APPID}&`,
|
||||
srcappid: SRC_APPID,
|
||||
})
|
||||
const key = body.data?.qrcode
|
||||
if (!key) throw new Error('获取二维码 key 失败')
|
||||
const qrurl = `${QR_URL_PREFIX}?qrcode=${key}`
|
||||
const qrimg = body.data.qrcode_img
|
||||
|| await import('qrcode').then(m => m.default.toDataURL(qrurl)).catch(() => '')
|
||||
return { key, qrurl, qrimg }
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询扫码状态(沿用登录路由的 800/801/802/803 语义,酷狗原生为 0/1/2/4)
|
||||
* @returns {{ code: 800|801|802|803, message?: string, profile? }} 800 过期 / 801 等待 / 802 已扫码 / 803 已授权(完成登录)
|
||||
*/
|
||||
export const qrCheck = async(key) => {
|
||||
const body = await loginUserGet('/v2/get_userinfo_qrcode', {
|
||||
plat: 4, appid: APPID, srcappid: SRC_APPID, qrcode: key,
|
||||
})
|
||||
const qrStatus = body.data?.status
|
||||
if (qrStatus === 4) {
|
||||
const { token, userid } = body.data
|
||||
if (!token) throw new Error('登录响应中缺少 token')
|
||||
return { code: 803, message: '登录成功', profile: await applyKgLogin({ token, userid }) }
|
||||
}
|
||||
const map = { 0: [800, '二维码已过期'], 1: [801, '等待扫码'], 2: [802, '已扫码,待确认'] }
|
||||
const [code, message] = map[qrStatus] ?? [qrStatus, `未知状态 ${qrStatus}`]
|
||||
return { code, message }
|
||||
}
|
||||
|
||||
// ---- 手机验证码登录 ----
|
||||
// 参考 KuGouMusicApi captcha_sent / login_cellphone:签名数据中 mobile 打码
|
||||
// (前 2 位 + ***** + 末位),完整号码与验证码走 AES 随机密钥加密;
|
||||
// pk = RSA 裸加密 {clienttime_ms, key},服务端解出密钥后还原 params。
|
||||
|
||||
const signLoginKey = (clienttimeMs) => md5(`${APPID}${SALT_SIGN}${LOGIN_CLIENTVER}${clienttimeMs}`)
|
||||
|
||||
/** 发送短信验证码(businessid 5 = 登录场景) */
|
||||
export const sendCaptcha = async(mobile) => {
|
||||
const body = await gatewayRequest('/v7/send_mobile_code', {
|
||||
router: 'login.user.kugou.com',
|
||||
params: { clientver: LOGIN_CLIENTVER },
|
||||
data: { businessid: 5, mobile: String(mobile), plat: 3 },
|
||||
})
|
||||
if (body.status !== 1) {
|
||||
const err = new Error(body.error ?? body.msg ?? '验证码发送失败')
|
||||
;(err as any).status = 502
|
||||
throw err
|
||||
}
|
||||
return { sent: true }
|
||||
}
|
||||
|
||||
/** 手机验证码登录(mobile + code,成功后自动保存凭据并返回账号资料) */
|
||||
export const cellphoneLogin = async({ mobile, code }) => {
|
||||
const clienttimeMs = Date.now()
|
||||
const enc = aesEncryptRandom({ mobile: String(mobile), code: String(code) })
|
||||
const body = await gatewayRequest('/v7/login_by_verifycode', {
|
||||
host: 'https://loginserviceretry.kugou.com',
|
||||
params: { clientver: LOGIN_CLIENTVER },
|
||||
data: {
|
||||
plat: 1,
|
||||
support_multi: 1,
|
||||
t1: 0,
|
||||
t2: 0,
|
||||
clienttime_ms: clienttimeMs,
|
||||
mobile: `${String(mobile).slice(0, 2)}*****${String(mobile).slice(10, 11)}`,
|
||||
key: signLoginKey(clienttimeMs),
|
||||
pk: rsaEncryptHex({ clienttime_ms: clienttimeMs, key: enc.key }).toUpperCase(),
|
||||
params: enc.str,
|
||||
t3: 'MCwwLDAsMCwwLDAsMCwwLDA=',
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'Android16-1070-11440-130-0-LOGIN-wifi',
|
||||
'support-calm': '1',
|
||||
},
|
||||
})
|
||||
if (body.status !== 1) {
|
||||
const err = new Error(body.error ?? `登录失败(code ${body.error_code ?? '未知'})`)
|
||||
;(err as any).status = 401
|
||||
throw err
|
||||
}
|
||||
const decrypted = body.data?.secu_params ? aesDecryptHex(body.data.secu_params, enc.key) : null
|
||||
const token = typeof decrypted === 'object' ? decrypted?.token : decrypted
|
||||
if (!token) throw new Error('登录响应解密失败')
|
||||
return applyKgLogin({ token, userid: decrypted?.userid ?? body.data?.userid ?? 0 })
|
||||
}
|
||||
|
||||
// ---- token 自动续期 ----
|
||||
// login_by_token 协议(与 lx-music-api-server 对齐):
|
||||
// login_by_token 协议:
|
||||
// p3 = AES({clienttime, token}),密钥由固定串派生;params = AES({})(随机密钥);
|
||||
// pk = RSA 裸加密 {clienttime_ms, key: 随机密钥}(右侧零填充 128 字节);
|
||||
// 成功响应 data.secu_params 用随机密钥 AES 解密得到新 token。
|
||||
|
||||
+196
-3
@@ -1,9 +1,12 @@
|
||||
// QQ音乐凭据 —— cookie 解析、VIP 检测与登录态续期(music.login.LoginServer)
|
||||
// 与 lx-music-api-server 的 refresh/tx.py 对齐,但走本项目既有的 musicu.fcg 未签名通道
|
||||
// (与 apis/tx.ts 一致);Login/VipLogin 模块若拒绝未签名请求则以 code != 0 失败,
|
||||
// QQ音乐凭据 —— cookie 解析、扫码登录、VIP 检测与登录态续期(music.login.LoginServer)
|
||||
// 登录态续期走本项目既有的 musicu.fcg 未签名通道(与 apis/tx.ts 一致);
|
||||
// Login/VipLogin 模块若拒绝未签名请求则以 code != 0 失败,
|
||||
// 此时保留原凭据并抛错,由调用方记录日志。
|
||||
// 续期条件:仅浏览器 cookie(无 openId 等抓包字段)时只带 musicid/musickey 尝试,
|
||||
// 失败率较高;完整凭据(/login/tx 可选传入 openId/accessToken/refreshToken/refreshKey)最可靠。
|
||||
// 扫码登录(参考 sansenjian/qq-music-api):ptlogin 扫码 → check_sig →
|
||||
// graph.qq.com OAuth authorize 换 code → QQConnectLogin.LoginServer/QQLogin 换凭据,
|
||||
// 成功响应体一般携带 refresh_key 等完整字段,可走自动续期。
|
||||
|
||||
import { httpFetch } from '../common/request.ts'
|
||||
import { getCredentials, setCredentials } from './store.ts'
|
||||
@@ -99,3 +102,193 @@ export const refreshLogin = async() => {
|
||||
const vipType = await checkVip().catch(() => null)
|
||||
return { refreshed: true, ...(vipType ? { vipType } : {}) }
|
||||
}
|
||||
|
||||
// ---------------- 扫码登录 ----------------
|
||||
|
||||
/** ptqrtoken:hash33(qrsig) */
|
||||
const hash33 = (str: string): number => {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; ++i) hash += (hash << 5) + str.charCodeAt(i)
|
||||
return 2147483647 & hash
|
||||
}
|
||||
|
||||
/** g_tk:p_skey 的 5381 哈希 */
|
||||
const getGtk = (skey: string): number => {
|
||||
let hash = 5381
|
||||
for (let i = 0; i < skey.length; ++i) hash += (hash << 5) + skey.charCodeAt(i)
|
||||
return 2147483647 & hash
|
||||
}
|
||||
|
||||
const randomGuid = (): string => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16).toUpperCase()
|
||||
})
|
||||
|
||||
/** 登录链路共享的 cookie 罐:同名后到优先,存「name=value」对 */
|
||||
class CookieJar {
|
||||
private jar = new Map<string, string>()
|
||||
|
||||
merge(setCookie: string[] | undefined) {
|
||||
for (const item of setCookie ?? []) {
|
||||
const pair = item.split(';')[0]
|
||||
const name = pair.split('=')[0]?.trim()
|
||||
if (name && pair.includes('=')) this.jar.set(name, pair)
|
||||
}
|
||||
}
|
||||
|
||||
get(name: string) { return this.jar.get(name)?.split('=').slice(1).join('=') ?? '' }
|
||||
|
||||
entries() { return this.jar.entries() }
|
||||
|
||||
header() { return [...this.jar.values()].join('; ') }
|
||||
}
|
||||
|
||||
const ptloginRequest = async(path: string, query: Record<string, string>, cookie: string) => {
|
||||
const r = await httpFetch(`https://ssl.ptlogin2.qq.com/${path}?${new URLSearchParams(query)}`, {
|
||||
method: 'get',
|
||||
...(cookie ? { headers: { cookie } } : {}),
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
if (r.statusCode !== 200) {
|
||||
const err = new Error(`ptlogin ${path} 失败(HTTP ${r.statusCode})`) as any
|
||||
err.statusCode = r.statusCode
|
||||
throw err
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
/** 创建扫码登录二维码(客户端扫码后轮询 qrCheck),qrsig 需原样回传 */
|
||||
export const qrShow = async() => {
|
||||
const r = await httpFetch(
|
||||
'https://ssl.ptlogin2.qq.com/ptqrshow?appid=716027609&e=2&l=M&s=3&d=72&v=4' +
|
||||
`&t=${Math.random()}&daid=383&pt_3rd_aid=100497308&u1=${encodeURIComponent('https://graph.qq.com/oauth2.0/login_jump')}`,
|
||||
{ method: 'get', timeout: 15000 },
|
||||
).promise
|
||||
const qrsig = (r.headers['set-cookie'] ?? []).map(c => c.match(/qrsig=([^;]+)/)?.[1]).find(Boolean)
|
||||
if (!qrsig) throw new Error('获取二维码失败(响应缺少 qrsig)')
|
||||
return { qrimg: `data:image/png;base64,${r.raw.toString('base64')}`, qrsig }
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询扫码状态(与 wy 扫码一致的状态码语义)
|
||||
* @returns {{ code: 800|801|802|803, message?: string, uin?, vipType? }} 800 过期 / 801 等待 / 802 已扫码 / 803 成功(凭据已保存)
|
||||
*/
|
||||
export const qrCheck = async(qrsig: string) => {
|
||||
if (!qrsig) {
|
||||
const err = new Error('缺少 qrsig') as any
|
||||
err.status = 422
|
||||
throw err
|
||||
}
|
||||
const jar = new CookieJar()
|
||||
jar.merge([`qrsig=${qrsig}`])
|
||||
|
||||
// 1. 轮询扫码状态:0 成功 / 65 已失效 / 66 未扫描 / 67 已扫描 / 68 授权中
|
||||
// qrsig 无效/已被 ptlogin 下线时返回 403,按二维码过期处理
|
||||
let poll
|
||||
try {
|
||||
poll = await ptloginRequest('ptqrlogin', {
|
||||
u1: 'https://graph.qq.com/oauth2.0/login_jump',
|
||||
ptqrtoken: String(hash33(qrsig)),
|
||||
ptredirect: '0', h: '1', t: '1', g: '1',
|
||||
from_ui: '1', ptlang: '2052',
|
||||
action: `0-0-${Date.now()}`,
|
||||
js_ver: '23111510', js_type: '1',
|
||||
pt_uistyle: '40', aid: '716027609', daid: '383',
|
||||
pt_3rd_aid: '100497308',
|
||||
o1vId: randomGuid(),
|
||||
}, jar.header())
|
||||
} catch (err: any) {
|
||||
if (err.statusCode === 403) return { code: 800, message: '二维码已失效' }
|
||||
throw err
|
||||
}
|
||||
jar.merge(poll.headers['set-cookie'])
|
||||
|
||||
const text = typeof poll.body === 'string' ? poll.body : ''
|
||||
const cb = text.match(/ptuiCB\('(\d+)','[^']*','([^']*)'/)
|
||||
const state = cb?.[1] ?? ''
|
||||
if (state !== '0') {
|
||||
const states: Record<string, [number, string]> = {
|
||||
65: [800, '二维码已失效'],
|
||||
66: [801, '等待扫码'],
|
||||
67: [802, '已扫码,待确认'],
|
||||
68: [802, '授权中'],
|
||||
}
|
||||
const [code, message] = states[state] ?? [801, '等待扫码']
|
||||
return { code, message }
|
||||
}
|
||||
|
||||
// 2. check_sig 换 p_skey(g_tk 需要)
|
||||
const checkSigUrl = cb?.[2]
|
||||
if (!checkSigUrl?.startsWith('http')) throw new Error('登录检查失败(未返回 check_sig 地址)')
|
||||
const checkSig = await httpFetch(checkSigUrl, {
|
||||
method: 'get',
|
||||
headers: { cookie: jar.header() },
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
jar.merge(checkSig.headers['set-cookie'])
|
||||
const pSkey = jar.get('p_skey')
|
||||
if (!pSkey) throw new Error('登录检查失败(响应缺少 p_skey)')
|
||||
|
||||
// 3. OAuth authorize 换 code(appid 100497308 为 QQ 音乐开放平台)
|
||||
const authorize = await httpFetch('https://graph.qq.com/oauth2.0/authorize', {
|
||||
method: 'post',
|
||||
headers: { Referer: 'https://graph.qq.com/oauth2.0/authorize', cookie: jar.header() },
|
||||
form: {
|
||||
response_type: 'code',
|
||||
client_id: '100497308',
|
||||
redirect_uri: 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/',
|
||||
scope: 'get_user_info,get_app_friends',
|
||||
state: 'state',
|
||||
switch: '',
|
||||
from_ptlogin: '1',
|
||||
src: '1',
|
||||
update_auth: '1',
|
||||
openapi: '1010_1030',
|
||||
g_tk: String(getGtk(pSkey)),
|
||||
auth_time: String(Date.now()),
|
||||
ui: randomGuid(),
|
||||
},
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
jar.merge(authorize.headers['set-cookie'])
|
||||
const code = String(authorize.headers.location ?? '').match(/[?&]code=([^&]+)/)?.[1]
|
||||
if (!code) throw new Error('OAuth 授权失败(未取得 code)')
|
||||
|
||||
// 4. QQLogin 换凭据:响应体 req.data 携带 musickey/musicid(及 openid 等续期字段),
|
||||
// 同时响应 Set-Cookie 也会下发 y.qq.com 登录 cookie
|
||||
const login = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {
|
||||
method: 'post',
|
||||
headers: { ...HEADERS, cookie: jar.header() },
|
||||
body: {
|
||||
comm: { g_tk: getGtk(pSkey), platform: 'yqq', ct: 24, cv: 0 },
|
||||
req: { module: 'QQConnectLogin.LoginServer', method: 'QQLogin', param: { code } },
|
||||
},
|
||||
timeout: 15000,
|
||||
}).promise
|
||||
jar.merge(login.headers['set-cookie'])
|
||||
|
||||
const data = login.body?.req?.data ?? {}
|
||||
const musickey = data.musickey || jar.get('qm_keyst')
|
||||
if (!musickey) throw new Error('登录失败(未取得 musickey)')
|
||||
// ptlogin 域的 uin 可能带 o 前缀(如 o0123456),统一剥掉
|
||||
const uin = String(data.musicid || jar.get('uin')).replace(/^o0*/, '') || '0'
|
||||
|
||||
// 最终 cookie:uin/qm_keyst 用凭据值放最前,其余链路 cookie 原样保留(排除 ptlogin 域噪声)
|
||||
const pairs = [`uin=${uin}`, `qm_keyst=${musickey}`]
|
||||
if (data.encryptUin) pairs.push(`euin=${data.encryptUin}`)
|
||||
for (const [name, pair] of jar.entries()) {
|
||||
if (!['qrsig', 'ptui_loginuin', 'uin', 'qm_keyst', 'euin'].includes(name)) pairs.push(pair)
|
||||
}
|
||||
|
||||
setCredentials('tx', {
|
||||
cookie: pairs.join('; '),
|
||||
uin,
|
||||
...(data.openid ? { openId: String(data.openid) } : {}),
|
||||
...(data.access_token ? { accessToken: String(data.access_token) } : {}),
|
||||
...(data.refresh_token ? { refreshToken: String(data.refresh_token) } : {}),
|
||||
...(data.refresh_key ? { refreshKey: String(data.refresh_key) } : {}),
|
||||
})
|
||||
|
||||
const vipType = await checkVip().catch(() => null)
|
||||
return { code: 803, message: '登录成功', uin, ...(vipType ? { vipType } : {}) }
|
||||
}
|
||||
Reference in new issue
Block a user