diff --git a/apps/desktop/src/platform-view.ts b/apps/desktop/src/platform-view.ts index d8a1439b89..acebb35131 100644 --- a/apps/desktop/src/platform-view.ts +++ b/apps/desktop/src/platform-view.ts @@ -38,6 +38,7 @@ export class DesktopPlatformView { /** @param next - private Host credential snapshot; replacement invalidates the current document. */ setSession(next: PlatformSession | null): void { if (next?.token === this.account?.token && next?.origin === this.account?.origin + && next?.embeddedPageDist === this.account?.embeddedPageDist && JSON.stringify(next?.requestHeaders) === JSON.stringify(this.account?.requestHeaders)) return this.close() this.account = next @@ -103,7 +104,9 @@ export class DesktopPlatformView { owner.contentView.addChildView(view) view.setBounds(bounds) try { - await view.webContents.loadURL(new URL(page === 'usage' ? '/usage' : '/top_up', account.origin).href) + const url = new URL(page === 'usage' ? '/usage' : '/top_up', account.origin) + if (account.embeddedPageDist) url.searchParams.set('dist', account.embeddedPageDist) + await view.webContents.loadURL(url.href) if (generation === this.generation && this.view === view) view.setVisible(true) } catch (error) { if (generation === this.generation) this.close() diff --git a/apps/desktop/tests/host-process.spec.ts b/apps/desktop/tests/host-process.spec.ts index fe95945964..1efc6804ff 100644 --- a/apps/desktop/tests/host-process.spec.ts +++ b/apps/desktop/tests/host-process.spec.ts @@ -184,12 +184,12 @@ describe('desktop host process', () => { }) it('carries Platform credentials over private IPC and clears them on shutdown', async () => { - const runtime = projectWithHost(HTTP_HOST.replace("process.send({ type: 'ready'", "process.send({ type: 'platform-session', session: { origin: 'https://platform.deepseek.com', token: 'fixture-secret' } }); process.send({ type: 'ready'")) + const runtime = projectWithHost(HTTP_HOST.replace("process.send({ type: 'ready'", "process.send({ type: 'platform-session', session: { origin: 'https://platform.deepseek.com', token: 'fixture-secret', embeddedPageDist: 'feat/test' } }); process.send({ type: 'ready'")) const changed = vi.fn() const host = new DesktopHostProcess(process.execPath, runtime, runtime, undefined, process.env, undefined, undefined, 'link', undefined, changed) hosts.push(host) await host.start() - expect(changed).toHaveBeenCalledWith({ origin: 'https://platform.deepseek.com', token: 'fixture-secret' }) + expect(changed).toHaveBeenCalledWith({ origin: 'https://platform.deepseek.com', token: 'fixture-secret', embeddedPageDist: 'feat/test' }) await host.stop() expect(changed).toHaveBeenLastCalledWith(null) }) diff --git a/apps/desktop/tests/platform-view.spec.ts b/apps/desktop/tests/platform-view.spec.ts index 3ca4fb8fd1..69eb8c1361 100644 --- a/apps/desktop/tests/platform-view.spec.ts +++ b/apps/desktop/tests/platform-view.spec.ts @@ -155,3 +155,17 @@ it('injects deployment headers only at the Platform origin and excludes them fro .toEqual({ origin: 'https://platform.deepseek.com', token: 'fixture-secret' }) manager.close() }) + +it.each(['usage', 'top-up'] as const)('selects the configured frontend deployment for %s', async (page) => { + const { manager, owner } = setup() + manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret', embeddedPageDist: 'feat/test&other=value' }) + await manager.open(owner, page, bounds) + const url = new URL(view().webContents.loadURL.mock.calls[0]![0] as string) + expect(url.origin).toBe('https://platform.deepseek.com') + expect(url.pathname).toBe(page === 'usage' ? '/usage' : '/top_up') + expect([...url.searchParams]).toEqual([['dist', 'feat/test&other=value']]) + const previous = view().webContents + manager.setSession({ origin: 'https://platform.deepseek.com', token: 'fixture-secret', embeddedPageDist: 'another' }) + expect(previous.close).toHaveBeenCalledOnce() + manager.close() +}) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 7e8960e576..fb8ae963f5 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -612,6 +612,8 @@ Requires: `credentials` · `authorization` export interface Config { /** Platform origin serving auth-api and browser pages. */ platformOrigin?: string + /** Optional frontend deployment selector for embedded Usage and Top-up pages. */ + embeddedPageDist?: string /** Allow HTTP only on loopback for the development Mock. */ allowLoopbackHttp?: boolean /** Map authorization and completion pages to platformOrigin for private development proxies. */ diff --git a/packages/client/ui-settings-account/README.i18n.yaml b/packages/client/ui-settings-account/README.i18n.yaml index edae6c7542..293204f88d 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: 496a130dcc27425dc1669eb1fe3ff04d781c3e7d -README.zh.md: c8639f3df3f4029e2421acdc279588eb840afb1d +README.md: 02e1054cd5d3c1b77f49b86682dd6bfaff5708b0 +README.zh.md: dbc20df87be97362682ae4445b91cab1b817d0ea diff --git a/packages/client/ui-settings-account/README.md b/packages/client/ui-settings-account/README.md index 496a130dcc..02e1054cd5 100644 --- a/packages/client/ui-settings-account/README.md +++ b/packages/client/ui-settings-account/README.md @@ -38,7 +38,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. 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. 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. ## Model Experience diff --git a/packages/client/ui-settings-account/README.zh.md b/packages/client/ui-settings-account/README.zh.md index c8639f3df3..dbc20df87b 100644 --- a/packages/client/ui-settings-account/README.zh.md +++ b/packages/client/ui-settings-account/README.zh.md @@ -38,7 +38,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/AccountMenu.module.css b/packages/client/ui-settings-account/src/client/AccountMenu.module.css index f2c21bb84e..0ea93c5977 100644 --- a/packages/client/ui-settings-account/src/client/AccountMenu.module.css +++ b/packages/client/ui-settings-account/src/client/AccountMenu.module.css @@ -1,6 +1,6 @@ .root { flex: 1; min-width: 0; } .anchor { width: 100%; } -.trigger { display: flex; align-items: center; width: 100%; gap: 8px; padding: 6px; border: 0; border-radius: 12px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; cursor: pointer; } +.trigger { user-select: none; display: flex; align-items: center; width: 100%; gap: 8px; padding: 6px; border: 0; border-radius: 12px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; cursor: pointer; } .trigger:hover { background: var(--dsw-alias-interactive-bg-hover); } .avatar { display: flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; border-radius: 16px; background: var(--dsw-alias-bg-skeleton); color: var(--dsw-alias-label-tertiary); } .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/packages/client/ui-settings-account/src/client/SignInDialog.module.css b/packages/client/ui-settings-account/src/client/SignInDialog.module.css index 624e88e304..17775a60c6 100644 --- a/packages/client/ui-settings-account/src/client/SignInDialog.module.css +++ b/packages/client/ui-settings-account/src/client/SignInDialog.module.css @@ -1,4 +1,4 @@ -.dialog { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-inverted); } +.dialog { user-select: none; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-inverted); } .content { display: flex; flex-direction: column; width: 100%; } .header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 22px 24px 12px; } .title { margin: 0; font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); } diff --git a/packages/credentials/deepseek-account-platform/README.i18n.yaml b/packages/credentials/deepseek-account-platform/README.i18n.yaml index e1562cd77d..7a0caf27cc 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: 522a98cd359f677409090823a5465be88f1fac37 -README.zh.md: b33c64404b4477214762eabccf616102a0ff84d8 +README.md: 67824c36dd4dd42405e31d5ac57d4b5c2c795458 +README.zh.md: da999fb2ee6fd9a70955ec2b35f5c646cf67148c diff --git a/packages/credentials/deepseek-account-platform/README.md b/packages/credentials/deepseek-account-platform/README.md index 522a98cd35..67824c36dd 100644 --- a/packages/credentials/deepseek-account-platform/README.md +++ b/packages/credentials/deepseek-account-platform/README.md @@ -82,3 +82,5 @@ The [desktop login decision](../../../.agents/notes/implemented/architecture/202 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. 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. + +The account provider’s `embeddedPageDist` configuration adds a `dist` query parameter to embedded Usage and Top-up URLs. Its default is empty; private frontend branch selectors belong in the local profile patch. It does not change API URLs or credential delivery. diff --git a/packages/credentials/deepseek-account-platform/README.zh.md b/packages/credentials/deepseek-account-platform/README.zh.md index b33c64404b..da999fb2ee 100644 --- a/packages/credentials/deepseek-account-platform/README.zh.md +++ b/packages/credentials/deepseek-account-platform/README.zh.md @@ -82,3 +82,5 @@ Host 诊断通过标准输出及 Host 调试器 Console 输出,前缀为 `[dee 登录后首次读取资料使用 auth_exchange 返回并经筛选的 user。user 为 null 或格式无效时回退到 current;后续刷新及 Host 重启也查询 current。exchange 负责登记提交的设备信息。 accountRequestHeaders 覆盖 requestHeaders,供 current、余额以及内嵌 Platform 页面/API 请求使用。Cookie 按名称合并,路由覆盖保留其他部署 Cookie。授权初始化、兑换、取消和退登保持使用 requestHeaders。两组请求头仅供 Host 和 Electron 主进程使用;渲染层 bootstrap 仅接收 origin 和 token。 + +账号提供者的 `embeddedPageDist` 配置为内嵌用量和充值页面 URL 添加 `dist` 查询参数。默认值为空;私有前端分支选择值应写在本地 profile patch 中。此配置不改变 API 地址或凭证传递方式。 diff --git a/packages/credentials/deepseek-account-platform/src/index.ts b/packages/credentials/deepseek-account-platform/src/index.ts index 9a14ad73ce..94a08e8a3e 100644 --- a/packages/credentials/deepseek-account-platform/src/index.ts +++ b/packages/credentials/deepseek-account-platform/src/index.ts @@ -23,6 +23,8 @@ const device = z.object({ id: z.uuid() }) export interface Config { /** Platform origin serving auth-api and browser pages. */ platformOrigin?: string + /** Optional frontend deployment selector for embedded Usage and Top-up pages. */ + embeddedPageDist?: string /** Allow HTTP only on loopback for the development Mock. */ allowLoopbackHttp?: boolean /** Map authorization and completion pages to platformOrigin for private development proxies. */ @@ -43,6 +45,7 @@ export interface Config { /** Validated deployment choices. */ export const Config = Schema.object({ platformOrigin: Schema.string().default('https://platform.deepseek.com'), + embeddedPageDist: Schema.string().default(''), allowLoopbackHttp: Schema.boolean().default(false), rewriteBrowserOrigin: Schema.boolean().default(false), requestHeaders: Schema.dict(Schema.string().role('secret')).default({}), @@ -72,6 +75,7 @@ export class PlatformAccount extends DeepSeekAccount { static inject = ['credentials', 'authorization'] static Config = Config private readonly origin: string + private readonly embeddedPageDist: string private readonly rewriteBrowserOrigin: boolean private readonly requestHeaders: Record private readonly accountRequestHeaders: Record @@ -90,6 +94,7 @@ export class PlatformAccount extends DeepSeekAccount { constructor(ctx: Context, config: Config = {}) { super(ctx) const resolved = Config(config) + this.embeddedPageDist = resolved.embeddedPageDist this.origin = platformOrigin(resolved.platformOrigin, resolved.allowLoopbackHttp) this.rewriteBrowserOrigin = resolved.rewriteBrowserOrigin this.requestHeaders = platformHeaders(resolved.requestHeaders) @@ -177,6 +182,7 @@ export class PlatformAccount extends DeepSeekAccount { if (!parsed.success) throw new PlatformAuthError('storage') if (parsed.data.issuer !== this.origin) throw new PlatformAuthError('protocol') return { origin: this.origin, token: parsed.data.token, + ...(this.embeddedPageDist ? { embeddedPageDist: this.embeddedPageDist } : {}), ...(Object.keys(this.accountRequestHeaders).length ? { requestHeaders: { ...this.accountRequestHeaders } } : {}) } } diff --git a/packages/credentials/deepseek-account-platform/tests/account.spec.ts b/packages/credentials/deepseek-account-platform/tests/account.spec.ts index 2130d82c59..655f334a83 100644 --- a/packages/credentials/deepseek-account-platform/tests/account.spec.ts +++ b/packages/credentials/deepseek-account-platform/tests/account.spec.ts @@ -18,7 +18,7 @@ afterEach(async () => { while (cleanups.length) await cleanups.pop()!() }) async function fixture(contact: { email: string; mobile?: string; mobile_number?: string } = { email: 't***@example.invalid', mobile: '138****5678', -}, requestHeaders: Record = {}, rewriteBrowserOrigin = false, accountRequestHeaders: Record = {}) { +}, requestHeaders: Record = {}, rewriteBrowserOrigin = false, accountRequestHeaders: Record = {}, embeddedPageDist = '') { const home = await mkdtemp(join(tmpdir(), 'dsh-account-')) cleanups.push(() => rm(home, { recursive: true, force: true })) let init: Record = {} @@ -116,7 +116,8 @@ async function fixture(contact: { email: string; mobile?: string; mobile_number? const authorization = ctx.plugin(AuthorizationService) await authorization const provider = ctx.plugin(PlatformAccount, { - platformOrigin: origin, allowLoopbackHttp: true, requestHeaders, accountRequestHeaders, rewriteBrowserOrigin, logoutRetryDelayMs: 1, + platformOrigin: origin, embeddedPageDist, allowLoopbackHttp: true, requestHeaders, accountRequestHeaders, + rewriteBrowserOrigin, logoutRetryDelayMs: 1, }) await provider cleanups.push(async () => { await provider.dispose(); await authorization.dispose(); await credentials.dispose(); await web.dispose() }) @@ -611,3 +612,11 @@ it('overlays account cookies without changing authorization or logout routing', } expect(JSON.stringify(await f.account.getState())).not.toContain('private') }) + +it('carries the configured embedded frontend selector in the private Platform session', async () => { + const f = await fixture(undefined, {}, false, {}, 'feat/test') + await f.account.startSignIn('en', f.callbackOrigin, 'desktop') + await f.wait('waiting-browser') + await fetch(f.callback(), { redirect: 'manual' }) + expect(await f.account.getPlatformSession()).toEqual({ origin: f.origin, token: 'dsh_mock_test', embeddedPageDist: 'feat/test' }) +}) diff --git a/packages/credentials/deepseek-account/src/index.ts b/packages/credentials/deepseek-account/src/index.ts index 00dcf9446e..e91f07d83a 100644 --- a/packages/credentials/deepseek-account/src/index.ts +++ b/packages/credentials/deepseek-account/src/index.ts @@ -13,6 +13,8 @@ declare module '@deepseek-ai/cordis' { export interface PlatformSession { readonly origin: string readonly token: string + /** Optional dist query value selecting the embedded frontend deployment. */ + readonly embeddedPageDist?: string /** Private deployment headers for native requests; excluded from renderer bootstrap. */ readonly requestHeaders?: Readonly> }