fix(desktop): extract real account name for wenxin/yuanbao/deepseek binding
The generic AI page-state heuristic only catches displayName via DOM patterns
like [class*="user-name"], so wenxin/yuanbao/deepseek (CSS-module hashed
classes or no exposed store) fell back to the placeholder "${label} 会话".
Add per-platform readers under adapters/{wenxin,yuanbao,deepseek}/account-identity.ts:
- wenxin: GET /eb/user/info with BDUSS cookie; fall back to __NEXT_REDUX_STORE__
and sidebar DOM if the API fails or hasn't responded yet.
- yuanbao: poll the stable BEM .yb-nav__user .nick-info-name + avatar img,
since yuanbao exposes no user-info API and the sidebar renders late.
- deepseek: poll localStorage for userToken, then call /api/v0/users/current
with Authorization: Bearer <token>; reads biz_data.id_profile.{name,picture}.
detectWenxin/Yuanbao/DeepSeekPlatform now run the credential-backed detector
first, then override the placeholder displayName/avatar via the per-platform
reader. Existing rows in DB still hold the placeholder until the account is
re-bound — probe does not write back display_name yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
|||||||
sessionFetchJson,
|
sessionFetchJson,
|
||||||
sessionFetchText,
|
sessionFetchText,
|
||||||
} from './adapters/common'
|
} from './adapters/common'
|
||||||
|
import { readDeepseekAccountIdentity } from './adapters/deepseek/account-identity'
|
||||||
import {
|
import {
|
||||||
buildDoubaoSessionCredentialEvidence,
|
buildDoubaoSessionCredentialEvidence,
|
||||||
DOUBAO_AUTH_PROBE_URL,
|
DOUBAO_AUTH_PROBE_URL,
|
||||||
@@ -44,6 +45,8 @@ import {
|
|||||||
QWEN_AUTH_PROBE_URL,
|
QWEN_AUTH_PROBE_URL,
|
||||||
readQwenAuthPageState,
|
readQwenAuthPageState,
|
||||||
} from './adapters/qwen/auth-rules'
|
} from './adapters/qwen/auth-rules'
|
||||||
|
import { readWenxinAccountIdentity } from './adapters/wenxin/account-identity'
|
||||||
|
import { readYuanbaoAccountIdentity } from './adapters/yuanbao/account-identity'
|
||||||
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
|
import type { AccountHealthProfile, AuthProbeResult } from './auth-types'
|
||||||
import {
|
import {
|
||||||
attachWindowDiagnostics,
|
attachWindowDiagnostics,
|
||||||
@@ -1454,7 +1457,28 @@ async function detectDeepSeekPlatform(
|
|||||||
label: string,
|
label: string,
|
||||||
context: DetectContext,
|
context: DetectContext,
|
||||||
): Promise<DetectedAccount | null> {
|
): Promise<DetectedAccount | null> {
|
||||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
const detected = await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||||
|
if (!detected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderName = `${label} 会话`
|
||||||
|
const needsName = !detected.displayName || detected.displayName === placeholderName
|
||||||
|
const needsAvatar = !detected.avatarUrl
|
||||||
|
if (!needsName && !needsAvatar) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await readDeepseekAccountIdentity(context.webContents).catch(() => null)
|
||||||
|
if (!identity || (!identity.displayName && !identity.avatarUrl)) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeDetectedAccount({
|
||||||
|
platformUid: detected.platformUid,
|
||||||
|
displayName: needsName && identity.displayName ? identity.displayName : detected.displayName,
|
||||||
|
avatarUrl: needsAvatar && identity.avatarUrl ? identity.avatarUrl : detected.avatarUrl,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectYuanbaoPlatform(
|
async function detectYuanbaoPlatform(
|
||||||
@@ -1462,7 +1486,28 @@ async function detectYuanbaoPlatform(
|
|||||||
label: string,
|
label: string,
|
||||||
context: DetectContext,
|
context: DetectContext,
|
||||||
): Promise<DetectedAccount | null> {
|
): Promise<DetectedAccount | null> {
|
||||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
const detected = await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||||
|
if (!detected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderName = `${label} 会话`
|
||||||
|
const needsName = !detected.displayName || detected.displayName === placeholderName
|
||||||
|
const needsAvatar = !detected.avatarUrl
|
||||||
|
if (!needsName && !needsAvatar) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await readYuanbaoAccountIdentity(context.webContents).catch(() => null)
|
||||||
|
if (!identity || (!identity.displayName && !identity.avatarUrl)) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeDetectedAccount({
|
||||||
|
platformUid: detected.platformUid,
|
||||||
|
displayName: needsName && identity.displayName ? identity.displayName : detected.displayName,
|
||||||
|
avatarUrl: needsAvatar && identity.avatarUrl ? identity.avatarUrl : detected.avatarUrl,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectWenxinPlatform(
|
async function detectWenxinPlatform(
|
||||||
@@ -1470,7 +1515,28 @@ async function detectWenxinPlatform(
|
|||||||
label: string,
|
label: string,
|
||||||
context: DetectContext,
|
context: DetectContext,
|
||||||
): Promise<DetectedAccount | null> {
|
): Promise<DetectedAccount | null> {
|
||||||
return await detectCredentialBackedAIPlatform(platformId, label, context)
|
const detected = await detectCredentialBackedAIPlatform(platformId, label, context)
|
||||||
|
if (!detected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderName = `${label} 会话`
|
||||||
|
const needsName = !detected.displayName || detected.displayName === placeholderName
|
||||||
|
const needsAvatar = !detected.avatarUrl
|
||||||
|
if (!needsName && !needsAvatar) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await readWenxinAccountIdentity(context.webContents).catch(() => null)
|
||||||
|
if (!identity || (!identity.displayName && !identity.avatarUrl)) {
|
||||||
|
return detected
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeDetectedAccount({
|
||||||
|
platformUid: detected.platformUid,
|
||||||
|
displayName: needsName && identity.displayName ? identity.displayName : detected.displayName,
|
||||||
|
avatarUrl: needsAvatar && identity.avatarUrl ? identity.avatarUrl : detected.avatarUrl,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detectQwenFromSession(
|
async function detectQwenFromSession(
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { WebContents } from 'electron/main'
|
||||||
|
|
||||||
|
export interface DeepseekAccountIdentityHints {
|
||||||
|
displayName: string | null
|
||||||
|
avatarUrl: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const POLL_TIMEOUT_MS = 6_000
|
||||||
|
const POLL_INTERVAL_MS = 250
|
||||||
|
|
||||||
|
function normalizeText(value: unknown): string | null {
|
||||||
|
if (typeof value !== 'string') return null
|
||||||
|
const trimmed = value.replace(/ /g, ' ').trim().replace(/\s+/g, ' ')
|
||||||
|
return trimmed || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSeek (chat.deepseek.com) 没有 Redux/Pinia/window state,登录态只有 localStorage
|
||||||
|
// 里的 `userToken`(JSON 包了 value 字段)。账号信息要走 /api/v0/users/current,
|
||||||
|
// 用 Authorization: Bearer <token> 调。Cookie 不带 token,因此只能在页面上下文里
|
||||||
|
// 执行 fetch(自带 token),返回 biz_data.id_profile.{name, picture}。
|
||||||
|
//
|
||||||
|
// probe 通常在隐藏窗口、刚加载完就跑,React app 可能还没把 userToken 写入 localStorage,
|
||||||
|
// 所以要 poll 一下直到 token 出现,再调 API。
|
||||||
|
export async function readDeepseekAccountIdentity(
|
||||||
|
webContents: WebContents | null | undefined,
|
||||||
|
): Promise<DeepseekAccountIdentityHints> {
|
||||||
|
if (!webContents || webContents.isDestroyed()) {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await webContents
|
||||||
|
.executeJavaScript(
|
||||||
|
`(async () => {
|
||||||
|
try {
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
const readToken = () => {
|
||||||
|
const raw = window.localStorage.getItem('userToken');
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const value = parsed?.value;
|
||||||
|
return typeof value === 'string' && value ? value : null;
|
||||||
|
} catch {
|
||||||
|
return typeof raw === 'string' && raw ? raw : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deadline = Date.now() + ${POLL_TIMEOUT_MS};
|
||||||
|
let token = readToken();
|
||||||
|
while (!token && Date.now() < deadline) {
|
||||||
|
await sleep(${POLL_INTERVAL_MS});
|
||||||
|
token = readToken();
|
||||||
|
}
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const response = await fetch('/api/v0/users/current', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
const profile = payload?.data?.biz_data?.id_profile ?? null;
|
||||||
|
const bizData = payload?.data?.biz_data ?? null;
|
||||||
|
const name = profile?.name ?? bizData?.email ?? bizData?.mobile_number ?? null;
|
||||||
|
const picture = profile?.picture ?? null;
|
||||||
|
return {
|
||||||
|
displayName: typeof name === 'string' ? name : null,
|
||||||
|
avatarUrl: typeof picture === 'string' ? picture : null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const typed = result as { displayName?: unknown; avatarUrl?: unknown }
|
||||||
|
return {
|
||||||
|
displayName: normalizeText(typed.displayName),
|
||||||
|
avatarUrl: normalizeText(typed.avatarUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import type { WebContents } from 'electron/main'
|
||||||
|
|
||||||
|
export interface WenxinAccountIdentityHints {
|
||||||
|
displayName: string | null
|
||||||
|
avatarUrl: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(value: unknown): string | null {
|
||||||
|
if (typeof value !== 'string') return null
|
||||||
|
const trimmed = value.replace(/ /g, ' ').trim().replace(/\s+/g, ' ')
|
||||||
|
return trimmed || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文心一言 (yiyan.baidu.com) 账号信息优先走 GET /eb/user/info,cookie 自带 BDUSS 即可,
|
||||||
|
// 返回 content.uname (真实账号) + content.fuzzyName (脱敏) + content.portrait (头像),
|
||||||
|
// 对所有账号通用、不依赖 Redux hydrate 时序。
|
||||||
|
//
|
||||||
|
// API 失败时兜底:
|
||||||
|
// 1. 读 __NEXT_REDUX_STORE__.updateReducer.userInfo.{uname, fuzzyName, portrait}
|
||||||
|
// 2. 抓侧栏用户信息块的 DOM
|
||||||
|
export async function readWenxinAccountIdentity(
|
||||||
|
webContents: WebContents | null | undefined,
|
||||||
|
): Promise<WenxinAccountIdentityHints> {
|
||||||
|
if (!webContents || webContents.isDestroyed()) {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await webContents
|
||||||
|
.executeJavaScript(
|
||||||
|
`(async () => {
|
||||||
|
try {
|
||||||
|
const normalize = (value) => {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const trimmed = value.replace(/\\u00a0/g, ' ').trim();
|
||||||
|
return trimmed || null;
|
||||||
|
};
|
||||||
|
const isRecord = (value) =>
|
||||||
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
const isVisible = (node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) return false;
|
||||||
|
const style = window.getComputedStyle(node);
|
||||||
|
if (
|
||||||
|
style.display === 'none' ||
|
||||||
|
style.visibility === 'hidden' ||
|
||||||
|
style.opacity === '0' ||
|
||||||
|
node.hidden
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return rect.width >= 4 && rect.height >= 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
let displayName = null;
|
||||||
|
let avatarUrl = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/eb/user/info', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
const content = payload && payload.ret === 0 ? payload.content : null;
|
||||||
|
if (isRecord(content) && content.isLogin === true) {
|
||||||
|
displayName =
|
||||||
|
normalize(content.uname) ?? normalize(content.fuzzyName);
|
||||||
|
avatarUrl = normalize(content.portrait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to fallbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!displayName || !avatarUrl) {
|
||||||
|
const store = window.__NEXT_REDUX_STORE__;
|
||||||
|
const state = typeof store?.getState === 'function' ? store.getState() : null;
|
||||||
|
const userInfo = isRecord(state?.updateReducer) ? state.updateReducer.userInfo : null;
|
||||||
|
if (isRecord(userInfo)) {
|
||||||
|
displayName =
|
||||||
|
displayName ??
|
||||||
|
normalize(userInfo.uname) ??
|
||||||
|
normalize(userInfo.fuzzyName) ??
|
||||||
|
normalize(userInfo.userName) ??
|
||||||
|
normalize(userInfo.nickName);
|
||||||
|
avatarUrl =
|
||||||
|
avatarUrl ??
|
||||||
|
normalize(userInfo.portrait) ??
|
||||||
|
normalize(userInfo.avatar) ??
|
||||||
|
normalize(userInfo.avatarUrl) ??
|
||||||
|
normalize(userInfo.headPic) ??
|
||||||
|
normalize(userInfo.headUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!displayName) {
|
||||||
|
const sidebarSelectors = [
|
||||||
|
'[class*="userInfo"] [class*="name"]',
|
||||||
|
'[class*="user-info"] [class*="name"]',
|
||||||
|
'[class*="userName"]',
|
||||||
|
'[class*="user-name"]',
|
||||||
|
'[class*="nickName"]',
|
||||||
|
'aside [class*="user"] [class*="name"]',
|
||||||
|
];
|
||||||
|
for (const selector of sidebarSelectors) {
|
||||||
|
const node = document.querySelector(selector);
|
||||||
|
if (!isVisible(node)) continue;
|
||||||
|
const text = normalize(node.textContent || '');
|
||||||
|
if (!text || text.length < 2 || text.length > 48) continue;
|
||||||
|
displayName = text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avatarUrl) {
|
||||||
|
const avatarSelectors = [
|
||||||
|
'[class*="userInfo"] img',
|
||||||
|
'[class*="avatar"] img',
|
||||||
|
'aside img[alt]',
|
||||||
|
];
|
||||||
|
for (const selector of avatarSelectors) {
|
||||||
|
const node = document.querySelector(selector);
|
||||||
|
if (!isVisible(node)) continue;
|
||||||
|
const src = normalize(node && 'src' in node ? node.src : null);
|
||||||
|
if (!src) continue;
|
||||||
|
if (/(logo|icon|favicon)/i.test(src)) continue;
|
||||||
|
avatarUrl = src;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { displayName, avatarUrl };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const typed = result as { displayName?: unknown; avatarUrl?: unknown }
|
||||||
|
return {
|
||||||
|
displayName: normalizeText(typed.displayName),
|
||||||
|
avatarUrl: normalizeText(typed.avatarUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { WebContents } from 'electron/main'
|
||||||
|
|
||||||
|
export interface YuanbaoAccountIdentityHints {
|
||||||
|
displayName: string | null
|
||||||
|
avatarUrl: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const POLL_TIMEOUT_MS = 6_000
|
||||||
|
const POLL_INTERVAL_MS = 250
|
||||||
|
|
||||||
|
function normalizeText(value: unknown): string | null {
|
||||||
|
if (typeof value !== 'string') return null
|
||||||
|
const trimmed = value.replace(/ /g, ' ').trim().replace(/\s+/g, ' ')
|
||||||
|
return trimmed || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 腾讯元宝 (yuanbao.tencent.com) 没暴露用户信息 API,初始化也不会动态拉取。
|
||||||
|
// 但元宝用稳定的 BEM 命名(不是 CSS module hash),从侧栏用户块读:
|
||||||
|
// - 用户名: .yb-nav__user .nick-info-name (<p>)
|
||||||
|
// - 头像: .yb-nav__user .yb-common-nav__ft__avatar img
|
||||||
|
// probe 通常在隐藏窗口里、刚 navigation 就跑,React 还没把侧栏渲染出来,
|
||||||
|
// 所以这里要 poll 一下 DOM 直到 .yb-nav__user 渲染出且 .nick-info-name 有文本。
|
||||||
|
export async function readYuanbaoAccountIdentity(
|
||||||
|
webContents: WebContents | null | undefined,
|
||||||
|
): Promise<YuanbaoAccountIdentityHints> {
|
||||||
|
if (!webContents || webContents.isDestroyed()) {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await webContents
|
||||||
|
.executeJavaScript(
|
||||||
|
`(async () => {
|
||||||
|
try {
|
||||||
|
const normalize = (value) => {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const trimmed = value.replace(/\\u00a0/g, ' ').trim();
|
||||||
|
return trimmed || null;
|
||||||
|
};
|
||||||
|
const isVisible = (node) => {
|
||||||
|
if (!(node instanceof HTMLElement)) return false;
|
||||||
|
const style = window.getComputedStyle(node);
|
||||||
|
if (
|
||||||
|
style.display === 'none' ||
|
||||||
|
style.visibility === 'hidden' ||
|
||||||
|
style.opacity === '0' ||
|
||||||
|
node.hidden
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
return rect.width >= 4 && rect.height >= 4;
|
||||||
|
};
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
const deadline = Date.now() + ${POLL_TIMEOUT_MS};
|
||||||
|
let displayName = null;
|
||||||
|
let avatarUrl = null;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const root = document.querySelector('.yb-nav__user');
|
||||||
|
if (root) {
|
||||||
|
const nameNode = root.querySelector('.nick-info-name');
|
||||||
|
if (isVisible(nameNode)) {
|
||||||
|
const text = normalize(nameNode.textContent || '');
|
||||||
|
if (text) displayName = text;
|
||||||
|
}
|
||||||
|
const avatarImg = root.querySelector('.yb-common-nav__ft__avatar img');
|
||||||
|
if (avatarImg && isVisible(avatarImg)) {
|
||||||
|
const src = normalize(avatarImg.src);
|
||||||
|
if (src && !/(logo|icon|favicon)/i.test(src)) {
|
||||||
|
avatarUrl = src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (displayName && avatarUrl) break;
|
||||||
|
await sleep(${POLL_INTERVAL_MS});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { displayName, avatarUrl };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();`,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.catch(() => null)
|
||||||
|
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
return { displayName: null, avatarUrl: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const typed = result as { displayName?: unknown; avatarUrl?: unknown }
|
||||||
|
return {
|
||||||
|
displayName: normalizeText(typed.displayName),
|
||||||
|
avatarUrl: normalizeText(typed.avatarUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user