test(desktop): cover deepseek account name extraction for wechat + phone logins
Desktop Client Build / Resolve Build Metadata (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 4m5s
Backend CI / Backend (push) Successful in 17m7s
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled

Refactor readDeepseekAccountIdentity so the JSON payload parser is a pure
ts function (extractDeepseekIdentityFromPayload). The webContents IIFE now
just polls for userToken and returns the raw API JSON; payload picking lives
in ts and is unit-tested.

Tests lock the contract for both login types we observed in the wild:
  - WeChat-bound account: id_profile.name = '阿白', picture present.
  - Phone-only login: id_profile = null, mobile_number = '177******08',
    email = '' (empty string, not null) — the case that originally regressed.

Plus regressions for the priority order (profile.name > mobile_number > email),
the all-empty fallback, and non-record payloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 20:02:57 +08:00
parent bc8f1c353f
commit af35301d9b
9 changed files with 963 additions and 208 deletions
@@ -0,0 +1,115 @@
import { describe, expect, it } from 'vitest'
import { extractDeepseekIdentityFromPayload } from './account-identity'
describe('extractDeepseekIdentityFromPayload', () => {
it('returns the WeChat profile name + avatar for WECHAT-bound accounts', () => {
const wechatPayload = {
code: 0,
msg: '',
data: {
biz_code: 0,
biz_msg: '',
biz_data: {
id: '7a15e288-1560-49be-a519-3e3da071071c',
email: '',
mobile_number: '130******73',
area_code: '+86',
status: 0,
id_profile: {
provider: 'WECHAT',
id: 'e5a1861a-6c1b-48ae-9d2c-00e643e6b4bd',
name: '阿白',
picture: 'https://static.deepseek.com/user-avatar/LwJ3Jq-HLkgwCGbl0HS1lECW',
locale: 'zh_CN',
email: null,
},
},
},
}
expect(extractDeepseekIdentityFromPayload(wechatPayload)).toEqual({
displayName: '阿白',
avatarUrl: 'https://static.deepseek.com/user-avatar/LwJ3Jq-HLkgwCGbl0HS1lECW',
})
})
it('falls back to mobile_number for phone-only logins (id_profile is null, email is empty string)', () => {
const phonePayload = {
code: 0,
msg: '',
data: {
biz_code: 0,
biz_msg: '',
biz_data: {
id: 'd782f68b-7358-4290-b891-38cb7a1e11fa',
email: '',
mobile_number: '177******08',
area_code: '+86',
status: 0,
id_profile: null,
id_profiles: [],
},
},
}
expect(extractDeepseekIdentityFromPayload(phonePayload)).toEqual({
displayName: '177******08',
avatarUrl: null,
})
})
it('prefers id_profile.name over mobile_number when both are present', () => {
const payload = {
data: {
biz_data: {
mobile_number: '130******73',
id_profile: {
provider: 'WECHAT',
name: '阿白',
picture: null,
},
},
},
}
expect(extractDeepseekIdentityFromPayload(payload).displayName).toBe('阿白')
})
it('uses email as last-resort fallback when id_profile and mobile_number are empty', () => {
const payload = {
data: {
biz_data: {
id_profile: null,
mobile_number: '',
email: 'user@example.com',
},
},
}
expect(extractDeepseekIdentityFromPayload(payload).displayName).toBe('user@example.com')
})
it('returns null displayName when nothing usable is present', () => {
expect(
extractDeepseekIdentityFromPayload({
data: { biz_data: { id_profile: null, email: '', mobile_number: '' } },
}),
).toEqual({ displayName: null, avatarUrl: null })
})
it('rejects non-record payloads without throwing', () => {
expect(extractDeepseekIdentityFromPayload(null)).toEqual({
displayName: null,
avatarUrl: null,
})
expect(extractDeepseekIdentityFromPayload('hello')).toEqual({
displayName: null,
avatarUrl: null,
})
expect(extractDeepseekIdentityFromPayload([])).toEqual({
displayName: null,
avatarUrl: null,
})
})
})
@@ -14,18 +14,52 @@ function normalizeText(value: unknown): string | null {
return trimmed || null
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function pickString(value: unknown): string | null {
return typeof value === 'string' && value.trim() ? value : null
}
// Pure parser, exported for tests. Both 微信登录 and 手机号登录 must work:
// 微信: { biz_data: { id_profile: { provider: 'WECHAT', name: '阿白', picture: '...' }, ... } }
// 手机: { biz_data: { id_profile: null, mobile_number: '177******08', email: '', ... } }
// Empty-string fields (email = "") are common, so we use truthy checks rather than ??.
export function extractDeepseekIdentityFromPayload(
payload: unknown,
): DeepseekAccountIdentityHints {
if (!isRecord(payload)) {
return { displayName: null, avatarUrl: null }
}
const data = isRecord(payload.data) ? payload.data : null
const bizData = data && isRecord(data.biz_data) ? data.biz_data : null
if (!bizData) {
return { displayName: null, avatarUrl: null }
}
const profile = isRecord(bizData.id_profile) ? bizData.id_profile : null
const displayName =
pickString(profile?.name) ??
pickString(bizData.mobile_number) ??
pickString(bizData.email)
const avatarUrl = pickString(profile?.picture)
return {
displayName: normalizeText(displayName),
avatarUrl: normalizeText(avatarUrl),
}
}
// DeepSeek (chat.deepseek.com) 没有 Redux/Pinia/window state,登录态只有 localStorage
// 里的 `userToken`JSON 包了 value 字段)。账号信息要走 /api/v0/users/current
// 用 Authorization: Bearer <token> 调。Cookie 不带 token,因此只能在页面上下文里
// 执行 fetch(自带 token)。
//
// 不同登录方式响应差异较大:
// 微信登录: id_profile = { provider: 'WECHAT', name: '阿白', picture: '...' }
// 手机登录: id_profile = null, mobile_number = '177******08'(其它字段是空串而非 null)
// 所以这里 fallback 必须用 truthy 判断而不是 ??,否则空串会击穿后续兜底。
// 里的 `userToken`JSON 包了 value 字段)。账号信息要走 GET /api/v0/users/current
// 用 Authorization: Bearer <token> 调cookie 不带 token)。
//
// probe 通常在隐藏窗口、刚加载完就跑,React app 可能还没把 userToken 写入 localStorage
// 所以要 poll 一下直到 token 出现,再调 API。
// 所以要 poll 一下直到 token 出现,再调 API。webContents 里的 IIFE 仅拉原始 JSON
// 解析与字段挑选放到 extractDeepseekIdentityFromPayload 里以便单测。
export async function readDeepseekAccountIdentity(
webContents: WebContents | null | undefined,
): Promise<DeepseekAccountIdentityHints> {
@@ -33,7 +67,7 @@ export async function readDeepseekAccountIdentity(
return { displayName: null, avatarUrl: null }
}
const result = await webContents
const rawJson = await webContents
.executeJavaScript(
`(async () => {
try {
@@ -64,17 +98,7 @@ export async function readDeepseekAccountIdentity(
headers: { Authorization: 'Bearer ' + token },
});
if (!response.ok) return null;
const payload = await response.json().catch(() => null);
const bizData = payload?.data?.biz_data ?? null;
const profile = bizData?.id_profile ?? null;
const pickString = (value) =>
typeof value === 'string' && value.trim() ? value : null;
const name =
pickString(profile?.name) ??
pickString(bizData?.mobile_number) ??
pickString(bizData?.email);
const picture = pickString(profile?.picture);
return { displayName: name, avatarUrl: picture };
return await response.text();
} catch {
return null;
}
@@ -83,13 +107,16 @@ export async function readDeepseekAccountIdentity(
)
.catch(() => null)
if (!result || typeof result !== 'object') {
if (typeof rawJson !== 'string' || !rawJson) {
return { displayName: null, avatarUrl: null }
}
const typed = result as { displayName?: unknown; avatarUrl?: unknown }
return {
displayName: normalizeText(typed.displayName),
avatarUrl: normalizeText(typed.avatarUrl),
let payload: unknown
try {
payload = JSON.parse(rawJson)
} catch {
return { displayName: null, avatarUrl: null }
}
return extractDeepseekIdentityFromPayload(payload)
}