{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',