diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml new file mode 100644 index 0000000000..e2fb6b15cf --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md +2026-09-14-deepseek-account-login.md: d570966b4758786195d5b3e43fff404d3d005bba +2026-09-14-deepseek-account-login.zh.md: 4b79bee13c855729088463d8b3a43cc785776a01 diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md new file mode 100644 index 0000000000..d570966b47 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md @@ -0,0 +1,55 @@ +# Agent Note: DeepSeek account browser login + +Status: implemented + +English | [中文](2026-09-14-deepseek-account-login.zh.md) + +Sign-in captures the initiating UI language per attempt so browser authorization follows the Desktop or Settings locale without depending on the Platform default. + +## Problem + +Desktop login must preserve local cancellation across the browser, UI, and Host and report success only after credentials are saved. + +## Decision + +The account Service Definition separates UI and model consumers from the platform protocol implementation. The platform provider registers an AuthorizationFlow and persists an owner-scoped GrantRecord. Account presence is derived from storage. Login attempts stay in memory and expose safe snapshots through the account Remote controller. + +Local cancellation is authoritative. The provider stops callbacks and ignores late exchange results; auth_cancel invalidates the remote application and unexchanged code using request_id and the original PKCE verifier. Its background request does not delay local cancellation, and failure never restores login. session.commit admits persistence synchronously before its first await and rejects an already-cancelled flow. Cancellation after admission waits for the commit result. The callback redirects to auth_exchange.biz_data.authorized_url only after authorization settlement confirms storage. + +Authorization registers a temporary /oauth/callback route on the Host webServer. The authenticated initiating client supplies its browser-accessible loopback HTTP origin, including any local SSH forwarding port. Non-loopback reverse proxies are unsupported. Cleanup removes only the route and preserves shared connections. Exchange failure returns Web to its login UI or focuses Desktop through account state; retry is explicit. PKCE secrets never cross UI transports. Model and file request tokens resolve only for https://api.deepseek.com; redirects are refused. Development loopback grants cannot authenticate production requests. API-key references remain separately stored. + +Platform navigation and authorization share one validated platformOrigin from Cordis configuration. Private profile patches or environment expressions supply deployment-specific origins without committing development addresses. + +## Alternatives considered + +Putting the protocol in Electron duplicates the implementation for Web consumers and gives the shell credential ownership. Putting tokens in DEEPSEEK_API_KEY loses the distinction between account grants and user API keys. Treating a browser callback as success before persistence admits false success. Waiting for a remote cancel acknowledgment leaves local cancellation dependent on network availability. These alternatives are rejected. + +## Consequences + +Sign out removes the local grant before starting Platform POST /auth-api/v0/users/logout with the captured token. Remote failure never restores login. The provider retries at most five times after the initial request, with configurable exponential backoff starting at one second by default. Retry jobs retain only their old token, cannot change later login state, and end at provider shutdown without durable storage. Local credential deletion must succeed before publishing signed-out state. Sign out belongs to the sidebar account menu; Account settings owns profile, balance and sign-in. Both read one plugin-owned Host stream through framework hooks. + +API keys and account grants remain separately stored. Account tokens have no expiry or refresh flow; exhausted logout retries can leave the removed token valid remotely. Profile and balance query failures preserve login state; authorization-attempt deadlines apply only before token issuance. The same grant authenticates Platform current and get_user_summary queries on its configured issuer origin. Host preserves Platform-masked contact details and drops response tokens; account changes invalidate pending results. UI shows normal_wallets recharge balances without adding bonus wallets. + +The account plugin supplies choice, waiting, failure and timeout dialogs through settings.models.sign-in. The model package owns credential readiness and the existing API-key editor; the settings shell coordinates explicit reopening so login and API-key onboarding do not mount competing dialogs. + +Platform embedding transfers a grant from Host to Electron over private Node IPC and then once to the trusted Platform main frame through a sandboxed preload. Preload performs one synchronous IPC before page scripts execute; the main process only validates the sender and returns prepared memory. Platform reads the token synchronously thereafter without a readiness API. A failed initialization retains embedded mode and a throwing getter. Account UI projections remain credential-free. The Platform document can read this credential, so its script security is part of account protection; context isolation protects native capabilities, not a token intentionally returned to the document. Replacing or removing the grant destroys the document and its temporary session. + +Support questionnaires receive only the Platform UID and available environment metadata through explicit prefill fields. Account tokens and masked contact details stay out of questionnaire URLs. The account provider projects the current-user UID for this purpose; the UI uses the latest account snapshot so sign-out cannot reuse a previous UID. + +Private proxy development can explicitly map authorization and completion URLs to platformOrigin with rewriteBrowserOrigin. Both pages retain their fixed paths and full query strings; allowing a remote origin without mapping it would send the browser outside the configured environment. Shipped configuration requires same-origin URLs. + +DSH grants authenticate Platform, inference and Files requests with x-dsh-auth-token, without a Bearer prefix. API-key authentication remains protocol-specific. + +Profile and recharge-wallet queries use independent getProfile and getBalance operations. The client publishes each result as it arrives, so a slow or failed balance request cannot delay the sidebar username. Account changes invalidate both pending results. + +## Verification + +Provider tests exercise real loopback callbacks, invalid state, delayed exchange cancellation, credential persistence, sign-out, and official-origin restrictions. Desktop tests cover the native action bridge and localized entry. Manual development integration uses the platform dev middleware Mock and the real Electron Host, including cancellation before browser approval. Production backend credentials and installer scheme registration require release-environment validation. + +## Related + +[Credential records and flows](2026-08-13-credential-records-and-authorization-flows.md) remains the generic credential authority. [Desktop wrapper](2026-09-10-desktop-web-wrapper.md) owns the transport composition. + +Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider. + +The Host sends client_type (desktop or web) in auth_init so Platform can choose the completion experience. Web failures close the authorization tab while the original tab receives Host state; no Web UI return URL is used. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal. diff --git a/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md new file mode 100644 index 0000000000..4b79bee13c --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.zh.md @@ -0,0 +1,55 @@ +# Agent Note: DeepSeek 账号浏览器登录 + +Status: implemented + +[English](2026-09-14-deepseek-account-login.md) | 中文 + +登录申请保存发起界面的语言,使浏览器授权跟随桌面或设置页的语言,不依赖平台默认值。 + +## 问题 + +桌面登录必须在浏览器、UI 和 Host 之间保留本地取消意图,并只在凭证保存后宣告成功。 + +## 决策 + +账号服务定义将 UI 和模型使用方与平台协议实现分开。平台提供者注册 AuthorizationFlow,并保存按所有者寻址的 GrantRecord。账号是否存在从存储派生。登录尝试保留在内存中,通过账号 Remote 控制器提供安全快照。 + +本地取消具有决定权。提供者停止回调并忽略迟到的兑换结果;auth_cancel 使用 request_id 和原始 PKCE verifier 作废远程申请及未兑换的 code。后台请求不延迟本地取消,失败也不会恢复登录。session.commit 在第一次 await 前同步准入持久化,并拒绝已取消的流程。准入后的取消等待提交结果。回调只有在授权结束确认存储后才跳转 auth_exchange.biz_data.authorized_url。 + +授权在 Host webServer 上注册临时 /oauth/callback 路由。已鉴权的发起客户端提供浏览器可访问的本机 HTTP 来源,包含 SSH 本地转发端口。不支持非本机域名反向代理。清理仅移除路由,保留共享连接。兑换失败返回 Web 登录界面,或通过账号状态聚焦 Desktop;重试由用户明确发起。PKCE 私密数据不经过 UI 传输。模型和文件请求的 token 仅为 https://api.deepseek.com 解析;请求拒绝重定向。本机开发授权记录不能认证生产请求。API Key 引用独立保存。 + +平台导航与授权共享 Cordis 配置中同一个经过校验的 platformOrigin。私有 profile patch 或环境表达式提供部署来源,开发地址不进入仓库。 + +## 替代方案 + +把协议放在 Electron 会为 Web 使用方重复实现,并让外壳拥有凭证。把 token 放入 DEEPSEEK_API_KEY 会丢失账号授权与用户 API Key 的区别。在持久化前将浏览器回调视为成功会产生虚假成功。等待远程取消确认会使本地取消依赖网络。这些方案均被拒绝。 + +## 影响 + +退出登录先删除本地授权,再使用捕获的 token 调用 Platform POST /auth-api/v0/users/logout。远程失败不会恢复登录态。首次请求失败后,提供者最多重试五次,使用可配置的指数退避,默认从一秒开始。重试任务只保留旧 token,不改变后续登录态,并在提供者关闭时结束,不持久化。本地凭证删除成功后才发布已退出状态。退出登录位于侧边栏账号菜单;账号设置负责资料、余额和登录。两者通过框架 hook 读取插件持有的同一条 Host 状态流。 + +API Key 与账号记录独立保存。账号 token 没有过期或刷新流程;退登重试耗尽后,已从本地删除的 token 在远端仍可能有效。资料和余额查询失败保留登录态;授权尝试的有效期仅适用于 token 签发前。同一授权 token 可认证已配置签发来源上的 Platform current 和 get_user_summary 查询。Host 原样保留 Platform 脱敏后的联系方式并丢弃响应 token;账号变化使未完成结果失效。UI 显示 normal_wallets 充值余额,不合并赠送钱包。 + +账号插件通过 settings.models.sign-in 提供选择、等待、失败和超时对话框。模型包保留凭证就绪检查和现有 API Key 编辑器;设置外壳协调显式重开,避免登录与 API Key 引导同时挂载冲突的对话框。 + +Platform 内嵌通过私有 Node IPC 将授权从 Host 传给 Electron,再经沙箱 preload 一次性传给受信任的 Platform 主 frame。preload 在页面脚本执行前执行一次同步 IPC;主进程仅校验调用来源并返回已准备的内存数据。Platform 此后同步读取 token,无需就绪接口。初始化失败时保留内嵌模式,由 getter 抛错。账号 UI 投影仍不包含凭证。Platform 文档能够读取此凭证,因此其脚本安全也是账号保护的一部分;上下文隔离保护原生能力,不能隐藏有意返回给文档的 token。替换或移除授权会销毁文档及其临时会话。 + +支持问卷仅通过显式预填字段接收 Platform UID 和已有的环境信息,URL 不包含账号 token 或脱敏联系方式。账号提供者为此投影 current-user 的 UID;UI 使用最新账号快照,避免退出登录后复用旧 UID。 + +私有代理开发可通过 rewriteBrowserOrigin 显式将授权页和完成页映射到 platformOrigin。两者保留固定路径和完整查询字符串;仅放宽来源而不做映射会让浏览器离开配置的环境。发布配置要求浏览器地址同源。 + +DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请求,不加 Bearer 前缀。API Key 继续使用各协议的认证方式。 + +资料和充值钱包余额分别通过 getProfile 与 getBalance 查询。客户端在各自返回时立即更新,余额请求慢或失败不会延迟侧边栏用户名显示。账号变化使两种进行中的查询结果失效。 + +## 验证 + +提供者测试覆盖真实本机回调、错误 state、延迟兑换取消、凭证持久化、退出和官方来源限制。桌面测试覆盖原生操作桥接与本地化入口。手动开发联调使用平台 dev middleware Mock 和真实 Electron Host,包括在浏览器批准前取消。生产后端凭证及安装器 scheme 注册仍需发布环境验证。 + +## 相关记录 + +[凭证记录与流程](2026-08-13-credential-records-and-authorization-flows.zh.md)仍是通用凭证依据。[桌面外壳](2026-09-10-desktop-web-wrapper.zh.md)负责传输组合。 + +开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。 + +Host 在 auth_init 中发送 client_type(desktop 或 web),供 Platform 选择完成页交互。Web 失败时关闭授权标签页,原标签页接收 Host 状态,不使用 Web UI 返回地址。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index ec698dca33..ccd764f0db 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -69,6 +69,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause | | [`acorn`](https://github.com/acornjs/acorn) | MIT | | [`anser`](https://github.com/IonicaBizau/anser) | MIT | +| [`big.js`](https://github.com/MikeMcl/big.js) | MIT | | [`buffer`](https://github.com/feross/buffer) | MIT | | [`chokidar`](https://github.com/paulmillr/chokidar) | MIT | | [`chrome-devtools-mcp`](https://github.com/ChromeDevTools/chrome-devtools-mcp) | Apache-2.0 | @@ -174,6 +175,7 @@ External packages **directly declared** for development, tests, types, or toolin | [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT | | [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT | | [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/big.js`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/compression`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | diff --git a/apps/cli/composition.md b/apps/cli/composition.md index fb7a946d7c..f6c99c4d79 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -48,6 +48,10 @@ flowchart LR cfg --> plugin_dsh_base_llm_retry plugin_dsh_base_settings["settings
@deepseek-ai/dsh-settings-file"] cfg --> plugin_dsh_base_settings + plugin_dsh_base_authorization["authorization
@deepseek-ai/dsh-authorization"] + cfg --> plugin_dsh_base_authorization + plugin_dsh_base_deepseek_account["deepseek-account
@deepseek-ai/dsh-deepseek-account-platform"] + cfg --> plugin_dsh_base_deepseek_account plugin_dsh_base_credentials["credentials
@deepseek-ai/dsh-credentials-local"] cfg --> plugin_dsh_base_credentials plugin_dsh_base_llm_pi_ai["llm-pi-ai
@deepseek-ai/dsh-llm-pi-ai"] @@ -210,6 +214,8 @@ flowchart LR | `jobs` | `@deepseek-ai/dsh-jobs-local` | | `llm-retry` | `@deepseek-ai/dsh-llm-retry` | | `settings` | `@deepseek-ai/dsh-settings-file` | +| `authorization` | `@deepseek-ai/dsh-authorization` | +| `deepseek-account` | `@deepseek-ai/dsh-deepseek-account-platform` | | `credentials` | `@deepseek-ai/dsh-credentials-local` | | `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` | | `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` | diff --git a/apps/desktop-host/package.json b/apps/desktop-host/package.json index 092e3c2813..fba5fda302 100644 --- a/apps/desktop-host/package.json +++ b/apps/desktop-host/package.json @@ -19,6 +19,7 @@ "@deepseek-ai/dsh-home-paths": "workspace:^", "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/dsh-skill-office": "workspace:^" + "@deepseek-ai/dsh-skill-office": "workspace:^", + "@deepseek-ai/dsh-deepseek-account": "workspace:^" } } diff --git a/apps/desktop-host/src/index.ts b/apps/desktop-host/src/index.ts index 573ef11541..9e8ecfc71c 100644 --- a/apps/desktop-host/src/index.ts +++ b/apps/desktop-host/src/index.ts @@ -5,6 +5,7 @@ import { loadLayeredEnv, loadProfileDirectory } from '@deepseek-ai/dsh-app-boot' import { runProfile } from '@deepseek-ai/dsh/profile-boot' import type {} from '@deepseek-ai/dsh-client-connection' import type {} from '@deepseek-ai/dsh-host-webserver' +import type {} from '@deepseek-ai/dsh-deepseek-account' import { resolveDshHome } from '@deepseek-ai/dsh-home-paths' import * as desktopOffice from './office.ts' @@ -70,6 +71,26 @@ async function main(): Promise { source: process.argv[4] ?? join(runtimeDir, '..', 'runtime', 'primary-runtime'), root: join(resolveDshHome(), 'dsh-runtimes', 'dsh-primary-runtime'), }) + const account = ctx.get('deepseekAccount') + if (account !== undefined) { + const publish = async (): Promise => { + const session = await account.getPlatformSession() + if (process.connected) process.send?.({ type: 'platform-session', session }) + } + await publish() + ctx.effect(() => { + const lifetime = new AbortController() + const updates = (async () => { + for await (const _state of account.watch(lifetime.signal)) { + if (lifetime.signal.aborted) break + await publish() + } + })().catch(() => { + if (process.connected) process.send?.({ type: 'platform-session', session: null }) + }) + return async () => { lifetime.abort(); await updates } + }) + } const url = ctx.connection.authenticatedUrl(`http://127.0.0.1:${String(ctx.webServer.port)}`) if (process.connected) process.send?.({ type: 'ready', url, injections: ctx.webServer.collectIndexInjections() }, (error) => { if (error !== null) console.error(error) }) } diff --git a/apps/desktop-host/tsconfig.json b/apps/desktop-host/tsconfig.json index 89780f7b94..b077063d9b 100644 --- a/apps/desktop-host/tsconfig.json +++ b/apps/desktop-host/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../packages/host/webserver" + }, + { + "path": "../../packages/credentials/deepseek-account" } ] } diff --git a/apps/desktop/electron-builder.config.d.mts b/apps/desktop/electron-builder.config.d.mts index f7cfd63f42..84d4a259ef 100644 --- a/apps/desktop/electron-builder.config.d.mts +++ b/apps/desktop/electron-builder.config.d.mts @@ -3,6 +3,7 @@ import type { AfterPackContext, BeforePackContext } from 'app-builder-lib' /** Electron-builder fields asserted by the Desktop release tests. */ export interface DesktopElectronBuilderConfig { readonly appId: string + readonly protocols: readonly [{ readonly name: 'DeepSeek Harness'; readonly schemes: readonly ['dsh'] }] readonly directories: { readonly output: string } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c060f1c673..0d7e652bad 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -35,7 +35,9 @@ }, "dependencies": { "electron-updater": "^6.8.9", - "semver": "^7.8.5" + "semver": "^7.8.5", + "ws": "^8.21.0", + "@deepseek-ai/dsh-api-gateway": "workspace:^" }, "devDependencies": { "@deepseek-ai/dsh-app-boot": "workspace:^", @@ -54,6 +56,8 @@ "js-yaml": "^4.2.0", "pnpm": "11.7.0", "tar": "^7.5.0", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "@deepseek-ai/dsh-deepseek-account": "workspace:^", + "@types/ws": "^8.18.1" } } diff --git a/apps/desktop/renderer/welcome.css b/apps/desktop/renderer/welcome.css index 2bfd791bb1..f3ae24cb45 100644 --- a/apps/desktop/renderer/welcome.css +++ b/apps/desktop/renderer/welcome.css @@ -282,3 +282,9 @@ body { clip-path: inset(50%); white-space: nowrap; } + +.auth-expired { gap: 12px; padding-top: 28px; } +.auth-expired h1 { font-size: 20px; line-height: 24px; font-weight: 400; letter-spacing: normal; } +.auth-expired p { line-height: 16px; letter-spacing: normal; } + +.welcome.expired-page { padding-bottom: 60px; } diff --git a/apps/desktop/renderer/welcome.html b/apps/desktop/renderer/welcome.html index 83248438c4..886b478e3f 100644 --- a/apps/desktop/renderer/welcome.html +++ b/apps/desktop/renderer/welcome.html @@ -24,8 +24,18 @@ + +
- +