qqmusic-api-enhanced 1.0.4 → 1.0.6

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/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 { cookieToJson, cookieToString, parseSetCookie } from "./util/cookie.js";
26
- import { getGtk, getGuid, getSearchId, hash33, qrcDecrypt } from "./util/crypto.js";
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
 
@@ -15,4 +15,5 @@ export default function login_qr_check(query?: {
15
15
  uuid?: string;
16
16
  qrcodeID?: string;
17
17
  key?: string;
18
+ guid?: string;
18
19
  }, req?: typeof request): Promise<ApiResponse>;
@@ -1,6 +1,6 @@
1
1
  import { request, requestCgi } from "../util/request.js";
2
- import { getGtk, getGuid } from "../util/crypto.js";
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", getGuid());
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
- cookieStr: session.cookieStr || "",
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
- if (cgiBody.musicid && !cookieObj.uin)
219
- cookieObj.uin = String(cgiBody.musicid);
220
- if (cgiBody.musickey && !cookieObj.qm_keyst)
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
  },
@@ -1,6 +1,11 @@
1
1
  import { request, requestCgi, type ApiResponse } from "../util/request.js";
2
- import { getGtk, getGuid } from "../util/crypto.js";
3
- import { cookieToString, parseSetCookie } from "../util/cookie.js";
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", getGuid());
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
- cookieStr: session.cookieStr || "",
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
- if (cgiBody.musicid && !cookieObj.uin) cookieObj.uin = String(cgiBody.musicid);
274
- if (cgiBody.musickey && !cookieObj.qm_keyst) cookieObj.qm_keyst = String(cgiBody.musickey);
275
- if (cgiBody.musickey && !cookieObj.qqmusic_key) cookieObj.qqmusic_key = String(cgiBody.musickey);
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
  },
@@ -7,4 +7,5 @@ import { request, type ApiResponse } from "../util/request.js";
7
7
  export default function login_qr_create(query?: {
8
8
  type?: "qq" | "wx" | "mobile";
9
9
  cookie?: any;
10
+ guid?: string;
10
11
  }, req?: typeof request): Promise<ApiResponse>;
@@ -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
- }, req?: typeof request): Promise<ApiResponse<{
7
+ guid?: string;
8
+ }
9
+ export interface LoginRefreshBody {
15
10
  code: number;
16
- data: any;
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>>;