From e7989befb7e29e0ae44ffb08702b1ef9100384c2 Mon Sep 17 00:00:00 2001 From: Holden Cross <46531473+Holden-Cross@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:56:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Windows=20?= =?UTF-8?q?=E7=BD=91=E6=98=93=E4=BA=91=E6=89=AB=E7=A0=81=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/apis/netease/index.ts | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/electron/main/apis/netease/index.ts b/electron/main/apis/netease/index.ts index bec4c8651..5b6df7616 100644 --- a/electron/main/apis/netease/index.ts +++ b/electron/main/apis/netease/index.ts @@ -11,6 +11,7 @@ * 4) 只在登录相关接口上把响应 set-cookie 写回 sessions;其它接口不落库 */ +import { hostname, userInfo } from "node:os"; import { clearSessionCookies, getSessionCookies, @@ -243,6 +244,7 @@ export const callNetease = async ( query.realIP = sessionRealIp(); } + const requestDeviceId = (query.cookie as Record).deviceId || getDeviceId(); const res = await fn(query, createRequest); // 仅登录态变更接口才把响应 cookie 写回 SQLite @@ -257,6 +259,34 @@ export const callNetease = async ( persistSession({ ...loadSession(), ...patch }); cacheClear(); } + + if ( + process.platform === "win32" && + name === "login_qr_check" && + Number(res.body.code) === 803 + ) { + // 使用本次登录的凭据快照,避免异步上报误用随后切换的账号。 + const cookie = { + ...(query.cookie as Record), + deviceId: requestDeviceId, + ...patch, + }; + void (async () => { + try { + const deviceName = `${userInfo().username} 的 ${hostname()}`; + const result = await createRequest( + "/api/deviceinfo/center/upload", + { deviceName }, + { crypto: "eapi", cookie, realIP: query.realIP, ip: query.ip }, + ); + if (result.status !== 200 || Number(result.body.code) !== 200) { + neteaseLog.warn("登录设备名称上报未成功"); + } + } catch { + neteaseLog.warn("登录设备名称上报失败"); + } + })(); + } } const value = { status: res.status, body: res.body }; From 6c71b0eebc77f99298fda355094c95923e884fdc Mon Sep 17 00:00:00 2001 From: imsyy Date: Sun, 18 Oct 2026 17:32:17 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/apis/netease/core/config.ts | 12 +++++++ electron/main/apis/netease/core/request.ts | 9 ++++-- electron/main/apis/netease/index.ts | 31 +------------------ .../modules/deviceinfo_center_upload.ts | 28 +++++++++++++++++ electron/main/apis/netease/modules/index.ts | 2 ++ src/apis/login/netease.ts | 16 +++++++++- src/components/modals/LoginDialog.vue | 3 +- 7 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 electron/main/apis/netease/modules/deviceinfo_center_upload.ts diff --git a/electron/main/apis/netease/core/config.ts b/electron/main/apis/netease/core/config.ts index 10cee918e..df2864bff 100644 --- a/electron/main/apis/netease/core/config.ts +++ b/electron/main/apis/netease/core/config.ts @@ -2,6 +2,8 @@ * Netease API 通用常量(密钥、域名、客户端伪装标识等) */ +import { release, type as osType } from "node:os"; + /** AES-CBC 初始向量 */ export const IV = "0102030405060708"; /** weapi 第一层 AES 预设密钥 */ @@ -75,6 +77,16 @@ export const OS_MAP = { }, } as const; +/** + * 宿主平台对应的客户端标识 + * 协议层不依赖 electron,直接读 process.platform + */ +export const HOST_OS_KEY: keyof typeof OS_MAP = + process.platform === "darwin" ? "osx" : process.platform === "linux" ? "linux" : "pc"; + +/** 宿主真实系统版本,作为未显式指定 os 时的 osver */ +export const HOST_OS_VERSION = `${osType()} ${release()}`; + /** 不同加密方式 + 设备类型下的 User-Agent */ export const UA_MAP = { weapi: { diff --git a/electron/main/apis/netease/core/request.ts b/electron/main/apis/netease/core/request.ts index 09d8972f7..3205d0d77 100644 --- a/electron/main/apis/netease/core/request.ts +++ b/electron/main/apis/netease/core/request.ts @@ -6,6 +6,8 @@ import { DOMAIN, EAPI_DOMAIN, ENCRYPT_RESPONSE, + HOST_OS_KEY, + HOST_OS_VERSION, OS_MAP, SPECIAL_STATUS_CODES, UA_MAP, @@ -81,7 +83,10 @@ const processCookieObject = ( crypto: CryptoMode | "", ): Record => { const ntesNuid = cookie._ntes_nuid || randomBytes(16).toString("hex"); - const os = OS_MAP[(cookie.os as keyof typeof OS_MAP) || "pc"] || OS_MAP.pc; + /** 未显式指定 os 时跟随宿主平台,系统版本取本机真实值 */ + const osKey = (cookie.os as keyof typeof OS_MAP) || HOST_OS_KEY; + const os = OS_MAP[osKey] || OS_MAP[HOST_OS_KEY]; + const osver = cookie.osver || (cookie.os ? os.osver : HOST_OS_VERSION); const processed: Record = { ...cookie, @@ -91,7 +96,7 @@ const processCookieObject = ( _ntes_nnid: cookie._ntes_nnid || `${ntesNuid},${Date.now()}`, WNMCID: cookie.WNMCID || WNMCID, WEVNSM: cookie.WEVNSM || "1.0.0", - osver: cookie.osver || os.osver, + osver, deviceId: cookie.deviceId || getDeviceId(), os: cookie.os || os.os, channel: cookie.channel || os.channel, diff --git a/electron/main/apis/netease/index.ts b/electron/main/apis/netease/index.ts index 5db31674b..a378c5541 100644 --- a/electron/main/apis/netease/index.ts +++ b/electron/main/apis/netease/index.ts @@ -1,6 +1,5 @@ /** 注入平台会话、路由业务接口并管理有界响应缓存 */ -import { hostname, userInfo } from "node:os"; import { clearSessionCookies, getSessionCookies, @@ -48,6 +47,7 @@ const NON_CACHEABLE: ReadonlySet = new Set([ "login_status", "logout", "register_anonimous", + "deviceinfo_center_upload", "song_url", "song_download_url", "scrobble", @@ -274,7 +274,6 @@ export const callNetease = async ( query.realIP = sessionRealIp(); } - const requestDeviceId = (query.cookie as Record).deviceId || getDeviceId(); const requestSessionVersion = sessionVersion; const res = await fn(query, createRequest); /** 旧会话的响应不能覆盖登出或切换账号后的凭据与缓存 */ @@ -294,34 +293,6 @@ export const callNetease = async ( persistSession({ ...loadSession(), ...patch }); cacheClear(); } - - if ( - process.platform === "win32" && - name === "login_qr_check" && - Number(res.body.code) === 803 - ) { - // 使用本次登录的凭据快照,避免异步上报误用随后切换的账号。 - const cookie = { - ...(query.cookie as Record), - deviceId: requestDeviceId, - ...patch, - }; - void (async () => { - try { - const deviceName = `${userInfo().username} 的 ${hostname()}`; - const result = await createRequest( - "/api/deviceinfo/center/upload", - { deviceName }, - { crypto: "eapi", cookie, realIP: query.realIP, ip: query.ip }, - ); - if (result.status !== 200 || Number(result.body.code) !== 200) { - neteaseLog.warn("登录设备名称上报未成功"); - } - } catch { - neteaseLog.warn("登录设备名称上报失败"); - } - })(); - } } const value = { status: res.status, body: res.body }; diff --git a/electron/main/apis/netease/modules/deviceinfo_center_upload.ts b/electron/main/apis/netease/modules/deviceinfo_center_upload.ts new file mode 100644 index 000000000..913f69027 --- /dev/null +++ b/electron/main/apis/netease/modules/deviceinfo_center_upload.ts @@ -0,0 +1,28 @@ +/** + * 上报登录设备名称 + * + * 官方客户端在登录成功后调用,用于「登录设备管理」展示自定义名称 + * + * params: + * - deviceName 设备名,缺省为「用户名 的 计算机名」 + * + * 响应:`{ code }`,成功为 200 + */ + +import { hostname, userInfo } from "node:os"; +import { createOption } from "../core/option"; +import type { NeteaseModule } from "../core/types"; + +/** 与官方客户端一致的设备名,取不到用户名时退化为计算机名 */ +const defaultDeviceName = (): string => { + const user = userInfo().username.trim(); + const host = hostname(); + return user ? `${user} 的 ${host}` : host; +}; + +const deviceinfoCenterUpload: NeteaseModule = async (query, request) => { + const deviceName = String(query.deviceName ?? "").trim() || defaultDeviceName(); + return request("/api/deviceinfo/center/upload", { deviceName }, createOption(query, "eapi")); +}; + +export default deviceinfoCenterUpload; diff --git a/electron/main/apis/netease/modules/index.ts b/electron/main/apis/netease/modules/index.ts index 69f66528f..4f6079c99 100644 --- a/electron/main/apis/netease/modules/index.ts +++ b/electron/main/apis/netease/modules/index.ts @@ -32,6 +32,7 @@ import user_playlist from "./user_playlist"; import user_record from "./user_record"; import record_recent_song from "./record_recent_song"; import user_subcount from "./user_subcount"; +import deviceinfo_center_upload from "./deviceinfo_center_upload"; import cloudsearch from "./cloudsearch"; import search from "./search"; @@ -123,6 +124,7 @@ export const modules: Record = { user_record, record_recent_song, user_subcount, + deviceinfo_center_upload, cloudsearch, search, diff --git a/src/apis/login/netease.ts b/src/apis/login/netease.ts index b92c83fb3..d7a8d2c08 100644 --- a/src/apis/login/netease.ts +++ b/src/apis/login/netease.ts @@ -3,7 +3,7 @@ */ import type { UserProfile } from "@/types/user"; -import { NeteaseApiError, netease as neteaseApi } from "@/apis/netease"; +import { NeteaseApiError, netease as neteaseApi, neteaseCall } from "@/apis/netease"; import type { QrLoginAdapter, QrLoginState } from "./platform"; interface LoginStatusBody { @@ -37,6 +37,18 @@ export interface QrCheckResult { avatarUrl?: string; } +/** + * 登录成功后上报设备名,避免「登录设备管理」里显示为 pc + * 名称由主进程按当前宿主平台生成;纯展示信息,失败不触发登录失效流程 + */ +export const syncNeteaseDeviceName = async (): Promise => { + try { + await neteaseCall("deviceinfo_center_upload", {}, { notifyAuthFailure: false }); + } catch { + // 上报失败只影响设备名展示,不影响登录态 + } +}; + /** * 轮询扫码状态 * - 800 已过期 / 801 待扫码 / 802 待确认 / 803 已确认(含 cookie) @@ -46,6 +58,8 @@ export interface QrCheckResult { export const qrCheck = async (key: string): Promise => { const body = await neteaseApi.login_qr_check({ key, timestamp: Date.now() }); const code = (body?.code ?? 801) as QrStatusCode; + /** 803 时主进程已写回登录凭据,可以带上新会话上报设备名 */ + if (code === 803) void syncNeteaseDeviceName(); return { code, cookie: body?.cookie, diff --git a/src/components/modals/LoginDialog.vue b/src/components/modals/LoginDialog.vue index b43b0d8f2..c9bc3dc06 100644 --- a/src/components/modals/LoginDialog.vue +++ b/src/components/modals/LoginDialog.vue @@ -3,7 +3,7 @@ import { useUserStore } from "@/stores/user"; import { toast } from "@/composables/useToast"; import { dialog } from "@/composables/useDialog"; import { REPO_NAME } from "@/utils/config"; -import { neteaseQrLoginAdapter } from "@/apis/login/netease"; +import { neteaseQrLoginAdapter, syncNeteaseDeviceName } from "@/apis/login/netease"; const props = defineProps<{ open: boolean }>(); const emit = defineEmits<{ "update:open": [value: boolean] }>(); @@ -53,6 +53,7 @@ const startAutoFetch = async (): Promise => { return; } if (!(await finishLogin())) void panelRef.value?.refresh(); + else void syncNeteaseDeviceName(); } finally { loading.value = false; }