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:
2026-05-08 19:54:46 +08:00
parent 1fb6d28102
commit 34b26e1079
4 changed files with 404 additions and 3 deletions
@@ -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/infocookie 自带 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),
}
}