diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index a8630872bf..26a6cb1c79 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: 2a7d70882a06d8cbdd5b9e6922b403faaadeb4f4 -README.zh.md: a8fdd70ff1c82db37cba64c072e67ec068de6cd9 +README.md: 196e16b03bef4a817b1ff28c8b4c5e3d7ed5309d +README.zh.md: 22a55a767d48ab8dc6eeef86b06fb4526f5db11d diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index 2a7d70882a..196e16b03b 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -40,7 +40,7 @@ The Account section appears first in Settings only while signed in; it is hidden The [credentials subsystem](../../../docs/subsystems/credentials.md) owns storage APIs; the [architecture](../../../docs/architecture.md) explains application composition. -Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator. Browser login preopens a tab on the click and navigates it only when that attempt receives its authorization URL; blocked popups retain the copy-link route. The original tab retains the authorization window handle and closes it on failure, timeout, or cancellation. Login feedback stays in the original tab; callbacks never navigate to a second Web UI. +Account login uses a dismissible dialog before the model onboarding credential editor. The dialog uses a compact, right-aligned action row with the primary action last. The sidebar displays the profile name, falling back to the server-masked phone number or email when the name is absent; it stays blank while the profile is loading. Dialog copy and the sidebar account label are not text-selectable; the copy-link button remains available and restores its label two seconds after each successful copy. Waiting shows a copyable authorization link and a loading indicator; timeout and failure require an explicit retry. Closing a waiting dialog cancels its Host attempt. The sidebar can reopen the same API-key editor through the settings coordinator. Browser login preopens a tab on the click and navigates it only when that attempt receives its authorization URL; blocked popups retain the copy-link route. The original tab retains the authorization window handle and closes it on failure, timeout, or cancellation. Login feedback stays in the original tab; callbacks never navigate to a second Web UI. ## Model Experience diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index a8fdd70ff1..22a55a767d 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -40,7 +40,7 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl [凭证子系统](../../../docs/subsystems/credentials.zh.md)定义存储接口;[架构](../../../docs/architecture.zh.md)说明应用组合。 -账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮仍可使用。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。浏览器登录在点击时预先打开标签页,只有该次流程收到授权地址后才导航;弹窗被拦截时保留复制链接入口。原标签页保留授权窗口引用,并在失败、超时或取消时关闭它。登录反馈留在原标签页,回调不会导航到第二个 Web UI。 +账号登录在模型引导的凭证编辑器之前显示可关闭的弹窗。弹窗底部采用紧凑的右对齐横排操作,主操作位于末尾。侧边栏优先显示昵称,缺少昵称时显示服务端返回的脱敏手机号或邮箱;资料加载期间保持留空。 弹窗文字和侧边栏账号状态文字不可选中,复制登录链接按钮始终可用,并在每次复制成功两秒后恢复原文案。等待状态提供可复制的授权链接和加载图标;超时和失败均须用户手动重试。关闭等待弹窗会取消对应 Host 登录流程。侧边栏通过设置协调器重新打开同一个 API Key 编辑器。浏览器登录在点击时预先打开标签页,只有该次流程收到授权地址后才导航;弹窗被拦截时保留复制链接入口。原标签页保留授权窗口引用,并在失败、超时或取消时关闭它。登录反馈留在原标签页,回调不会导航到第二个 Web UI。 ## 模型体验 diff --git a/packages/client/ui-settings-account/src/client/SignInDialog.tsx b/packages/client/ui-settings-account/src/client/SignInDialog.tsx index 8661dfbefb..0ff2071fd3 100644 --- a/packages/client/ui-settings-account/src/client/SignInDialog.tsx +++ b/packages/client/ui-settings-account/src/client/SignInDialog.tsx @@ -17,9 +17,14 @@ export function SignInDialog({ account, start, cancel, close, useApiKey, t }: { }) { const [busy, setBusy] = useState(false) const [failed, setFailed] = useState(false) - const [copied, setCopied] = useState(false) + const [copyCount, setCopyCount] = useState(0) const attempt = account.view?.attempt - useEffect(() => { setCopied(false) }, [attempt?.id, attempt?.authorizeUrl]) + useEffect(() => { setCopyCount(0) }, [attempt?.id, attempt?.authorizeUrl]) + useEffect(() => { + if (copyCount === 0) return + const timer = setTimeout(() => { setCopyCount(0) }, 2000) + return () => { clearTimeout(timer) } + }, [copyCount]) const phase = attempt?.phase const active = busy || phase === 'initializing' || phase === 'waiting-browser' || phase === 'exchanging' || phase === 'committing' const expired = phase === 'expired' @@ -49,9 +54,9 @@ export function SignInDialog({ account, start, cancel, close, useApiKey, t }: { {t('browserPrompt')}{t('browserDescription')}
:
{expired ? t('timeoutDescription') : error ? t('failed') : t('loginDescription')}
diff --git a/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx b/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx
index 203d3cb588..c2bab65253 100644
--- a/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx
+++ b/packages/client/ui-settings-account/tests/sign-in-dialog.client.spec.tsx
@@ -47,3 +47,29 @@ it('keeps an admitted credential commit open on Escape and close', () => {
expect(props.close).not.toHaveBeenCalled()
expect(props.cancel).not.toHaveBeenCalled()
})
+
+it.each([en, zh])('restores the copy label two seconds after the latest successful copy', async (copy) => {
+ const clipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
+ const writeText = vi.fn(async () => {})
+ vi.useFakeTimers()
+ try {
+ Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
+ const authorizeUrl = 'https://platform.deepseek.com/dsh/authorize?state=example'
+ mount({ id, phase: 'waiting-browser', authorizeUrl }, copy)
+ await act(async () => { fireEvent.click(screen.getByRole('button', { name: copy.copyLink })) })
+ expect(writeText).toHaveBeenLastCalledWith(authorizeUrl)
+ act(() => { vi.advanceTimersByTime(1000) })
+ await act(async () => { fireEvent.click(screen.getByRole('button', { name: copy.copiedLink })) })
+ expect(writeText).toHaveBeenCalledTimes(2)
+ act(() => { vi.advanceTimersByTime(1999) })
+ expect(screen.getByRole('button', { name: copy.copiedLink })).toBeTruthy()
+ act(() => { vi.advanceTimersByTime(1) })
+ await act(async () => { fireEvent.click(screen.getByRole('button', { name: copy.copyLink })) })
+ expect(writeText).toHaveBeenCalledTimes(3)
+ } finally {
+ cleanup()
+ vi.useRealTimers()
+ if (clipboard) Object.defineProperty(navigator, 'clipboard', clipboard)
+ else Reflect.deleteProperty(navigator, 'clipboard')
+ }
+})
diff --git a/packages/credentials/deepseek-account-platform/README.i18n.yaml b/packages/credentials/deepseek-account-platform/README.i18n.yaml
index 36fc4f6edb..be04d8af1b 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: 7cc76d5cb003b98d8512601c498731d03385f7ba
-README.zh.md: b3df2f2da2309929eab6e53ef125dd55a1d99e3b
+README.md: 0d44a85b9802eef9e750ac092ff90e7474c22bd7
+README.zh.md: 746a2938d583e6eac82212ef67b81aa3ac28388b
diff --git a/packages/credentials/deepseek-account-platform/README.md b/packages/credentials/deepseek-account-platform/README.md
index 7cc76d5cb0..0d44a85b98 100644
--- a/packages/credentials/deepseek-account-platform/README.md
+++ b/packages/credentials/deepseek-account-platform/README.md
@@ -85,7 +85,7 @@ No model request prefix changes.
The [desktop login decision](../../../.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md) records cancellation and storage ownership.
-The first profile read after sign-in uses the sanitized user returned by auth_exchange. A null or malformed user falls back to current; subsequent refreshes and Host restarts also query current. Exchange registers the submitted device information.
+The first profile read after sign-in uses the sanitized user returned by auth_exchange. A null or malformed user falls back to current; subsequent refreshes and Host restarts also query current. Failed current requests retain the latest successful profile for the same credential in Host memory; credential changes and disposal clear it. Exchange registers the submitted device information.
accountRequestHeaders overlays requestHeaders for current, balance and embedded Platform page/API requests. Cookie pairs merge by name so routing overrides retain other deployment cookies. Authorization initialization, exchange, cancellation and logout keep requestHeaders. Both maps remain private to Host and Electron main; renderer bootstrap receives only origin and token.
diff --git a/packages/credentials/deepseek-account-platform/README.zh.md b/packages/credentials/deepseek-account-platform/README.zh.md
index b3df2f2da2..746a2938d5 100644
--- a/packages/credentials/deepseek-account-platform/README.zh.md
+++ b/packages/credentials/deepseek-account-platform/README.zh.md
@@ -85,7 +85,7 @@ inferenceOrigin 默认为 `https://api.deepseek.com`。私有部署 patch 可将
[桌面登录决策](../../../.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md)记录取消和存储的职责。
-登录后首次读取资料使用 auth_exchange 返回并经筛选的 user。user 为 null 或格式无效时回退到 current;后续刷新及 Host 重启也查询 current。exchange 负责登记提交的设备信息。
+登录后首次读取资料使用 auth_exchange 返回并经筛选的 user。user 为 null 或格式无效时回退到 current;后续刷新及 Host 重启也查询 current。current 请求失败时保留 Host 内存中同一凭证最近一次成功的资料;凭证变更或提供者销毁时清空。exchange 负责登记提交的设备信息。
accountRequestHeaders 覆盖 requestHeaders,供 current、余额以及内嵌 Platform 页面/API 请求使用。Cookie 按名称合并,路由覆盖保留其他部署 Cookie。授权初始化、兑换、取消和退登保持使用 requestHeaders。两组请求头仅供 Host 和 Electron 主进程使用;渲染层 bootstrap 仅接收 origin 和 token。
diff --git a/packages/credentials/deepseek-account-platform/src/index.ts b/packages/credentials/deepseek-account-platform/src/index.ts
index 16614201cb..dac8167b98 100644
--- a/packages/credentials/deepseek-account-platform/src/index.ts
+++ b/packages/credentials/deepseek-account-platform/src/index.ts
@@ -94,6 +94,7 @@ export class PlatformAccount extends DeepSeekAccount {
private readonly revocations = new Set