From 2d0f431181959dd9a1fd57e93bf4965705ada979 Mon Sep 17 00:00:00 2001 From: lsdsjy <1356263+lsdsjy@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:34:04 +0800 Subject: [PATCH] feat(deepseek-account,ui-settings-account): display profile avatars with icon fallback --- .../ui-settings-account/README.i18n.yaml | 4 +-- packages/client/ui-settings-account/README.md | 2 ++ .../client/ui-settings-account/README.zh.md | 2 ++ .../src/client/AccountAvatar.module.css | 1 + .../src/client/AccountAvatar.tsx | 16 +++++++++ .../src/client/AccountMenu.tsx | 3 +- .../src/client/AccountSection.tsx | 5 +-- .../tests/account.client.spec.tsx | 33 +++++++++++++++++++ .../README.i18n.yaml | 4 +-- .../deepseek-account-platform/README.md | 4 ++- .../deepseek-account-platform/README.zh.md | 4 ++- .../deepseek-account-platform/src/details.ts | 3 +- .../tests/account.spec.ts | 5 +-- .../tests/details.spec.ts | 7 ++++ .../deepseek-account/README.i18n.yaml | 4 +-- .../credentials/deepseek-account/README.md | 2 ++ .../credentials/deepseek-account/README.zh.md | 2 ++ .../credentials/deepseek-account/src/types.ts | 2 ++ .../extensions/tool-cordis/src/api-catalog.ts | 2 +- 19 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 packages/client/ui-settings-account/src/client/AccountAvatar.module.css create mode 100644 packages/client/ui-settings-account/src/client/AccountAvatar.tsx create mode 100644 packages/credentials/deepseek-account-platform/tests/details.spec.ts diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index 26a6cb1c79..1813e8c0b7 100644 --- a/packages/client/ui-settings-account/README.i18n.yaml +++ b/packages/client/ui-settings-account/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-account/README.md -README.md: 196e16b03bef4a817b1ff28c8b4c5e3d7ed5309d -README.zh.md: 22a55a767d48ab8dc6eeef86b06fb4526f5db11d +README.md: e27803ee5ac9ebc3d96a4f77f179bfa696c6bbbf +README.zh.md: 24ebc4e9afb76d5db5b6a701707556699717829e diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index 196e16b03b..e27803ee5a 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -20,6 +20,8 @@ The Account settings section displays DeepSeek login state and offers browser si ## Use this package +The sidebar and Account settings display the profile avatar as a circular image, with the account icon as fallback when the URL is absent or the image fails to load. + The section registers through settings.section and uses the account Remote namespace. The stream survives carrier reconnects through the shared Remote supervisor. The page renders feature-owned English and Chinese copy and keeps API keys separate from account state. Balances use Platform Web currency formatting: two decimal places and digit grouping, positive amounts truncated to cents, positive sub-cent amounts shown as <0.01, and negative amounts rounded with a minimum displayed magnitude of 0.01. Raw Host balance strings remain unchanged. diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index 22a55a767d..24ebc4e9af 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -20,6 +20,8 @@ kind: "package-reference" ## 使用此包 +侧边栏和账号设置以圆形图片显示账号头像;头像 URL 缺失或图片加载失败时使用账号图标兜底。 + 该页面通过 settings.section 注册,并使用 account Remote 命名空间。共享 Remote 管理器负责连接恢复后的流重连。页面使用功能自有的中英文文案,将 API Key 与账号状态分开。 余额沿用 Platform Web 的金额格式:两位小数和千分位分组,正金额截断至分,小于一分的正金额显示为 <0.01,负金额按舍入规则处理且显示绝对值至少为 0.01。Host 返回的原始余额字符串保持不变。 diff --git a/packages/client/ui-settings-account/src/client/AccountAvatar.module.css b/packages/client/ui-settings-account/src/client/AccountAvatar.module.css new file mode 100644 index 0000000000..55148e1b29 --- /dev/null +++ b/packages/client/ui-settings-account/src/client/AccountAvatar.module.css @@ -0,0 +1 @@ +.image { display: block; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; } diff --git a/packages/client/ui-settings-account/src/client/AccountAvatar.tsx b/packages/client/ui-settings-account/src/client/AccountAvatar.tsx new file mode 100644 index 0000000000..9c8b4cd6cc --- /dev/null +++ b/packages/client/ui-settings-account/src/client/AccountAvatar.tsx @@ -0,0 +1,16 @@ +/** Shared account picture with an icon fallback for missing or unavailable images. */ +import { useState } from 'react' +import { IconUserOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives' +import css from './AccountAvatar.module.css' + +/** + * Render a decorative avatar next to the account identity. + * @param props - profile picture URL; absent while signed out or loading. + * @returns picture or the default account icon. + */ +export function AccountAvatar({ url }: { url?: string | null | undefined }) { + const [failedUrl, setFailedUrl] = useState() + return url && url !== failedUrl + ? { setFailedUrl(url) }} /> + : +} diff --git a/packages/client/ui-settings-account/src/client/AccountMenu.tsx b/packages/client/ui-settings-account/src/client/AccountMenu.tsx index e50e39c3b9..0a01a4859b 100644 --- a/packages/client/ui-settings-account/src/client/AccountMenu.tsx +++ b/packages/client/ui-settings-account/src/client/AccountMenu.tsx @@ -5,6 +5,7 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-cli import type { AccountSectionInjected } from './AccountSection.tsx' import { SignInDialog } from './SignInDialog.tsx' import { LogoutIcon } from './LogoutIcon.tsx' +import { AccountAvatar } from './AccountAvatar.tsx' import css from './AccountMenu.module.css' /** Account launcher composed by the settings shell. */ @@ -36,7 +37,7 @@ export function AccountMenu({ { setOpen(value => !value) }}> - + {wide && {signedIn ? label : t('signedOut')}} } items={[ diff --git a/packages/client/ui-settings-account/src/client/AccountSection.tsx b/packages/client/ui-settings-account/src/client/AccountSection.tsx index fc32f52eef..ddf16784b9 100644 --- a/packages/client/ui-settings-account/src/client/AccountSection.tsx +++ b/packages/client/ui-settings-account/src/client/AccountSection.tsx @@ -1,10 +1,11 @@ /** Account settings renders safe Host state and explicit login actions. */ import { useEffect, useState } from 'react' -import { Button, IconUserOutlineMedium } from '@deepseek-ai/dsh-client-ui-primitives' +import { Button } from '@deepseek-ai/dsh-client-ui-primitives' import type { AccountDetails, AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-account/types' import type { PropsRuntime, PropsLocale, InjectFace, HostObservable } from '@deepseek-ai/dsh-client-ui-slots' import { PlatformOverlay, type PlatformBridge } from './PlatformOverlay.tsx' import { formatBalance } from './formatBalance.ts' +import { AccountAvatar } from './AccountAvatar.tsx' import css from './AccountSection.module.css' /** Safe account snapshot shared by the settings page and launcher. */ @@ -92,7 +93,7 @@ export function AccountSection({ t, useAccount, start, cancel, refresh, platform loadingLabel={t('loading')} failureLabel={t('failed')} onClose={() => { setPlatformPage(undefined) }} />}
- +
{signedIn ? profile?.name ?? t('signedIn') : t('signedOut')} {status} diff --git a/packages/client/ui-settings-account/tests/account.client.spec.tsx b/packages/client/ui-settings-account/tests/account.client.spec.tsx index dc11930120..789dd37adf 100644 --- a/packages/client/ui-settings-account/tests/account.client.spec.tsx +++ b/packages/client/ui-settings-account/tests/account.client.spec.tsx @@ -173,3 +173,36 @@ it.each([ wide openSettings={() => {}} openOnboarding={() => {}} t={key => en[key as AccountKey]} />) expect(screen.getByRole('button', { name: en.menu }).textContent).toBe(expected) }) + +it('shows the profile image in settings and the sidebar, with independent load-error fallbacks', async () => { + const operations = mount({ status: 'credential-stored', attempt: null }, en, { + profile: { status: 'ready', value: { id: null, name: 'User', contact: null, avatarUrl: 'https://example.test/avatar.png' } }, + }) + const { AccountMenu } = await import('../src/client/AccountMenu.tsx') + render( selector(operations.hooks.account.getSnapshot())} + wide openSettings={() => {}} openOnboarding={() => {}} t={key => en[key as AccountKey]} />) + const images = document.querySelectorAll('img') + expect(images).toHaveLength(2) + for (const image of images) { + expect(image.getAttribute('src')).toBe('https://example.test/avatar.png') + const parent = image.parentElement! + fireEvent.error(image) + expect(parent.querySelector('img')).toBeNull() + expect(parent.querySelector('svg')).not.toBeNull() + } +}) + +it('retries a changed avatar URL after an image fails', async () => { + const { AccountAvatar } = await import('../src/client/AccountAvatar.tsx') + const view = render() + expect(view.container.querySelector('img')).toBeNull() + expect(view.container.querySelector('svg')).not.toBeNull() + view.rerender() + fireEvent.error(view.container.querySelector('img')!) + expect(view.container.querySelector('svg')).not.toBeNull() + view.rerender() + expect(view.container.querySelector('img')!.getAttribute('src')).toBe('https://example.test/new.png') + view.rerender() + expect(view.container.querySelector('img')).toBeNull() +}) diff --git a/packages/credentials/deepseek-account-platform/README.i18n.yaml b/packages/credentials/deepseek-account-platform/README.i18n.yaml index 862b198b66..4820b81520 100644 --- a/packages/credentials/deepseek-account-platform/README.i18n.yaml +++ b/packages/credentials/deepseek-account-platform/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/credentials/deepseek-account-platform/README.md -README.md: 388e65e84f8011318f4e18746919b4582e12b712 -README.zh.md: b65a8c0a12fd075d71ada02f1a4f6220aec799c1 +README.md: bf18ce3cde01503bc5bd2bc2cfb02fe787e7b918 +README.zh.md: c7971c0d89aa00383a47a41313f0b8cd6adc8b9e diff --git a/packages/credentials/deepseek-account-platform/README.md b/packages/credentials/deepseek-account-platform/README.md index 388e65e84f..bf18ce3cde 100644 --- a/packages/credentials/deepseek-account-platform/README.md +++ b/packages/credentials/deepseek-account-platform/README.md @@ -26,7 +26,9 @@ Sign in through the system browser and keep the account credential in the existi ## Use this package -getProfile / getBalance sends the stored grant in the x-dsh-auth-token header to GET /auth-api/v0/users/current and GET /api/v0/users/get_user_summary on platformOrigin. The grant issuer must match that origin. Host projects only the account UID, profile name, phone or email exactly as masked by Platform, and normal_wallets currency/balance strings; response tokens and other fields are discarded. Bonus wallets remain separate from recharge balances. Credential changes and disposal invalidate in-flight queries. +The profile projection maps `id_profile.picture` to `avatarUrl`, using null when no picture is configured. + +getProfile / getBalance sends the stored grant in the x-dsh-auth-token header to GET /auth-api/v0/users/current and GET /api/v0/users/get_user_summary on platformOrigin. The grant issuer must match that origin. Host projects only the account UID, profile name, avatar URL, phone or email exactly as masked by Platform, and normal_wallets currency/balance strings; response tokens and other fields are discarded. Bonus wallets remain separate from recharge balances. Credential changes and disposal invalidate in-flight queries. Configure platformOrigin, allowLoopbackHttp, requestTimeoutMs, and attemptTimeoutMs in the plugin row. HTTP is accepted only for explicitly enabled loopback development. The provider registers /oauth/callback on the existing Host webServer before auth_init, validates state, exchanges an S256 PKCE code once, and commits a grant before redirecting to auth_exchange.biz_data.authorized_url. Browser destinations default to the configured platform origin and always require the fixed /dsh/authorize or /dsh/authorized path. The completion URL sets `client_type` to the initiating client (`web` or `desktop`) and preserves other Platform-supplied query parameters. Device identity is a separate random UUID record shared by processes using the same credential store; device_model reports OS and architecture. diff --git a/packages/credentials/deepseek-account-platform/README.zh.md b/packages/credentials/deepseek-account-platform/README.zh.md index b65a8c0a12..c7971c0d89 100644 --- a/packages/credentials/deepseek-account-platform/README.zh.md +++ b/packages/credentials/deepseek-account-platform/README.zh.md @@ -26,7 +26,9 @@ getPlatformSession 仅在已存授权的 issuer 与 platformOrigin 一致时导 ## 使用此包 -getProfile / getBalance 将保存的授权 token 通过 x-dsh-auth-token 请求头,向 platformOrigin 上的 GET /auth-api/v0/users/current 和 GET /api/v0/users/get_user_summary 发起请求。授权签发来源必须与该来源一致。Host 只投影账号 UID、资料名称、由 Platform 脱敏的手机号或邮箱(原样保留),以及 normal_wallets 的币种和余额字符串,丢弃响应 token 与其他字段。赠送钱包不计入充值余额。凭证变化和销毁会使进行中的查询失效。 +账号资料投影将 `id_profile.picture` 映射为 `avatarUrl`,没有配置头像时返回 null。 + +getProfile / getBalance 将保存的授权 token 通过 x-dsh-auth-token 请求头,向 platformOrigin 上的 GET /auth-api/v0/users/current 和 GET /api/v0/users/get_user_summary 发起请求。授权签发来源必须与该来源一致。Host 只投影账号 UID、资料名称、头像 URL、由 Platform 脱敏的手机号或邮箱(原样保留),以及 normal_wallets 的币种和余额字符串,丢弃响应 token 与其他字段。赠送钱包不计入充值余额。凭证变化和销毁会使进行中的查询失效。 在插件行配置 platformOrigin、allowLoopbackHttp、requestTimeoutMs 和 attemptTimeoutMs。HTTP 仅用于显式启用的本机开发。提供者先在现有 Host webServer 注册 /oauth/callback,再调用 auth_init;校验 state、使用 S256 PKCE 授权码兑换一次,并在跳转 auth_exchange.biz_data.authorized_url 前提交授权记录。浏览器地址默认要求匹配配置的平台来源,并始终要求固定的 /dsh/authorize 或 /dsh/authorized 路径。完成页地址将 `client_type` 设为发起登录的客户端类型(`web` 或 `desktop`),并保留平台返回的其他查询参数。设备标识是独立的随机 UUID 记录,由使用同一凭证存储的进程共享;device_model 报告操作系统和架构。 diff --git a/packages/credentials/deepseek-account-platform/src/details.ts b/packages/credentials/deepseek-account-platform/src/details.ts index d6c6670eb5..d00aeabd57 100644 --- a/packages/credentials/deepseek-account-platform/src/details.ts +++ b/packages/credentials/deepseek-account-platform/src/details.ts @@ -6,7 +6,7 @@ import { PlatformAuthError, requestAccount } from './protocol.ts' const user = z.object({ id: z.string().nullish(), email: z.string(), mobile: z.string().optional(), mobile_number: z.string().optional(), - id_profile: z.object({ name: z.string().nullable() }).nullish(), + id_profile: z.object({ name: z.string().nullable(), picture: z.string().nullish() }).nullish(), }) const summary = z.object({ normal_wallets: z.array(z.object({ currency: z.enum(['CNY', 'USD']), balance: z.string().regex(/^-?\d+(?:\.\d+)?$/), @@ -22,6 +22,7 @@ export function profile(value: unknown): AccountProfile { const { email, mobile, mobile_number: mobileNumber, id_profile: identity } = parsed.data return { id: parsed.data.id == null ? null : parsed.data.id as AccountUserId, + avatarUrl: identity?.picture || null, name: identity?.name || null, contact: mobile || mobileNumber || email || null, } } diff --git a/packages/credentials/deepseek-account-platform/tests/account.spec.ts b/packages/credentials/deepseek-account-platform/tests/account.spec.ts index c6884ed9c4..03a737ef23 100644 --- a/packages/credentials/deepseek-account-platform/tests/account.spec.ts +++ b/packages/credentials/deepseek-account-platform/tests/account.spec.ts @@ -298,7 +298,7 @@ it('queries Platform Web endpoints with the stored grant and projects only maske expect(await readDetails(f.account)).toBeNull() await storeAccount(f) expect(await readDetails(f.account)).toEqual({ - profile: { status: 'ready', value: { id: 'test-user', name: 'Test Account', contact: '138****5678' } }, + profile: { status: 'ready', value: { id: 'test-user', name: 'Test Account', avatarUrl: null, contact: '138****5678' } }, balance: { status: 'ready', value: [{ currency: 'CNY', balance: '123.45' }, { currency: 'USD', balance: '6.78' }] }, }) expect(f.detailRequests).toEqual(expect.arrayContaining([ @@ -353,7 +353,7 @@ it.each([ const f = await fixture(input) await storeAccount(f) expect((await readDetails(f.account))?.profile).toEqual({ - status: 'ready', value: { id: 'test-user', name: 'Test Account', contact: expected }, + status: 'ready', value: { id: 'test-user', name: 'Test Account', avatarUrl: null, contact: expected }, }) }) @@ -589,6 +589,7 @@ it('uses exchange user for the first profile read and fetches current on refresh { "status": "ready", "value": { + "avatarUrl": null, "contact": "e***@example.invalid", "id": "exchange-user", "name": "Exchange User", diff --git a/packages/credentials/deepseek-account-platform/tests/details.spec.ts b/packages/credentials/deepseek-account-platform/tests/details.spec.ts new file mode 100644 index 0000000000..c33019c54e --- /dev/null +++ b/packages/credentials/deepseek-account-platform/tests/details.spec.ts @@ -0,0 +1,7 @@ +import { expect, it } from 'vitest' +import { profile } from '../src/details.ts' + +it.each([undefined, null, '', 'https://example.test/avatar.png'])('projects Platform picture %s', (picture) => { + expect(profile({ id: 'user', email: 'masked@example.test', id_profile: { name: 'User', picture } }).avatarUrl) + .toBe(picture || null) +}) diff --git a/packages/credentials/deepseek-account/README.i18n.yaml b/packages/credentials/deepseek-account/README.i18n.yaml index bc458245e3..b1aaa360c7 100644 --- a/packages/credentials/deepseek-account/README.i18n.yaml +++ b/packages/credentials/deepseek-account/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/credentials/deepseek-account/README.md -README.md: f56907a93d0b0c6cd4aa1b6a639d09aa9a6d160a -README.zh.md: f9f2dd77b88988599df409e392dfaa168e5f77c3 +README.md: 396463e0207671faf70e8fcd5457b2c8431622fd +README.zh.md: f7d8b93cfcbccd23b6fc6c6c72e72db624f58e78 diff --git a/packages/credentials/deepseek-account/README.md b/packages/credentials/deepseek-account/README.md index f56907a93d..396463e020 100644 --- a/packages/credentials/deepseek-account/README.md +++ b/packages/credentials/deepseek-account/README.md @@ -24,6 +24,8 @@ Account consumers read stored login state, start or cancel a browser login, and ## Use this package +`AccountProfile.avatarUrl` is an optional profile image URL; null or absence means no avatar. + The service defines account operations and reconnectable state snapshots. The platform provider owns the protocol and stored grant. Credentials are Host-only; the API controller exports state and commands without resolveToken. diff --git a/packages/credentials/deepseek-account/README.zh.md b/packages/credentials/deepseek-account/README.zh.md index f9f2dd77b8..f7d8b93cfc 100644 --- a/packages/credentials/deepseek-account/README.zh.md +++ b/packages/credentials/deepseek-account/README.zh.md @@ -24,6 +24,8 @@ getPlatformSession 为原生 Platform 内嵌提供仅限 Host 的 origin/token ## 使用此包 +`AccountProfile.avatarUrl` 是可选的账号头像 URL;为空或缺失时表示没有头像。 + 该服务定义账号操作和可重连的状态快照。平台提供者负责协议和授权记录。凭证仅限 Host;API 控制器只导出状态与操作,不导出 resolveToken。 diff --git a/packages/credentials/deepseek-account/src/types.ts b/packages/credentials/deepseek-account/src/types.ts index aebeaa4a6d..2639f8552a 100644 --- a/packages/credentials/deepseek-account/src/types.ts +++ b/packages/credentials/deepseek-account/src/types.ts @@ -32,6 +32,8 @@ export interface AccountProfile { readonly id: AccountUserId | null readonly name: string | null readonly contact: string | null + /** Profile image URL supplied by Platform, absent when no picture is configured. */ + readonly avatarUrl?: string | null } /** Platform recharge-wallet balance; decimal strings retain server precision. */ export interface AccountWallet { diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 172a94018b..31a84a0f2b 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -4059,7 +4059,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'AccountProfile', - declaration: 'export interface AccountProfile {\n readonly id: AccountUserId | null;\n readonly name: string | null;\n readonly contact: string | null;\n}', + declaration: 'export interface AccountProfile {\n readonly id: AccountUserId | null;\n readonly name: string | null;\n readonly contact: string | null;\n readonly avatarUrl?: string | null;\n}', }, { name: 'AccountUserId',