qqmusic-api-enhanced 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/interface.d.ts +25 -0
- package/main.js +5 -3
- package/main.ts +18 -2
- package/module/login_qr_check.d.ts +1 -0
- package/module/login_qr_check.js +25 -14
- package/module/login_qr_check.ts +31 -9
- package/module/login_qr_create.d.ts +1 -0
- package/module/login_qr_create.js +8 -1
- package/module/login_qr_create.ts +9 -2
- package/module/login_refresh.d.ts +29 -11
- package/module/login_refresh.js +105 -41
- package/module/login_refresh.ts +143 -55
- package/module/login_status.d.ts +4 -0
- package/module/login_status.js +24 -27
- package/module/login_status.ts +34 -24
- package/module/search.js +29 -1
- package/module/search.ts +22 -1
- package/module/song_url.js +4 -2
- package/module/song_url.ts +4 -2
- package/package.json +9 -1
- package/util/cookie.d.ts +14 -0
- package/util/cookie.js +72 -0
- package/util/cookie.ts +87 -0
- package/util/crypto.d.ts +8 -0
- package/util/crypto.js +11 -0
- package/util/crypto.ts +12 -0
- package/util/des.ts +336 -336
- package/util/mobile_qr_session.d.ts +1 -0
- package/util/mobile_qr_session.js +6 -1
- package/util/mobile_qr_session.ts +7 -1
- package/util/option.js +7 -2
- package/util/option.ts +8 -2
package/README.md
CHANGED
|
@@ -122,6 +122,8 @@ import {
|
|
|
122
122
|
hash33, // 经典 hash33 哈希计算
|
|
123
123
|
getGtk, // 从 skey 计算 g_tk
|
|
124
124
|
getGuid, // 生成设备与会话 32 位 GUID
|
|
125
|
+
getStableGuid, // 由账号/二维码会话派生稳定设备 GUID
|
|
126
|
+
mergeLoginPayloadCookies, // 合并登录/续期响应中的凭证字段
|
|
125
127
|
request, // 统一网络调度器 (自动判断 cgi 或 raw http)
|
|
126
128
|
requestCgi, // 请求 musicu.fcg / musics.fcg
|
|
127
129
|
requestRaw, // 原始 fetch 包装
|
|
@@ -130,6 +132,24 @@ import {
|
|
|
130
132
|
|
|
131
133
|
---
|
|
132
134
|
|
|
135
|
+
## 发布
|
|
136
|
+
|
|
137
|
+
版本号在 `package.json`;发布由 GitHub Actions 自动完成:
|
|
138
|
+
|
|
139
|
+
1. 修改 `package.json` 的 `version`,提交并推送。
|
|
140
|
+
2. 打标签并推送:`git tag v1.0.7 && git push origin v1.0.7`。
|
|
141
|
+
3. `.github/workflows/publish.yml` 会安装依赖、构建、测试,并通过 npm Trusted Publishing(OIDC)执行 `npm publish --provenance`;不需要 `NPM_TOKEN`,也不需要 OTP。
|
|
142
|
+
|
|
143
|
+
首次配置(npm 控制台,一次性):
|
|
144
|
+
|
|
145
|
+
- 打开 npmjs.com 上 `qqmusic-api-enhanced` 的 Settings → Trusted Publisher。
|
|
146
|
+
- 选择 GitHub Actions,填写仓库 `owner/repo`、workflow 文件名 `publish.yml`、Environment 留空。
|
|
147
|
+
- workflow 文件名必须与 npm 侧逐字一致;如果 npm 侧填了 Environment,要同步把它加到 workflow 的 job 上。
|
|
148
|
+
|
|
149
|
+
也可以在 Actions 页面手动 `workflow_dispatch` 重跑;tag 触发时会先校验 tag 与 `package.json` 版本一致,不一致直接失败,避免发错版本。
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
133
153
|
## 协议与免责声明
|
|
134
154
|
|
|
135
155
|
本项目仅供个人学习、技术研究与教育用途。所有接口数据与媒体资源版权归腾讯公司所有。请勿将本项目用于任何商业用途或违反相关法律法规的场景。
|
package/interface.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface LoginQrCreateResult {
|
|
|
39
39
|
qrurl?: string;
|
|
40
40
|
qrimg?: string;
|
|
41
41
|
url?: string;
|
|
42
|
+
/** 本次登录的设备标识;登录成功后会写入 Cookie,后续续期沿用。 */
|
|
43
|
+
guid?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface LoginQrCheckQuery extends RequestBaseConfig {
|
|
@@ -54,6 +56,8 @@ export interface LoginQrCheckQuery extends RequestBaseConfig {
|
|
|
54
56
|
qrcodeID?: string;
|
|
55
57
|
/** 兼容通用 key 参数 */
|
|
56
58
|
key?: string;
|
|
59
|
+
/** 设备标识;缺省由二维码会话派生,登录成功后写入 Cookie。 */
|
|
60
|
+
guid?: string;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
export interface LoginQrCheckResult {
|
|
@@ -61,6 +65,9 @@ export interface LoginQrCheckResult {
|
|
|
61
65
|
message: string;
|
|
62
66
|
uin?: string;
|
|
63
67
|
musickey?: string;
|
|
68
|
+
guid?: string;
|
|
69
|
+
/** 新凭证过期时刻(毫秒),上游给出时才有。 */
|
|
70
|
+
expiresAt?: number;
|
|
64
71
|
cookie?: Record<string, string>;
|
|
65
72
|
cookieStr?: string;
|
|
66
73
|
data?: Record<string, any>;
|
|
@@ -95,6 +102,10 @@ export interface LoginStatusResult {
|
|
|
95
102
|
avatarUrl?: string;
|
|
96
103
|
vip: NormalizedVipInfo;
|
|
97
104
|
account: NormalizedLoginAccount;
|
|
105
|
+
/** 校验过程中轮换后的完整 Cookie;调用方应回写。 */
|
|
106
|
+
cookieStr?: string;
|
|
107
|
+
/** 新凭证过期时刻(毫秒),上游给出时才有。 */
|
|
108
|
+
expiresAt?: number;
|
|
98
109
|
data?: Record<string, any>;
|
|
99
110
|
userInfo?: Record<string, any>;
|
|
100
111
|
}
|
|
@@ -103,12 +114,17 @@ export interface LoginRefreshQuery extends RequestBaseConfig {
|
|
|
103
114
|
openid?: string;
|
|
104
115
|
refresh_token?: string;
|
|
105
116
|
refresh_key?: string;
|
|
117
|
+
/** 设备标识;缺省从 Cookie/账号派生。 */
|
|
118
|
+
guid?: string;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
export interface LoginRefreshResult {
|
|
109
122
|
code: number;
|
|
110
123
|
musickey?: string;
|
|
111
124
|
cookieStr?: string;
|
|
125
|
+
/** 新凭证过期时刻(毫秒),上游给出时才有。 */
|
|
126
|
+
expiresAt?: number;
|
|
127
|
+
message?: string;
|
|
112
128
|
data?: Record<string, any>;
|
|
113
129
|
raw?: Record<string, any>;
|
|
114
130
|
}
|
|
@@ -341,9 +357,16 @@ export function qrcDecrypt(hexData: string): string | null;
|
|
|
341
357
|
export function hash33(str: string): number;
|
|
342
358
|
export function getGtk(skey: string): number;
|
|
343
359
|
export function getGuid(): string;
|
|
360
|
+
/** 由稳定种子派生 32 位设备 guid;同一账号/登录会话内保持不变。 */
|
|
361
|
+
export function getStableGuid(seed: string): string;
|
|
344
362
|
export function getSearchId(): string;
|
|
345
363
|
export function cookieToJson(cookie: string | Record<string, string> | undefined | null): Record<string, string>;
|
|
346
364
|
export function cookieToString(cookieObj: Record<string, string> | undefined | null): string;
|
|
365
|
+
/** 把登录/续期响应里的凭证字段(含 refresh_token/refresh_key/openid)并入 Cookie。 */
|
|
366
|
+
export function mergeLoginPayloadCookies(
|
|
367
|
+
cookieObj: Record<string, string>,
|
|
368
|
+
payload: unknown
|
|
369
|
+
): { obtainedMusickey: boolean; expiresAt?: number };
|
|
347
370
|
export function parseSetCookie(setCookieHeaders?: string[]): Record<string, string>;
|
|
348
371
|
|
|
349
372
|
declare const api: {
|
|
@@ -372,9 +395,11 @@ declare const api: {
|
|
|
372
395
|
hash33: typeof hash33;
|
|
373
396
|
getGtk: typeof getGtk;
|
|
374
397
|
getGuid: typeof getGuid;
|
|
398
|
+
getStableGuid: typeof getStableGuid;
|
|
375
399
|
getSearchId: typeof getSearchId;
|
|
376
400
|
cookieToJson: typeof cookieToJson;
|
|
377
401
|
cookieToString: typeof cookieToString;
|
|
402
|
+
mergeLoginPayloadCookies: typeof mergeLoginPayloadCookies;
|
|
378
403
|
parseSetCookie: typeof parseSetCookie;
|
|
379
404
|
};
|
|
380
405
|
|
package/main.js
CHANGED
|
@@ -14,8 +14,8 @@ import song_url_fn from "./module/song_url.js";
|
|
|
14
14
|
import user_detail_fn from "./module/user_detail.js";
|
|
15
15
|
import user_playlist_fn from "./module/user_playlist.js";
|
|
16
16
|
import user_vip_info_fn, { deriveVipInfo, parseVipFromIcons, } from "./module/user_vip_info.js";
|
|
17
|
-
import { cookieToJson, cookieToString, parseSetCookie } from "./util/cookie.js";
|
|
18
|
-
import { getGtk, getGuid, getSearchId, hash33, qrcDecrypt } from "./util/crypto.js";
|
|
17
|
+
import { cookieToJson, cookieToString, mergeLoginPayloadCookies, parseSetCookie, } from "./util/cookie.js";
|
|
18
|
+
import { getGtk, getGuid, getSearchId, getStableGuid, hash33, qrcDecrypt, } from "./util/crypto.js";
|
|
19
19
|
import { request, requestCgi, requestRaw } from "./util/request.js";
|
|
20
20
|
import { zzcSign } from "./util/sign.js";
|
|
21
21
|
export const artist_detail = (query = {}) => artist_detail_fn(query, request);
|
|
@@ -34,7 +34,7 @@ export const song_url = (query = {}) => song_url_fn(query, request);
|
|
|
34
34
|
export const user_detail = (query = {}) => user_detail_fn(query, request);
|
|
35
35
|
export const user_playlist = (query = {}) => user_playlist_fn(query, request);
|
|
36
36
|
export const user_vip_info = (query = {}) => user_vip_info_fn(query, request);
|
|
37
|
-
export { request, requestCgi, requestRaw, zzcSign, qrcDecrypt, hash33, getGtk, getGuid, getSearchId, cookieToJson, cookieToString, parseSetCookie, deriveVipInfo, parseVipFromIcons, };
|
|
37
|
+
export { request, requestCgi, requestRaw, zzcSign, qrcDecrypt, hash33, getGtk, getGuid, getStableGuid, getSearchId, cookieToJson, cookieToString, mergeLoginPayloadCookies, parseSetCookie, deriveVipInfo, parseVipFromIcons, };
|
|
38
38
|
const api = {
|
|
39
39
|
artist_detail,
|
|
40
40
|
artist_songs,
|
|
@@ -62,9 +62,11 @@ const api = {
|
|
|
62
62
|
hash33,
|
|
63
63
|
getGtk,
|
|
64
64
|
getGuid,
|
|
65
|
+
getStableGuid,
|
|
65
66
|
getSearchId,
|
|
66
67
|
cookieToJson,
|
|
67
68
|
cookieToString,
|
|
69
|
+
mergeLoginPayloadCookies,
|
|
68
70
|
parseSetCookie,
|
|
69
71
|
};
|
|
70
72
|
export default api;
|
package/main.ts
CHANGED
|
@@ -22,8 +22,20 @@ import user_vip_info_fn, {
|
|
|
22
22
|
type NormalizedVipInfo,
|
|
23
23
|
} from "./module/user_vip_info.js";
|
|
24
24
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
25
|
+
import {
|
|
26
|
+
cookieToJson,
|
|
27
|
+
cookieToString,
|
|
28
|
+
mergeLoginPayloadCookies,
|
|
29
|
+
parseSetCookie,
|
|
30
|
+
} from "./util/cookie.js";
|
|
31
|
+
import {
|
|
32
|
+
getGtk,
|
|
33
|
+
getGuid,
|
|
34
|
+
getSearchId,
|
|
35
|
+
getStableGuid,
|
|
36
|
+
hash33,
|
|
37
|
+
qrcDecrypt,
|
|
38
|
+
} from "./util/crypto.js";
|
|
27
39
|
import { request, requestCgi, requestRaw, type ApiResponse } from "./util/request.js";
|
|
28
40
|
import { zzcSign } from "./util/sign.js";
|
|
29
41
|
|
|
@@ -84,9 +96,11 @@ export {
|
|
|
84
96
|
hash33,
|
|
85
97
|
getGtk,
|
|
86
98
|
getGuid,
|
|
99
|
+
getStableGuid,
|
|
87
100
|
getSearchId,
|
|
88
101
|
cookieToJson,
|
|
89
102
|
cookieToString,
|
|
103
|
+
mergeLoginPayloadCookies,
|
|
90
104
|
parseSetCookie,
|
|
91
105
|
deriveVipInfo,
|
|
92
106
|
parseVipFromIcons,
|
|
@@ -123,9 +137,11 @@ const api = {
|
|
|
123
137
|
hash33,
|
|
124
138
|
getGtk,
|
|
125
139
|
getGuid,
|
|
140
|
+
getStableGuid,
|
|
126
141
|
getSearchId,
|
|
127
142
|
cookieToJson,
|
|
128
143
|
cookieToString,
|
|
144
|
+
mergeLoginPayloadCookies,
|
|
129
145
|
parseSetCookie,
|
|
130
146
|
};
|
|
131
147
|
|
package/module/login_qr_check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { request, requestCgi } from "../util/request.js";
|
|
2
|
-
import { getGtk,
|
|
3
|
-
import { cookieToString, parseSetCookie } from "../util/cookie.js";
|
|
2
|
+
import { getGtk, getStableGuid } from "../util/crypto.js";
|
|
3
|
+
import { cookieToJson, cookieToString, mergeLoginPayloadCookies, parseSetCookie, } from "../util/cookie.js";
|
|
4
4
|
import { getMobileSession, registerMobileSession } from "../util/mobile_qr_session.js";
|
|
5
5
|
/**
|
|
6
6
|
* 轮询检查二维码登录状态.
|
|
@@ -25,6 +25,7 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
25
25
|
`&ptqrtoken=${ptqrtoken}&ptredirect=0&h=1&t=1&g=1&from_ui=1&ptlang=2052` +
|
|
26
26
|
`&action=0-0-${Date.now()}&js_ver=20102616&js_type=1&pt_uistyle=40` +
|
|
27
27
|
`&aid=716027609&daid=383&pt_3rd_aid=100497308&has_onekey=1`;
|
|
28
|
+
const guid = query.guid || getStableGuid(qrsig);
|
|
28
29
|
const checkRes = await fetch(checkUrl, {
|
|
29
30
|
headers: {
|
|
30
31
|
Referer: "https://xui.ptlogin2.qq.com/",
|
|
@@ -97,7 +98,7 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
97
98
|
authorizeFormData.append("openapi", "1010_1030");
|
|
98
99
|
authorizeFormData.append("g_tk", String(gtk));
|
|
99
100
|
authorizeFormData.append("auth_time", String(Date.now()));
|
|
100
|
-
authorizeFormData.append("ui",
|
|
101
|
+
authorizeFormData.append("ui", guid);
|
|
101
102
|
const authRes = await fetch("https://graph.qq.com/oauth2.0/authorize", {
|
|
102
103
|
method: "POST",
|
|
103
104
|
headers: {
|
|
@@ -125,6 +126,10 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
125
126
|
});
|
|
126
127
|
const resultCookie = loginCgiRes.cookie || [];
|
|
127
128
|
const finalCookies = { ...allCookies, ...parseSetCookie(resultCookie) };
|
|
129
|
+
// 登录响应里的 refresh_token / refresh_key / openid 是后续续期的关键,
|
|
130
|
+
// 旧实现只取 musickey,导致 key 过期后无法恢复。
|
|
131
|
+
const merged = mergeLoginPayloadCookies(finalCookies, loginCgiRes.body);
|
|
132
|
+
finalCookies.guid = guid;
|
|
128
133
|
const uin = finalCookies.uin || finalCookies.qqmusic_uin || "";
|
|
129
134
|
const musickey = finalCookies.qm_keyst || finalCookies.qqmusic_key || "";
|
|
130
135
|
return {
|
|
@@ -134,6 +139,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
134
139
|
message: "登录成功",
|
|
135
140
|
uin,
|
|
136
141
|
musickey,
|
|
142
|
+
guid,
|
|
143
|
+
expiresAt: merged.expiresAt,
|
|
137
144
|
data: loginCgiRes.body,
|
|
138
145
|
cookieStr: cookieToString(finalCookies),
|
|
139
146
|
},
|
|
@@ -148,6 +155,7 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
148
155
|
body: { code: 400, message: "缺少必要参数 qrcodeID" },
|
|
149
156
|
};
|
|
150
157
|
}
|
|
158
|
+
const guid = query.guid || getStableGuid(qrcodeID);
|
|
151
159
|
// 1. 确保后台 MQTT 会话已启动
|
|
152
160
|
let session = getMobileSession(qrcodeID);
|
|
153
161
|
if (!session) {
|
|
@@ -159,6 +167,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
159
167
|
}
|
|
160
168
|
// 2. 优先检查实时会话事件
|
|
161
169
|
if (session && session.code === 200) {
|
|
170
|
+
const cookieObj = cookieToJson(session.cookieStr);
|
|
171
|
+
cookieObj.guid = cookieObj.guid || guid;
|
|
162
172
|
return {
|
|
163
173
|
status: 200,
|
|
164
174
|
body: {
|
|
@@ -168,7 +178,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
168
178
|
musickey: session.musickey || "",
|
|
169
179
|
nickname: session.nickname || "",
|
|
170
180
|
avatarUrl: session.avatarUrl || "",
|
|
171
|
-
|
|
181
|
+
guid,
|
|
182
|
+
cookieStr: cookieToString(cookieObj),
|
|
172
183
|
},
|
|
173
184
|
};
|
|
174
185
|
}
|
|
@@ -215,16 +226,9 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
215
226
|
if (code === 0) {
|
|
216
227
|
const cookies = cgiRes.cookie || [];
|
|
217
228
|
const cookieObj = parseSetCookie(cookies);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
cookieObj.qm_keyst = String(cgiBody.musickey);
|
|
222
|
-
if (cgiBody.musickey && !cookieObj.qqmusic_key)
|
|
223
|
-
cookieObj.qqmusic_key = String(cgiBody.musickey);
|
|
224
|
-
if (cgiBody.openid && !cookieObj.openid)
|
|
225
|
-
cookieObj.openid = String(cgiBody.openid);
|
|
226
|
-
if (cgiBody.refresh_token && !cookieObj.refresh_token)
|
|
227
|
-
cookieObj.refresh_token = String(cgiBody.refresh_token);
|
|
229
|
+
// 与 QQ 网页扫码同一套合并逻辑:refresh_key/openid/access_token/过期时间都要留下。
|
|
230
|
+
const merged = mergeLoginPayloadCookies(cookieObj, cgiBody);
|
|
231
|
+
cookieObj.guid = guid;
|
|
228
232
|
const cookieStr = cookieToString(cookieObj);
|
|
229
233
|
return {
|
|
230
234
|
status: 200,
|
|
@@ -233,6 +237,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
233
237
|
message: "登录成功",
|
|
234
238
|
uin: cookieObj.uin || String(cgiBody.musicid || ""),
|
|
235
239
|
musickey: cookieObj.qm_keyst || String(cgiBody.musickey || ""),
|
|
240
|
+
guid,
|
|
241
|
+
expiresAt: merged.expiresAt,
|
|
236
242
|
cookieStr,
|
|
237
243
|
data: cgiBody,
|
|
238
244
|
},
|
|
@@ -257,6 +263,7 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
257
263
|
body: { code: 400, message: "缺少必要参数 uuid" },
|
|
258
264
|
};
|
|
259
265
|
}
|
|
266
|
+
const guid = query.guid || getStableGuid(uuid);
|
|
260
267
|
const pollUrl = `https://lp.open.weixin.qq.com/connect/l/qrconnect?uuid=${uuid}&_=${Date.now()}`;
|
|
261
268
|
const wxRes = await fetch(pollUrl, {
|
|
262
269
|
headers: {
|
|
@@ -295,6 +302,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
295
302
|
});
|
|
296
303
|
const cookies = wxLoginRes.cookie || [];
|
|
297
304
|
const cookieObj = parseSetCookie(cookies);
|
|
305
|
+
const merged = mergeLoginPayloadCookies(cookieObj, wxLoginRes.body);
|
|
306
|
+
cookieObj.guid = guid;
|
|
298
307
|
return {
|
|
299
308
|
status: 200,
|
|
300
309
|
body: {
|
|
@@ -302,6 +311,8 @@ export default async function login_qr_check(query = {}, req = request) {
|
|
|
302
311
|
message: "微信登录成功",
|
|
303
312
|
uin: cookieObj.wxuin || cookieObj.uin || "",
|
|
304
313
|
musickey: cookieObj.qm_keyst || cookieObj.musickey || "",
|
|
314
|
+
guid,
|
|
315
|
+
expiresAt: merged.expiresAt,
|
|
305
316
|
data: wxLoginRes.body,
|
|
306
317
|
cookieStr: cookieToString(cookieObj),
|
|
307
318
|
},
|
package/module/login_qr_check.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { request, requestCgi, type ApiResponse } from "../util/request.js";
|
|
2
|
-
import { getGtk,
|
|
3
|
-
import {
|
|
2
|
+
import { getGtk, getStableGuid } from "../util/crypto.js";
|
|
3
|
+
import {
|
|
4
|
+
cookieToJson,
|
|
5
|
+
cookieToString,
|
|
6
|
+
mergeLoginPayloadCookies,
|
|
7
|
+
parseSetCookie,
|
|
8
|
+
} from "../util/cookie.js";
|
|
4
9
|
import { getMobileSession, registerMobileSession } from "../util/mobile_qr_session.js";
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -20,6 +25,7 @@ export default async function login_qr_check(
|
|
|
20
25
|
uuid?: string;
|
|
21
26
|
qrcodeID?: string;
|
|
22
27
|
key?: string;
|
|
28
|
+
guid?: string;
|
|
23
29
|
} = {},
|
|
24
30
|
req = request
|
|
25
31
|
): Promise<ApiResponse> {
|
|
@@ -40,6 +46,7 @@ export default async function login_qr_check(
|
|
|
40
46
|
`&action=0-0-${Date.now()}&js_ver=20102616&js_type=1&pt_uistyle=40` +
|
|
41
47
|
`&aid=716027609&daid=383&pt_3rd_aid=100497308&has_onekey=1`;
|
|
42
48
|
|
|
49
|
+
const guid = query.guid || getStableGuid(qrsig);
|
|
43
50
|
const checkRes = await fetch(checkUrl, {
|
|
44
51
|
headers: {
|
|
45
52
|
Referer: "https://xui.ptlogin2.qq.com/",
|
|
@@ -127,7 +134,7 @@ export default async function login_qr_check(
|
|
|
127
134
|
authorizeFormData.append("openapi", "1010_1030");
|
|
128
135
|
authorizeFormData.append("g_tk", String(gtk));
|
|
129
136
|
authorizeFormData.append("auth_time", String(Date.now()));
|
|
130
|
-
authorizeFormData.append("ui",
|
|
137
|
+
authorizeFormData.append("ui", guid);
|
|
131
138
|
|
|
132
139
|
const authRes = await fetch("https://graph.qq.com/oauth2.0/authorize", {
|
|
133
140
|
method: "POST",
|
|
@@ -165,6 +172,10 @@ export default async function login_qr_check(
|
|
|
165
172
|
|
|
166
173
|
const resultCookie = loginCgiRes.cookie || [];
|
|
167
174
|
const finalCookies = { ...allCookies, ...parseSetCookie(resultCookie) };
|
|
175
|
+
// 登录响应里的 refresh_token / refresh_key / openid 是后续续期的关键,
|
|
176
|
+
// 旧实现只取 musickey,导致 key 过期后无法恢复。
|
|
177
|
+
const merged = mergeLoginPayloadCookies(finalCookies, loginCgiRes.body);
|
|
178
|
+
finalCookies.guid = guid;
|
|
168
179
|
|
|
169
180
|
const uin = finalCookies.uin || finalCookies.qqmusic_uin || "";
|
|
170
181
|
const musickey = finalCookies.qm_keyst || finalCookies.qqmusic_key || "";
|
|
@@ -176,6 +187,8 @@ export default async function login_qr_check(
|
|
|
176
187
|
message: "登录成功",
|
|
177
188
|
uin,
|
|
178
189
|
musickey,
|
|
190
|
+
guid,
|
|
191
|
+
expiresAt: merged.expiresAt,
|
|
179
192
|
data: loginCgiRes.body,
|
|
180
193
|
cookieStr: cookieToString(finalCookies),
|
|
181
194
|
},
|
|
@@ -191,6 +204,7 @@ export default async function login_qr_check(
|
|
|
191
204
|
body: { code: 400, message: "缺少必要参数 qrcodeID" },
|
|
192
205
|
};
|
|
193
206
|
}
|
|
207
|
+
const guid = query.guid || getStableGuid(qrcodeID);
|
|
194
208
|
|
|
195
209
|
// 1. 确保后台 MQTT 会话已启动
|
|
196
210
|
let session = getMobileSession(qrcodeID);
|
|
@@ -203,6 +217,8 @@ export default async function login_qr_check(
|
|
|
203
217
|
|
|
204
218
|
// 2. 优先检查实时会话事件
|
|
205
219
|
if (session && session.code === 200) {
|
|
220
|
+
const cookieObj = cookieToJson(session.cookieStr);
|
|
221
|
+
cookieObj.guid = cookieObj.guid || guid;
|
|
206
222
|
return {
|
|
207
223
|
status: 200,
|
|
208
224
|
body: {
|
|
@@ -212,7 +228,8 @@ export default async function login_qr_check(
|
|
|
212
228
|
musickey: session.musickey || "",
|
|
213
229
|
nickname: session.nickname || "",
|
|
214
230
|
avatarUrl: session.avatarUrl || "",
|
|
215
|
-
|
|
231
|
+
guid,
|
|
232
|
+
cookieStr: cookieToString(cookieObj),
|
|
216
233
|
},
|
|
217
234
|
};
|
|
218
235
|
}
|
|
@@ -270,11 +287,9 @@ export default async function login_qr_check(
|
|
|
270
287
|
if (code === 0) {
|
|
271
288
|
const cookies = cgiRes.cookie || [];
|
|
272
289
|
const cookieObj = parseSetCookie(cookies);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (cgiBody.openid && !cookieObj.openid) cookieObj.openid = String(cgiBody.openid);
|
|
277
|
-
if (cgiBody.refresh_token && !cookieObj.refresh_token) cookieObj.refresh_token = String(cgiBody.refresh_token);
|
|
290
|
+
// 与 QQ 网页扫码同一套合并逻辑:refresh_key/openid/access_token/过期时间都要留下。
|
|
291
|
+
const merged = mergeLoginPayloadCookies(cookieObj, cgiBody);
|
|
292
|
+
cookieObj.guid = guid;
|
|
278
293
|
|
|
279
294
|
const cookieStr = cookieToString(cookieObj);
|
|
280
295
|
return {
|
|
@@ -284,6 +299,8 @@ export default async function login_qr_check(
|
|
|
284
299
|
message: "登录成功",
|
|
285
300
|
uin: cookieObj.uin || String(cgiBody.musicid || ""),
|
|
286
301
|
musickey: cookieObj.qm_keyst || String(cgiBody.musickey || ""),
|
|
302
|
+
guid,
|
|
303
|
+
expiresAt: merged.expiresAt,
|
|
287
304
|
cookieStr,
|
|
288
305
|
data: cgiBody,
|
|
289
306
|
},
|
|
@@ -309,6 +326,7 @@ export default async function login_qr_check(
|
|
|
309
326
|
body: { code: 400, message: "缺少必要参数 uuid" },
|
|
310
327
|
};
|
|
311
328
|
}
|
|
329
|
+
const guid = query.guid || getStableGuid(uuid);
|
|
312
330
|
|
|
313
331
|
const pollUrl = `https://lp.open.weixin.qq.com/connect/l/qrconnect?uuid=${uuid}&_=${Date.now()}`;
|
|
314
332
|
const wxRes = await fetch(pollUrl, {
|
|
@@ -359,6 +377,8 @@ export default async function login_qr_check(
|
|
|
359
377
|
|
|
360
378
|
const cookies = wxLoginRes.cookie || [];
|
|
361
379
|
const cookieObj = parseSetCookie(cookies);
|
|
380
|
+
const merged = mergeLoginPayloadCookies(cookieObj, wxLoginRes.body);
|
|
381
|
+
cookieObj.guid = guid;
|
|
362
382
|
|
|
363
383
|
return {
|
|
364
384
|
status: 200,
|
|
@@ -367,6 +387,8 @@ export default async function login_qr_check(
|
|
|
367
387
|
message: "微信登录成功",
|
|
368
388
|
uin: cookieObj.wxuin || cookieObj.uin || "",
|
|
369
389
|
musickey: cookieObj.qm_keyst || cookieObj.musickey || "",
|
|
390
|
+
guid,
|
|
391
|
+
expiresAt: merged.expiresAt,
|
|
370
392
|
data: wxLoginRes.body,
|
|
371
393
|
cookieStr: cookieToString(cookieObj),
|
|
372
394
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { request } from "../util/request.js";
|
|
2
|
-
import { hash33 } from "../util/crypto.js";
|
|
2
|
+
import { getStableGuid, hash33 } from "../util/crypto.js";
|
|
3
3
|
import { parseSetCookie } from "../util/cookie.js";
|
|
4
4
|
import { registerMobileSession } from "../util/mobile_qr_session.js";
|
|
5
5
|
/**
|
|
@@ -23,6 +23,8 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
23
23
|
const cookieObj = parseSetCookie(res.cookie);
|
|
24
24
|
const qrsig = cookieObj.qrsig || "";
|
|
25
25
|
const ptqrtoken = hash33(qrsig);
|
|
26
|
+
// 与 login_qr_check 用同一个种子派生,登录成功时写入 Cookie,保证设备标识稳定。
|
|
27
|
+
const guid = query.guid || getStableGuid(qrsig);
|
|
26
28
|
// 二维码图片转 base64
|
|
27
29
|
let qrimg = "";
|
|
28
30
|
if (Buffer.isBuffer(res.body)) {
|
|
@@ -46,6 +48,7 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
46
48
|
ptqrtoken,
|
|
47
49
|
qrimg,
|
|
48
50
|
url,
|
|
51
|
+
guid,
|
|
49
52
|
},
|
|
50
53
|
cookie: res.cookie,
|
|
51
54
|
};
|
|
@@ -64,6 +67,7 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
64
67
|
const qrcodeUrl = uuid
|
|
65
68
|
? `https://open.weixin.qq.com/connect/qrcode/${uuid}`
|
|
66
69
|
: "";
|
|
70
|
+
const guid = query.guid || (uuid ? getStableGuid(uuid) : "");
|
|
67
71
|
return {
|
|
68
72
|
status: 200,
|
|
69
73
|
body: {
|
|
@@ -71,6 +75,7 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
71
75
|
type: "wx",
|
|
72
76
|
uuid,
|
|
73
77
|
qrurl: qrcodeUrl,
|
|
78
|
+
guid,
|
|
74
79
|
},
|
|
75
80
|
cookie: res.cookie,
|
|
76
81
|
};
|
|
@@ -86,6 +91,7 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
86
91
|
}
|
|
87
92
|
catch { }
|
|
88
93
|
}
|
|
94
|
+
const guid = query.guid || (qrcodeID ? getStableGuid(qrcodeID) : "");
|
|
89
95
|
return {
|
|
90
96
|
status: cgiRes.status,
|
|
91
97
|
body: {
|
|
@@ -93,6 +99,7 @@ export default async function login_qr_create(query = {}, req = request) {
|
|
|
93
99
|
type: "mobile",
|
|
94
100
|
qrcode: cgiRes.body?.qrcode,
|
|
95
101
|
qrcodeID,
|
|
102
|
+
guid,
|
|
96
103
|
},
|
|
97
104
|
cookie: cgiRes.cookie,
|
|
98
105
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { request, type ApiResponse } from "../util/request.js";
|
|
2
|
-
import { hash33 } from "../util/crypto.js";
|
|
2
|
+
import { getStableGuid, hash33 } from "../util/crypto.js";
|
|
3
3
|
import { parseSetCookie } from "../util/cookie.js";
|
|
4
4
|
import { registerMobileSession } from "../util/mobile_qr_session.js";
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ import { registerMobileSession } from "../util/mobile_qr_session.js";
|
|
|
9
9
|
* @param query.type 登录类型: 'qq' (默认), 'wx' (微信), 'mobile' (QQ音乐App)
|
|
10
10
|
*/
|
|
11
11
|
export default async function login_qr_create(
|
|
12
|
-
query: { type?: "qq" | "wx" | "mobile"; cookie?: any } = {},
|
|
12
|
+
query: { type?: "qq" | "wx" | "mobile"; cookie?: any; guid?: string } = {},
|
|
13
13
|
req = request
|
|
14
14
|
): Promise<ApiResponse> {
|
|
15
15
|
const type = query.type || "qq";
|
|
@@ -32,6 +32,8 @@ export default async function login_qr_create(
|
|
|
32
32
|
const cookieObj = parseSetCookie(res.cookie);
|
|
33
33
|
const qrsig = cookieObj.qrsig || "";
|
|
34
34
|
const ptqrtoken = hash33(qrsig);
|
|
35
|
+
// 与 login_qr_check 用同一个种子派生,登录成功时写入 Cookie,保证设备标识稳定。
|
|
36
|
+
const guid = query.guid || getStableGuid(qrsig);
|
|
35
37
|
|
|
36
38
|
// 二维码图片转 base64
|
|
37
39
|
let qrimg = "";
|
|
@@ -54,6 +56,7 @@ export default async function login_qr_create(
|
|
|
54
56
|
ptqrtoken,
|
|
55
57
|
qrimg,
|
|
56
58
|
url,
|
|
59
|
+
guid,
|
|
57
60
|
},
|
|
58
61
|
cookie: res.cookie,
|
|
59
62
|
};
|
|
@@ -77,6 +80,7 @@ export default async function login_qr_create(
|
|
|
77
80
|
const qrcodeUrl = uuid
|
|
78
81
|
? `https://open.weixin.qq.com/connect/qrcode/${uuid}`
|
|
79
82
|
: "";
|
|
83
|
+
const guid = query.guid || (uuid ? getStableGuid(uuid) : "");
|
|
80
84
|
|
|
81
85
|
return {
|
|
82
86
|
status: 200,
|
|
@@ -85,6 +89,7 @@ export default async function login_qr_create(
|
|
|
85
89
|
type: "wx",
|
|
86
90
|
uuid,
|
|
87
91
|
qrurl: qrcodeUrl,
|
|
92
|
+
guid,
|
|
88
93
|
},
|
|
89
94
|
cookie: res.cookie,
|
|
90
95
|
};
|
|
@@ -106,6 +111,7 @@ export default async function login_qr_create(
|
|
|
106
111
|
registerMobileSession(qrcodeID);
|
|
107
112
|
} catch {}
|
|
108
113
|
}
|
|
114
|
+
const guid = query.guid || (qrcodeID ? getStableGuid(qrcodeID) : "");
|
|
109
115
|
|
|
110
116
|
return {
|
|
111
117
|
status: cgiRes.status,
|
|
@@ -114,6 +120,7 @@ export default async function login_qr_create(
|
|
|
114
120
|
type: "mobile",
|
|
115
121
|
qrcode: cgiRes.body?.qrcode,
|
|
116
122
|
qrcodeID,
|
|
123
|
+
guid,
|
|
117
124
|
},
|
|
118
125
|
cookie: cgiRes.cookie,
|
|
119
126
|
};
|
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import { request, type ApiResponse } from "../util/request.js";
|
|
2
|
-
|
|
3
|
-
* 刷新登录凭证.
|
|
4
|
-
*
|
|
5
|
-
* @param query.cookie 登录 Cookie
|
|
6
|
-
* @param query.openid 微信/QQ openid (可选)
|
|
7
|
-
* @param query.refresh_token 刷新令牌 (可选)
|
|
8
|
-
*/
|
|
9
|
-
export default function login_refresh(query?: {
|
|
2
|
+
export interface LoginRefreshQuery {
|
|
10
3
|
cookie?: any;
|
|
11
4
|
openid?: string;
|
|
12
5
|
refresh_token?: string;
|
|
13
6
|
refresh_key?: string;
|
|
14
|
-
|
|
7
|
+
guid?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LoginRefreshBody {
|
|
15
10
|
code: number;
|
|
16
|
-
|
|
11
|
+
musickey?: string;
|
|
17
12
|
cookieStr: string;
|
|
18
|
-
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
data?: any;
|
|
15
|
+
message?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 刷新登录凭证。
|
|
19
|
+
*
|
|
20
|
+
* QQ 音乐不同登录入口的续期接口并不一样:
|
|
21
|
+
* - 手机 QQ / QQ 互联网页扫码拿到的是 `uin + qm_keyst`,续期走
|
|
22
|
+
* `QQConnectLogin.LoginServer/QQLogin`(带 `expired_in` 与旧 `musickey`),
|
|
23
|
+
* 这是上游长期使用的稳定路径;
|
|
24
|
+
* - QQ 音乐 App / 微信等移动端登录会额外拿到 `refresh_token` / `refresh_key`,
|
|
25
|
+
* 续期走 `music.login.LoginServer/Login` 的 refresh-token 流程。
|
|
26
|
+
*
|
|
27
|
+
* 旧实现无条件走后者,网页扫码的 Cookie 没有 refresh_token,续期会失败,
|
|
28
|
+
* 账号随后被判失效。这里先走 QQLogin,再回退到 refresh-token 流程。
|
|
29
|
+
*
|
|
30
|
+
* @param query.cookie 登录 Cookie
|
|
31
|
+
* @param query.openid 微信/QQ openid (可选)
|
|
32
|
+
* @param query.refresh_token 刷新令牌 (可选)
|
|
33
|
+
* @param query.refresh_key 刷新密钥 (可选)
|
|
34
|
+
* @param query.guid 设备标识;缺省从 Cookie/账号派生,保持稳定
|
|
35
|
+
*/
|
|
36
|
+
export default function login_refresh(query?: LoginRefreshQuery, req?: typeof request): Promise<ApiResponse<LoginRefreshBody>>;
|