Merge pull request #4650 from deepseek-harness/feat/account-provider-signout

feat(account,models): separate account routing and confirm sign-out impact
This commit is contained in:
lsdsjy authored and GitHub committed 2026-09-23 21:48:20 +08:00
commit bf5eb8a39a
366 files changed
+4392 -1189

No files matched your search

@@ -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 .agents/notes/implemented/architecture/2026-09-14-deepseek-account-login.md
2026-09-14-deepseek-account-login.md: 458e020c2000fd3db5ee9806e99d4b653710cb1e
2026-09-14-deepseek-account-login.zh.md: 2fb59009302309a67e2c3ec99050685ba39b4212
2026-09-14-deepseek-account-login.md: 165cd2d0a2fbe3a4a71d3ce31de69c5005c639b2
2026-09-14-deepseek-account-login.zh.md: 5e09c0a17cc933b46aa6498c70d86fdefd26d21c
@@ -30,7 +30,7 @@ Putting the protocol in Electron duplicates the implementation for Web consumers
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. Provider initialization discards grants from a different configured Platform issuer locally, without remote revocation, so switching environments starts signed out instead of aborting Desktop startup. API keys and device identity survive this cleanup. 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 and positive bonus_wallets in separate rows, so promotional credit is never presented as recharge funds.
API keys and account grants remain separately stored. Account tokens have no proactive refresh flow; exhausted logout retries can leave the removed token valid remotely. Provider initialization discards grants from a different configured Platform issuer locally, without remote revocation, so switching environments starts signed out instead of aborting Desktop startup. API keys and device identity survive this cleanup. Profile and balance HTTP 401 responses clear the rejected credential; other 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 and positive bonus_wallets in separate rows, so promotional credit is never presented as recharge funds.
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.
@@ -30,7 +30,7 @@ Desktop profile 为 Host API 请求头提供原生平台信息。账号与更新
退出登录先删除本地授权,再使用捕获的 token 调用 Platform POST /auth-api/v0/users/logout。远程失败不会恢复登录态。首次请求失败后,提供者最多重试五次,使用可配置的指数退避,默认从一秒开始。重试任务只保留旧 token,不改变后续登录态,并在提供者关闭时结束,不持久化。本地凭证删除成功后才发布已退出状态。退出登录位于侧边栏账号菜单;账号设置负责资料、余额和登录。两者通过框架 hook 读取插件持有的同一条 Host 状态流。
API Key 与账号记录独立保存。账号 token 没有过期或刷新流程;退登重试耗尽后,已从本地删除的 token 在远端仍可能有效。提供者初始化时在本地丢弃与配置的 Platform 来源不同的授权,不调用远端撤销,使环境切换以未登录态启动,而不导致 Desktop 启动失败。API Key 和设备标识保留。资料与余额查询失败保留登录态;授权尝试的有效期仅适用于 token 签发前。同一授权 token 可认证已配置签发来源上的 Platform current 和 get_user_summary 查询。Host 原样保留 Platform 脱敏后的联系方式并丢弃响应 token;账号变化使未完成结果失效。UI 分行显示 normal_wallets 充值余额与正额 bonus_wallets 赠金余额,避免将赠金额度显示为充值资金。
API Key 与账号记录独立保存。账号 token 没有主动刷新流程;退登重试耗尽后,已从本地删除的 token 在远端仍可能有效。提供者初始化时在本地丢弃与配置的 Platform 来源不同的授权,不调用远端撤销,使环境切换以未登录态启动,而不导致 Desktop 启动失败。API Key 和设备标识保留。资料与余额接口的 HTTP 401 响应清除被拒绝的凭据,其他查询错误保留登录态;授权尝试的有效期仅适用于 token 签发前。同一授权 token 可认证已配置签发来源上的 Platform current 和 get_user_summary 查询。Host 原样保留 Platform 脱敏后的联系方式并丢弃响应 token;账号变化使未完成结果失效。UI 分行显示 normal_wallets 充值余额与正额 bonus_wallets 赠金余额,避免将赠金额度显示为充值资金。
账号插件通过 settings.models.sign-in 提供选择、等待、失败和超时对话框。模型包保留凭证就绪检查和现有 API Key 编辑器;设置外壳协调显式重开,避免登录与 API Key 引导同时挂载冲突的对话框。
@@ -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-17-account-provider-signout.md
2026-09-17-account-provider-signout.md: 6b8ef842c54ef25d1e5fc063c40361775aae9a92
2026-09-17-account-provider-signout.zh.md: 588dd38bf9fbae8131a9e3c5cb1181dcc807df8c
@@ -0,0 +1,27 @@
# Agent Note: Account provider and active request cancellation
Status: implemented
English | [中文](2026-09-17-account-provider-signout.zh.md)
## Problem
Selecting credentials from login state changes the billing identity of an existing provider route. Sign-out also needs to stop account-dependent work without classifying requests from settings that may have changed while a request or tool is still running.
## Decision
`deepseek-official` and `deepseek-account` are independent routes sharing the DeepSeek protocol implementation. Each adapter has exactly one credential resolver and authentication header mode. Neither route falls back to the other credential. The account service retains the origin and issuer validation defined by the [login decision](2026-09-14-deepseek-account-login.md).
Account cancellation reads each running Agent’s latest `Session.requestContext()` instead of maintaining another provider field. This logged value remains through tools and retries and follows the next bound request. Idle Agents are excluded. During a new turn’s first preparation it can still name the previous turn’s account route, so sign-out can also interrupt an API-key turn that has not bound its new request yet. A missing account credential rejects a first request without a previous context before transport.
Successful local sign-out publishes an account event. The platform account provider installs the account module’s listener, which enumerates the Agent registry and cancels matching account routes through `Agent.cancel`, preserving inboxes. The account controller shares its predicate for sign-out confirmation. Each registered child is classified independently; existing parent-owned cancellation still applies to children whose lifetime is attached to a cancelled parent. The existing HTTP signal carries cancellation to transport.
## Alternatives considered
A historical set of providers wrongly cancels a turn after it switches to an API-key route. Current settings misclassify an older in-flight request. A second provider field or module-global map duplicates information already recorded in request context. A login-level cancellation signal duplicates the existing Agent and HTTP cancellation chain.
## Consequences
Model selection exposes both routes, and choosing an account route requires login even when an API key exists. The `llm-deepseek-account` and `llm-deepseek-api-key` plugins independently own catalogs, connection settings, and credentials; both use the `llm-deepseek` protocol library. Cancellation preserves queued input without waking it automatically. The recorded hook reason produces a localized conversation notice without changing durable event types.
Behavior tests cover credential separation, first preparation, tool-stage cancellation, and route replacement. The SDK account-provider-signout scenario boots the shipped profile and records interrupted output plus the cancellation cause.
@@ -0,0 +1,27 @@
# Agent Note: 账号提供方与当前请求取消
Status: implemented
[English](2026-09-17-account-provider-signout.md) | 中文
## Problem
根据登录态选择凭证会改变现有提供方路由的计费身份。退登也需要停止依赖账号的任务,不能根据请求或工具运行期间可能已经改变的设置判断归属。
## Decision
`deepseek-official` 和 `deepseek-account` 是独立路由,共享 DeepSeek 协议实现。每个适配器仅有一个凭证解析器和认证请求头模式。两条路由均不回退到另一凭证。账号服务保留[登录决策](2026-09-14-deepseek-account-login.zh.md)定义的来源及签发方校验。
账号取消读取运行中 Agent 最近的 `Session.requestContext()`,不维护额外提供方字段。该已记录值在工具和重试阶段保留,随下一次绑定请求更新;空闲 Agent 不计入。新轮次首次准备期间,它仍可能指向上一轮的账号路由,因此退登也可能打断尚未绑定新请求的 API key 轮次。没有历史上下文的首请求若缺少账号凭据,会在传输前失败。
本地退登成功后发布账号事件。Platform 账号提供方安装账号模块的监听器,遍历 Agent 注册表,通过 `Agent.cancel` 取消匹配的账号路由并保留收件箱。账号控制器的退登确认复用同一判断函数。每个已注册子代理独立分类;生命周期绑定到已取消父代理的子代理仍受现有父级取消规则约束。现有 HTTP 信号将取消传递到传输层。
## Alternatives considered
历史提供方集合会在轮次切换到 API Key 路由后仍错误取消它。当前设置会误判更早的在途请求。额外的提供方字段或模块级全局映射重复保存请求上下文中已有的信息。登录级取消信号重复现有 Agent 与 HTTP 取消链路。
## Consequences
模型选择展示两条路由,即使存在 API Key,选择账号路由仍要求登录。两个路由分别由 `llm-deepseek-account` 和 `llm-deepseek-api-key` 注册,拥有独立的模型目录与连接设置,共用 `llm-deepseek` 协议实现。取消保留排队输入,不自动唤醒。已记录的 hook 原因生成本地化对话提示,无须改变持久事件类型。
行为测试覆盖凭证分离、首请求准备、工具阶段取消及路由替换。SDK account-provider-signout 场景启动随附 profile,记录被中断的输出和取消原因。
+5 -2
View File
@@ -190,8 +190,10 @@ flowchart LR
cfg --> plugin_dsh_base_agent_loop
plugin_dsh_base_fs_sandbox["fs-sandbox<br/>@deepseek-ai/dsh-fs-sandbox"]
cfg --> plugin_dsh_base_fs_sandbox
plugin_dsh_base_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
plugin_dsh_base_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek-api-key"]
cfg --> plugin_dsh_base_llm_deepseek
plugin_dsh_base_llm_deepseek_account["llm-deepseek-account<br/>@deepseek-ai/dsh-llm-deepseek-account"]
cfg --> plugin_dsh_base_llm_deepseek_account
```
| Plugin id | Package / module |
@@ -287,7 +289,8 @@ flowchart LR
| `system-prompt` | `@deepseek-ai/dsh-system-prompt` |
| `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
| `fs-sandbox` | `@deepseek-ai/dsh-fs-sandbox` |
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek-api-key` |
| `llm-deepseek-account` | `@deepseek-ai/dsh-llm-deepseek-account` |
Source config: [`packages/bundle/base/cordis.patch.yml`](../../packages/bundle/base/cordis.patch.yml).
+2
View File
@@ -120,6 +120,8 @@
"@deepseek-ai/dsh-host-webserver": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-llm-mock-server": "workspace:*",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
"@deepseek-ai/dsh-llm-replay": "workspace:*",
+1 -1
View File
@@ -1246,7 +1246,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'],
['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'],
['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'],
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'],
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek-api-key'],
['sandbox', '@deepseek-ai/dsh-sandbox-local'],
['session-projection', '@deepseek-ai/dsh-session-projection'],
['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'],
@@ -1,7 +1,7 @@
# Keyless generic ACP v1 control-surface patch over `dsh --profile acp`.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
disabled: true
- id: acp
@@ -2,7 +2,7 @@
# local provider endpoint is supplied by the snapshot test; the real attachment
# store and ACP bridge carry two uploaded images into one model request.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
config:
apiKeyEnv: DSH_SNAPSHOT_API_KEY
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
@@ -9,6 +9,9 @@
- id: llm-deepseek
disabled: true
- id: llm-deepseek-account
disabled: true
- id: agent-loop
config:
agents:
@@ -24,6 +27,6 @@
- insert:
- id: llm-deepseek-keyless
name: '@deepseek-ai/dsh-llm-deepseek'
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
config:
baseURL: 'http://127.0.0.1:9'
@@ -8,7 +8,7 @@ import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import TokenMeter from '@deepseek-ai/dsh-token-meter'
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
@@ -17,7 +17,7 @@ import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
import NodeRuntime from '@deepseek-ai/dsh-ptc-runtime-node'
import Sandbox from '@deepseek-ai/dsh-sandbox-local'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
@@ -20,6 +20,7 @@ class Account extends DeepSeekAccount {
override async startSignIn() { return state }
override async cancelSignIn() { return state }
override async signOut() { return state }
override async rejectToken() {}
override async resolveToken() { return undefined }
override async getPlatformSession() { this.reading.resolve(undefined); return this.read() }
override async *watch(signal: AbortSignal) {
+2 -2
View File
@@ -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 apps/desktop/README.md
README.md: ec3f2f44d5e49db00bd595588a0594bcee0a1538
README.zh.md: 50fba340a0edc114a75d4b6363b8e6c42be0c45f
README.md: fb83b0fc66c281a596d743b561e0ee33f32b2c04
README.zh.md: 490eff4b35a3e7b52108ff529ed9d3b432f68bb3
+7 -1
View File
@@ -14,6 +14,8 @@ Embedded Platform documents use a dedicated non-persistent WebContentsView sessi
Desktop Host Platform API requests use the same `x-client-platform` mapping as update-policy requests. The account provider owns [API-only header configuration](../../packages/credentials/deepseek-account-platform/README.md#use-this-package).
A server-expired account credential returns to Welcome when no official API key is configured; an available API key keeps the workspace open. Explicit sign-out follows the same rule. Both Welcome and the workspace display the localized expiry notice.
Desktop microphone access is restricted to audio requests from the primary `dsh-app://app` frame. macOS uses system microphone authorization and a packaged usage description.
Press F12 (Fn+F12 on media-key keyboards), Command+Option+I on macOS, or Ctrl+Shift+I on Windows to toggle DevTools for the focused application page, including in packaged builds. These native shortcuts use hidden application-menu items. Update overlays and packaged embedded browser guests disable DevTools.
@@ -50,6 +52,8 @@ Node prepares the bundled interpreters and Python libraries without a system Pyt
The [thin-wrapper decision](../../.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.md) owns shared Web behavior and Desktop adapters. The [Electron packaging and update decision](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md) owns release identity, signing, and update qualification.
Welcome loads the shared Toast palette and shadow tokens, with system typography for body-mounted notifications.
## Installation ownership
Electron owns `$DSH_HOME/profiles/desktop`. Its `dependencies` contains packages installed by pnpm; `dsh.profile.bundles` contains the built-in bundles followed by enabled plugins. The signed application supplies dsh, the private Desktop Host, and their production packages from `resources/app.asar/dsh`. Packaged applications select runtime profile resolution without creating package links; development profiles use filesystem links. Both host and plugins execute in the same Electron Node-mode process; Desktop does not enable `--preserve-symlinks`. The CLI cannot boot or mutate this profile.
@@ -410,7 +414,9 @@ An unpackaged Electron process uses `.desktop-build/development/project` under i
- The desktop shell shares sessions, settings, credentials, workspaces, and storage under `$DSH_HOME` with CLI dsh, while executable packages, plugin activation, and lockfiles remain separate.
- Unpackaged startup on an Electron win32-arm64 host now succeeds, but the payload remains x64: the architecture check in `packages/skill/tool-workspace-dependencies/src/index.ts` compares the recorded payload architecture against the host `process.arch`, so the `load_workspace_dependencies` tool can still reject the primary runtime.
Sign in opens the configured platform page in the system browser. The Host owns PKCE and a temporary loopback callback, saves the credential before entering the workspace, and redirects the browser to the platform completion page. Opened and copied authorization links carry the effective Desktop theme as `theme=light` or `theme=dark`; `system` resolves at the time of the action. Cancel withdraws the local attempt even if the platform page later approves it. Settings offers Account sign-out; without a separate API key, sign-out returns to the welcome window. Packaged applications register dsh://open to show the window without passing credentials. On macOS, the development launcher prepares an ad-hoc-signed `Harness Dev.app` under `.desktop-build/development`, declares `dsh` in its Info.plist, and registers it with Launch Services. It loads the current workspace and records the selected development home, browser-data path, and debug settings for cold starts. Starting this bundle makes it the default `dsh://` handler; starting the packaged application registers the packaged handler again. The generated bundle does not contain account tokens and requires the workspace and prepared runtime to remain available.
Sign in opens the configured platform page in the system browser. The Host owns PKCE and a temporary loopback callback, saves the credential before entering the workspace, and redirects the browser to the platform completion page. Opened and copied authorization links carry the effective Desktop theme as `theme=light` or `theme=dark`; `system` resolves at the time of the action. Cancel withdraws the local attempt even if the platform page later approves it. Settings offers Account sign-out; without a separate API key, sign-out returns to the welcome window. Successful browser sign-in switches Welcome to the workspace without activating the application; the completion page’s dsh://open link brings it to the foreground. Packaged applications register dsh://open to show the window without passing credentials. On macOS, the development launcher prepares an ad-hoc-signed `Harness Dev.app` under `.desktop-build/development`, declares `dsh` in its Info.plist, and registers it with Launch Services. It loads the current workspace and records the selected development home, browser-data path, and debug settings for cold starts. Starting this bundle makes it the default `dsh://` handler; starting the packaged application registers the packaged handler again. The generated bundle does not contain account tokens and requires the workspace and prepared runtime to remain available.
When an expired account returns to Welcome, the main process retains a one-time notification until the renderer requests it over window-owned IPC. Reloading Welcome does not repeat the notice; manual sign-out and cold startup do not create it.
An expired login displays a timeout heading with explicit Sign in again and Add API Key actions. Opening the API-key form dismisses the authorization view; late account-state notifications do not replace an in-progress key entry.
+7 -1
View File
@@ -14,6 +14,8 @@ Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置
Desktop Host 的 Platform API 请求与更新策略请求使用相同的 `x-client-platform` 映射。账号 provider 管理[仅 API 使用的请求头配置](../../packages/credentials/deepseek-account-platform/README.zh.md#use-this-package)。
账号凭据被服务端判定失效后,未配置官方 API key 时返回 Welcome;有可用 API key 时保持工作区打开。主动退出登录遵循相同规则。Welcome 和工作区均显示本地化的登录失效提示。
桌面麦克风访问仅允许主 `dsh-app://app` 页面发起的音频请求。macOS 使用系统麦克风授权与随包用途说明。
按 F12(多媒体功能键键盘上为 Fn+F12)、macOS 的 Command+Option+I 或 Windows 的 Ctrl+Shift+I,可切换当前获得焦点的应用页面的 DevTools,打包版本同样支持。这些原生快捷键通过隐藏的应用菜单项注册。更新遮罩和打包版本的内嵌浏览器禁用 DevTools。
@@ -50,6 +52,8 @@ Node 准备内置解释器和 Python 库,无需系统 Python 或 pip。[下载
[薄壳决策](../../.agents/notes/implemented/architecture/2026-09-10-desktop-web-wrapper.zh.md)负责共享 Web 行为与 Desktop 适配。[Electron 打包与更新决策](../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md)负责发布身份、签名及更新验收。
Welcome 加载共享 Toast 的配色和阴影变量,挂载在 body 下的通知使用系统字体。
## 安装归属
Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 安装的包;`dsh.profile.bundles` 包含内置 bundle,后接已启用插件。签名应用从 `resources/app.asar/dsh` 提供 dsh、私有 Desktop Host 及其生产依赖。打包应用选择 runtime profile 解析,不创建包链接;开发 profile 使用文件系统链接。宿主与插件在同一个 Electron Node 模式进程中执行;Desktop 不启用 `--preserve-symlinks`。CLI 不能启动或修改此 profile。
@@ -410,7 +414,9 @@ node apps/desktop/node_modules/pnpm/bin/pnpm.mjs --dir apps/desktop run test:upd
- 桌面壳与 CLI dsh 共享 `$DSH_HOME` 下的会话、设置、凭据、工作区和存储,但可执行包、插件激活和锁文件彼此隔离。
- 在 Electron win32-arm64 宿主上,未打包启动现在可以成功,但载荷仍为 x64:`packages/skill/tool-workspace-dependencies/src/index.ts` 的架构校验会把载荷记录的架构与宿主 `process.arch` 比较,因此 `load_workspace_dependencies` 工具仍可能拒绝 primary runtime。
登录会在系统浏览器中打开配置的平台页面。Host 负责 PKCE 和临时本机回调,在进入工作区前保存凭证,再将浏览器跳转到平台完成页。打开和复制的授权链接通过 `theme=light` 或 `theme=dark` 携带当前生效的 Desktop 主题;`system` 在执行操作时解析。即使平台页面随后批准,取消仍会撤销本地尝试。设置中的账号页面提供退出;没有独立 API Key 时,退出后返回欢迎窗。打包应用注册 dsh://open,只显示窗口而不传递凭证。macOS 开发启动器在 `.desktop-build/development` 下准备经临时签名的 `Harness Dev.app`,在 Info.plist 中声明 `dsh` 并注册到 Launch Services。它加载当前工作区,并记录选定的开发 home、浏览器数据路径和调试设置,以供冷启动使用。启动此应用会将其设为 `dsh://` 默认处理程序;启动打包应用会重新注册打包版处理程序。生成的应用包不包含账号 token,依赖工作区和已准备的运行环境继续存在。
登录会在系统浏览器中打开配置的平台页面。Host 负责 PKCE 和临时本机回调,在进入工作区前保存凭证,再将浏览器跳转到平台完成页。打开和复制的授权链接通过 `theme=light` 或 `theme=dark` 携带当前生效的 Desktop 主题;`system` 在执行操作时解析。即使平台页面随后批准,取消仍会撤销本地尝试。设置中的账号页面提供退出;没有独立 API Key 时,退出后返回欢迎窗。浏览器登录成功后,Welcome 切换到工作区但不激活应用;完成页的 dsh://open 链接负责将客户端置前。打包应用注册 dsh://open,只显示窗口而不传递凭证。macOS 开发启动器在 `.desktop-build/development` 下准备经临时签名的 `Harness Dev.app`,在 Info.plist 中声明 `dsh` 并注册到 Launch Services。它加载当前工作区,并记录选定的开发 home、浏览器数据路径和调试设置,以供冷启动使用。启动此应用会将其设为 `dsh://` 默认处理程序;启动打包应用会重新注册打包版处理程序。生成的应用包不包含账号 token,依赖工作区和已准备的运行环境继续存在。
账号失效并返回 Welcome 时,主进程保留一次性通知,直到渲染器通过所属窗口的 IPC 领取。重新加载 Welcome 不会重复提示;主动退登和冷启动不会生成该通知。
登录超时后显示超时标题,并提供重新登录和添加 API Key 按钮。打开 API Key 表单会关闭授权视图;后续账号状态通知不会覆盖正在填写的密钥。
+6 -3
View File
@@ -1,7 +1,7 @@
@import url('./assets/window-material.css');
/* Figma 2121:39336 / 2138:44626, Platform modes. Native material supplies the blur. */
:root {
body {
color-scheme: light dark;
--dsw-alias-label-primary: #0f1115;
--dsw-alias-label-secondary: #61666b;
@@ -23,7 +23,7 @@
}
@media (prefers-color-scheme: dark) {
:root {
body {
--dsw-alias-label-primary: #f9fafb;
--dsw-alias-label-secondary: #cfd3d6;
--dsw-alias-label-tertiary: #adb2b8;
@@ -52,6 +52,8 @@ body {
margin: 0;
overflow: hidden;
background: transparent;
font-family: var(--dsw-font-family);
font-weight: 400;
}
#root {
@@ -192,6 +194,7 @@ body {
top: calc(100% + 16px);
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
min-width: 58px;
height: 28px;
padding: 4px 10px;
@@ -245,7 +248,7 @@ body {
.key-heading h1 {
margin: 0;
font-size: 20px;
font-weight: 300;
font-weight: 400;
line-height: 32px;
letter-spacing: 0.01em;
}
+1 -1
View File
@@ -6,8 +6,8 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self'; base-uri 'none'; form-action 'none'">
<title></title>
<link rel="stylesheet" href="../lib/welcome/brand-font.css">
<link rel="stylesheet" href="welcome.css">
<link rel="stylesheet" href="../lib/welcome/welcome.css">
<link rel="stylesheet" href="welcome.css">
<script src="../lib/welcome/welcome.js" defer></script>
</head>
<body>
+14 -4
View File
@@ -63,8 +63,13 @@ export interface DesktopAccountBackend {
cancel(id: SignInAttemptId): Promise<AccountView>
/** @returns state after local sign-out. */
signOut(): Promise<AccountView>
/** @param listener - state recipient. @param failed - stream failure recipient. @returns stream disposer. */
watch(listener: (state: AccountView) => void, failed: () => void): () => void
/**
* @param listener - state recipient.
* @param failed - stream failure recipient.
* @param expired - live credential-expiry recipient.
* @returns stream disposer.
*/
watch(listener: (state: AccountView) => void, failed: () => void, expired: () => void): () => void
}
/**
@@ -80,22 +85,27 @@ export function desktopAccountBackend(origin: string, invoke: AccountInvoke, coo
return {
state: () => call('getState'), start: locale => call('startSignIn', { locale, callbackOrigin: new URL(origin).origin, loginSource: 'desktop' }),
cancel: attemptId => call('cancelSignIn', { attemptId }), signOut: () => call('signOut'),
watch(listener, failed) {
watch(listener, failed, expired) {
let closed = false
let socket: WebSocket | undefined
let retry: ReturnType<typeof setTimeout> | undefined
const connect = (): void => {
const streamId = randomUUID()
const expiryStreamId = randomUUID()
void cookies().then((cookie) => {
if (closed) return
const url = new URL(REMOTE_STREAM_MUX_PATH, origin)
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
socket = new WebSocket(url, { headers: { cookie, origin }, maxPayload: 65_536 })
socket.on('open', () => socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } })))
socket.on('open', () => {
socket?.send(JSON.stringify({ type: 'open', streamId: expiryStreamId, endpoint: 'account/watchExpiry', payload: { args: {} } }))
socket?.send(JSON.stringify({ type: 'open', streamId, endpoint: 'account/watch', payload: { args: {} } }))
})
socket.on('message', (data) => {
try {
const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data)
const frame = parseRemoteStreamServerMessage(bytes.toString('utf8'))
if (frame.streamId === expiryStreamId && frame.type === 'item' && frame.value === 'session-expired') { expired(); return }
if (frame.streamId !== streamId) throw new Error('desktop account: unexpected stream')
if (frame.type === 'item') listener(accountView(frame.value))
else socket?.close()
+14 -1
View File
@@ -1,6 +1,7 @@
/** Desktop welcome presentation; account and credential operations stay in the preload. */
import { useEffect, useRef, useState } from 'react'
import type { FormEvent } from 'react'
import { Toast } from '@deepseek-ai/dsh-client-ui-primitives/src/Toast.tsx'
import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives/src/StateDot.tsx'
import type { AccountView } from '@deepseek-ai/dsh-deepseek-account/types'
import type { WelcomeApi } from '../welcome-api.ts'
@@ -9,11 +10,13 @@ type Page = 'entry' | 'key' | 'account'
/**
* Render the standalone welcome flow using shell-owned operations and localized copy.
* Clearing the account attempt returns the sign-in status page to the initial choices.
* @param props.api - isolated preload API; no account credentials reach the renderer.
* @returns welcome pages with fixed bottom actions.
*/
export function Welcome({ api }: { api: WelcomeApi }) {
const { messages: m } = api
const [expiryNotice, setExpiryNotice] = useState(false)
const [page, setPage] = useState<Page>('entry')
const pageRef = useRef<Page>('entry')
const [attempt, setAttempt] = useState<AccountView['attempt']>(null)
@@ -42,15 +45,24 @@ export function Welcome({ api }: { api: WelcomeApi }) {
setAttempt(state.attempt)
setStarting(false)
setCopyFeedback({ status: 'idle' })
navigate(state.attempt?.phase === 'cancelled' ? 'entry' : 'account')
navigate(state.attempt === null || state.attempt.phase === 'cancelled' ? 'entry' : 'account')
}
useEffect(() => {
mounted.current = true
document.documentElement.lang = api.id
document.title = m.welcomeTitle
const takeNotice = (): void => {
void api.takeNotice().then((notice) => {
if (mounted.current && notice === 'session-expired') setExpiryNotice(true)
}).catch((_closedChannel: unknown) => {
// A closed Welcome IPC channel must not interrupt the sign-in page.
})
}
takeNotice()
const stop = api.onAccountState((state) => {
revision.current++
takeNotice()
showAccount(state)
})
return () => { mounted.current = false; stop() }
@@ -156,6 +168,7 @@ export function Welcome({ api }: { api: WelcomeApi }) {
const heading = page === 'entry' ? 'welcome-heading' : page === 'key' ? 'key-title' : 'auth-status'
return <>
{expiryNotice && <Toast text={m.welcomeSessionExpired} onDone={() => { setExpiryNotice(false) }} />}
<div className="titlebar" aria-hidden="true" />
<main className="welcome" aria-labelledby={heading}>
<img className="brand" src="assets/welcome-brand.svg" alt={m.welcomeBrand} width="472" height="40" />
+2
View File
@@ -0,0 +1,2 @@
/** Stylesheets bundled by the standalone Welcome renderer. */
declare module '*.css'
+8
View File
@@ -1,4 +1,6 @@
/** Mount the local React renderer before Electron reveals the welcome window. */
import '@deepseek-ai/dsh-client-ui-theme/src/styles/design-platform.css'
import '@deepseek-ai/dsh-client-ui-theme/src/styles/gradient-shadow-text.css'
import { createRoot } from 'react-dom/client'
import { flushSync } from 'react-dom'
import { Welcome } from './WelcomePage.tsx'
@@ -10,6 +12,12 @@ declare global {
}
}
const palette = window.matchMedia('(prefers-color-scheme: dark)')
const syncPalette = (): void => { document.body.toggleAttribute('data-ds-dark-theme', palette.matches) }
syncPalette()
palette.addEventListener('change', syncPalette)
window.addEventListener('pagehide', () => { palette.removeEventListener('change', syncPalette) }, { once: true })
const container = document.getElementById('root')
if (container === null) throw new Error('desktop welcome: missing React root')
const root = createRoot(container)
+2
View File
@@ -54,6 +54,7 @@ export const en = {
welcomeKeySave: 'Save and continue',
welcomeKeyLater: 'Set up later',
welcomeKeyBack: 'Back to sign in',
welcomeSessionExpired: 'You have signed out of your account, please log in again.',
welcomeKeyBlank: 'Enter an API key.',
welcomeKeyInvalid: 'Enter the API key itself, without quotes, spaces, or an environment-variable assignment.',
welcomeKeyFailed: 'Could not save the API key. Please try again.',
@@ -184,6 +185,7 @@ export const zh = {
welcomeKeySave: '保存并继续',
welcomeKeyLater: '稍后配置',
welcomeKeyBack: '返回登录',
welcomeSessionExpired: '登录信息已失效,请重新登录',
welcomeKeyBlank: '请输入 API 密钥。',
welcomeKeyInvalid: '请仅输入 API 密钥,不要包含引号、空格或环境变量赋值。',
welcomeKeyFailed: '无法保存 API 密钥,请重试。',
+29 -8
View File
@@ -35,7 +35,7 @@ import { serveWebDocument, authenticateWebHost, forwardWebRequest } from './web-
import { DesktopFatalRecovery } from './fatal-recovery.ts'
import { pruneCrashReports, RendererConsoleTail, writeCrashReport, type CrashReportSource } from './crash-report.ts'
import { openWelcomeWindow } from './welcome-window.ts'
import { WELCOME_IPC, needsWelcome } from './welcome-api.ts'
import { WELCOME_IPC, needsWelcome, type WelcomeNotice } from './welcome-api.ts'
import { connectDesktopWelcome, type DesktopWelcomeBackend } from './welcome-backend.ts'
import { DesktopUpdateJournal } from './update-journal.ts'
import { DesktopUpdatePreparationError } from './update-error.ts'
@@ -352,6 +352,7 @@ async function main(): Promise<void> {
let stopAccount: (() => void) | undefined
let openedAttempt: string | undefined
let returnedAttempt: string | undefined
let pendingWelcomeNotice: WelcomeNotice | undefined
let previousAccountStatus: string | undefined
const assertProductSender = (event: IpcMainInvokeEvent): void => {
assertDesktopSender(event, ['app'])
@@ -392,7 +393,8 @@ async function main(): Promise<void> {
injections = ready.injections
welcomeBackend = await connectDesktopWelcome(ready.url, (input, init) => net.fetch(input, init), async () => (await session.defaultSession.cookies.get({ url: ready.url })).map(cookie => `${cookie.name}=${cookie.value}`).join('; '))
stopAccount?.()
stopAccount = welcomeBackend.account.watch((state) => {
const accountBackend = welcomeBackend.account
stopAccount = accountBackend.watch((state) => {
if (quitting) return
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)
const attempt = state.attempt
@@ -404,16 +406,29 @@ async function main(): Promise<void> {
returnedAttempt = attempt.id
focusPrimaryWindow()
}
if (attempt?.phase === 'succeeded' && welcomeWindow !== undefined) void enterWorkspace().catch(() => undefined)
if (state.status === 'credential-stored' && attempt?.phase === 'succeeded' && welcomeWindow !== undefined) void enterWorkspace({ activate: false }).catch(() => undefined)
if (previousAccountStatus === 'credential-stored' && state.status === 'signed-out') {
void readWelcomeState().then((value) => {
if (!value.hasApiKey && !quitting) { enteredWorkspace = false; return showWelcome() }
void readWelcomeState().then(async (value) => {
if (needsWelcome(value) && !quitting) {
enteredWorkspace = false
await showWelcome()
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)
}
return undefined
}).catch(() => undefined)
}
previousAccountStatus = state.status
}, () => {
// The stream reconnects; a transport failure does not change account state.
}, () => {
void readWelcomeState().then(async (value) => {
if (!needsWelcome(value) || quitting) return
pendingWelcomeNotice = 'session-expired'
enteredWorkspace = false
await showWelcome()
const state = await accountBackend.state()
if (welcomeWindow !== undefined && !welcomeWindow.isDestroyed()) welcomeWindow.webContents.send(WELCOME_IPC.state, state)
}).catch(() => undefined)
})
},
stop: async () => {
@@ -928,19 +943,20 @@ async function main(): Promise<void> {
})
return window
}
const enterWorkspace = async (): Promise<void> => {
const enterWorkspace = async ({ activate = true }: { activate?: boolean } = {}): Promise<void> => {
if (quitting) return
const window = mainWindow ?? createMainWindow()
await navigateMain(applicationUrl)
if (isQuitting() || recovery.active || window.isDestroyed()) return
window.show()
if (activate) window.show()
else window.showInactive()
enteredWorkspace = true
if (welcomeWindow !== undefined) {
welcomeWindow.close()
window.webContents.send(DESKTOP_IPC.enterWorkspace)
}
welcomeWindow = undefined
if (development && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== '0') {
if (activate && development && process.env.DSH_DESKTOP_OPEN_DEVTOOLS !== '0') {
window.webContents.openDevTools({ mode: 'detach' })
}
}
@@ -954,6 +970,11 @@ async function main(): Promise<void> {
}
openingWelcome ??= (async () => {
welcomeWindow = await openWelcomeWindow(locale, {
takeNotice: () => {
const notice = pendingWelcomeNotice
pendingWelcomeNotice = undefined
return Promise.resolve(notice)
},
startSignIn: async () => {
if (welcomeBackend === undefined) throw new Error('desktop welcome: backend unavailable')
return welcomeBackend.account.start(locale.id)
+2 -1
View File
@@ -3,13 +3,14 @@ import type { AccountView, SignInAttemptId } from '@deepseek-ai/dsh-deepseek-acc
import { contextBridge, ipcRenderer } from 'electron'
import { resolveDesktopLocale } from './locale.ts'
import { WELCOME_IPC, type WelcomeApi, type WelcomeSaveResult } from './welcome-api.ts'
import { WELCOME_IPC, type WelcomeApi, type WelcomeNotice, type WelcomeSaveResult } from './welcome-api.ts'
const prefix = '--dsh-welcome-locale='
const locale = process.argv.find(argument => argument.startsWith(prefix))?.slice(prefix.length)
if (locale === undefined) throw new Error('desktop welcome: missing window locale')
const api: WelcomeApi = {
...resolveDesktopLocale(locale),
takeNotice: () => ipcRenderer.invoke(WELCOME_IPC.takeNotice) as Promise<WelcomeNotice | undefined>,
startSignIn: () => ipcRenderer.invoke(WELCOME_IPC.start) as Promise<AccountView>,
cancelSignIn: (id: SignInAttemptId) => ipcRenderer.invoke(WELCOME_IPC.cancel, id) as Promise<AccountView>,
copySignInLink: (id: SignInAttemptId) => ipcRenderer.invoke(WELCOME_IPC.copyLink, id) as Promise<void>,
+6
View File
@@ -11,13 +11,19 @@ export const WELCOME_IPC = {
cancel: 'dsh-welcome:cancel',
copyLink: 'dsh-welcome:copy-link',
state: 'dsh-welcome:state',
takeNotice: 'dsh-welcome:take-notice',
} as const
/** Credential writes return a safe outcome without exposing Host diagnostics. */
export type WelcomeSaveResult = { readonly ok: true } | { readonly ok: false }
/** One-time notification retained by the main process until Welcome receives it. */
export type WelcomeNotice = 'session-expired'
/** Host-owned operations used by the welcome window. */
export interface WelcomeOperations {
/** @returns the pending notification, clearing it before another renderer can receive it. */
takeNotice(): Promise<WelcomeNotice | undefined>
/** @returns account state after starting a login attempt. */
startSignIn(): Promise<AccountView>
/** @param id - attempt to cancel. @returns the settled state. */
+4 -1
View File
@@ -63,7 +63,9 @@ export async function openWelcomeWindow(locale: DesktopLocale, operations: Welco
const disposeHandlers = (): void => {
if (!active) return
active = false
for (const channel of [WELCOME_IPC.saveApiKey, WELCOME_IPC.skip, WELCOME_IPC.start, WELCOME_IPC.cancel, WELCOME_IPC.copyLink]) {
for (const channel of [
WELCOME_IPC.takeNotice, WELCOME_IPC.saveApiKey, WELCOME_IPC.skip, WELCOME_IPC.start, WELCOME_IPC.cancel, WELCOME_IPC.copyLink,
]) {
ipcMain.removeHandler(channel)
}
disposeActiveHandlers = undefined
@@ -74,6 +76,7 @@ export async function openWelcomeWindow(locale: DesktopLocale, operations: Welco
throw new Error('desktop welcome: rejected action from an unowned frame')
}
}
ipcMain.handle(WELCOME_IPC.takeNotice, async (event) => { assertSender(event); return operations.takeNotice() })
ipcMain.handle(WELCOME_IPC.saveApiKey, async (event, value: unknown) => {
assertSender(event)
if (typeof value !== 'string' || !/^[\x21-\x7e]+$/.test(value)) return { ok: false }
+46 -1
View File
@@ -1,4 +1,6 @@
import { expect, it } from 'vitest'
import { once } from 'node:events'
import { WebSocketServer } from 'ws'
import { expect, it, onTestFinished, vi } from 'vitest'
import { accountView, desktopAccountBackend } from '../src/account-backend.ts'
it('projects only safe account fields and refuses non-browser authorization schemes', () => {
@@ -24,3 +26,46 @@ it('uses account Remote commands without returning additional wire fields', asyn
expect(await backend.start('en')).toEqual({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null })
expect(requests).toEqual([{ namespace: 'account', method: 'startSignIn', args: { locale: 'en', callbackOrigin: 'http://127.0.0.1:1234', loginSource: 'desktop' } }])
})
it('does not replay an expiry reason from an account snapshot', () => {
const state = { status: 'signed-out', attempt: null,
links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' } }
expect(accountView({ ...state, signOutReason: 'expired' })).toEqual(state)
})
it('receives live expiry separately from account snapshots', async () => {
const server = new WebSocketServer({ host: '127.0.0.1', port: 0 })
await once(server, 'listening')
onTestFinished(async () => {
for (const client of server.clients) client.terminate()
await new Promise<void>((resolve, reject) => { server.close((error) => { if (error) reject(error); else resolve() }) })
})
const address = server.address()
if (typeof address === 'string' || address === null) throw new Error('missing server address')
const connected = once(server, 'connection')
const expired = vi.fn()
const snapshot = { status: 'signed-out', attempt: null,
links: { usageUrl: 'https://platform.deepseek.com/usage', topUpUrl: 'https://platform.deepseek.com/top_up' } }
const received = Promise.withResolvers<undefined>()
const backend = desktopAccountBackend(`http://127.0.0.1:${address.port}`, () => Promise.resolve(snapshot), () => Promise.resolve(''))
const stop = backend.watch((value) => { expect(value).toEqual(snapshot); received.resolve(undefined) }, vi.fn(), expired)
onTestFinished(stop)
await connected
// Both logical streams share the socket; observe their opening frames before sending data.
const streams = new Map<string, string>()
const opened = Promise.withResolvers<undefined>()
const socket = [...server.clients][0]!
socket.on('message', (data) => {
if (!Buffer.isBuffer(data)) throw new Error('expected a Buffer WebSocket frame')
const frame = JSON.parse(data.toString('utf8')) as { endpoint: string; streamId: string }
streams.set(frame.endpoint, frame.streamId)
if (streams.size === 2) opened.resolve(undefined)
})
await opened.promise
socket.send(JSON.stringify({ type: 'item', streamId: streams.get('account/watch'), value: snapshot }))
await received.promise
expect(expired).not.toHaveBeenCalled()
socket.send(JSON.stringify({ type: 'item', streamId: streams.get('account/watchExpiry'), value: 'session-expired' }))
await vi.waitFor(() => { expect(expired).toHaveBeenCalledOnce() })
stop()
})
@@ -0,0 +1 @@
You have signed out of your account, please log in again.
@@ -0,0 +1 @@
登录信息已失效,请重新登录
+3
View File
@@ -80,6 +80,9 @@ describe.skipIf(!existsSync(preload('preload-app')))('built sandboxed Desktop pr
expect(send).toHaveBeenCalledWith('dsh-desktop:locale-changed', 'zh')
} else {
expect(exposed.has('dshWelcome')).toBe(true)
const bridge = exposed.get('dshWelcome') as { takeNotice(): Promise<unknown> }
void bridge.takeNotice()
expect(invoke).toHaveBeenCalledWith('dsh-welcome:take-notice')
}
})
})
@@ -1,20 +1,21 @@
// @vitest-environment jsdom
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { Welcome } from '../src/client/WelcomePage.tsx'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { resolveDesktopLocale } from '../src/locale.ts'
import type { AccountView } from '@deepseek-ai/dsh-deepseek-account/types'
import type { WelcomeSaveResult } from '../src/welcome-api.ts'
import type { WelcomeSaveResult, WelcomeNotice } from '../src/welcome-api.ts'
const html = readFileSync(join(import.meta.dirname, '../renderer/welcome.html'), 'utf8')
afterEach(cleanup)
function mount(language = 'zh-CN') {
function mount(language = 'zh-CN', takeNotice = vi.fn<() => Promise<WelcomeNotice | undefined>>().mockResolvedValue(undefined)) {
cleanup()
const stopAccount = vi.fn()
const api = {
takeNotice,
onAccountState: vi.fn((_listener: (state: AccountView) => void) => stopAccount),
startSignIn: vi.fn(async (): Promise<AccountView> => ({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null })),
cancelSignIn: vi.fn(async (): Promise<AccountView> => ({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out', attempt: null })),
@@ -260,3 +261,68 @@ it.each(['copied', 'failed'] as const)('restores the copy action after %s feedba
vi.useRealTimers()
}
})
it.each(['zh-CN', 'en'])('keeps the expiry notice visible after returning to Welcome: %s', async (language) => {
vi.useFakeTimers()
try {
const takeNotice = vi.fn<() => Promise<WelcomeNotice | undefined>>().mockResolvedValue(undefined).mockResolvedValueOnce('session-expired')
const view = mount(language, takeNotice)
await act(async () => {})
const publish = view.api.onAccountState.mock.calls[0]![0]
const expired: AccountView = { status: 'signed-out', attempt: null,
links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' } }
await act(async () => { publish(expired) })
const notice = screen.getByRole('alert')
expect(notice.textContent).toBe(view.api.messages.welcomeSessionExpired)
await expect(`${notice.textContent}\n`).toMatchFileSnapshot(`./expected/welcome/${language}-expired.expected.txt`)
expect(view.button('#sign-in').closest('[hidden]')).toBeNull()
await act(async () => { await vi.advanceTimersByTimeAsync(4000) })
expect(screen.queryByRole('alert')).toBeNull()
await act(async () => { publish(expired) })
expect(screen.queryByRole('alert')).toBeNull()
view.unmount()
mount(language, takeNotice)
await act(async () => {})
expect(screen.queryByRole('alert')).toBeNull()
} finally { cleanup(); vi.useRealTimers() }
})
it('does not infer a notification from a retained expired account snapshot', async () => {
const view = mount()
await act(async () => {
view.api.onAccountState.mock.calls[0]![0]({ status: 'signed-out', attempt: null,
links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' } })
})
expect(screen.queryByRole('alert')).toBeNull()
})
it('keeps the entry usable when notification IPC fails', async () => {
const view = mount('en', vi.fn<() => Promise<WelcomeNotice | undefined>>().mockRejectedValue(new Error('closed')))
await act(async () => {})
expect(screen.queryByRole('alert')).toBeNull()
fireEvent.click(view.button('#api-key'))
expect(view.input.closest('[hidden]')).toBeNull()
})
it('ignores a notification received after its renderer unmounts', async () => {
const pending = Promise.withResolvers<WelcomeNotice | undefined>()
const view = mount('en', vi.fn<() => Promise<WelcomeNotice | undefined>>().mockReturnValue(pending.promise))
view.unmount()
mount('en')
await act(async () => { pending.resolve('session-expired') })
expect(screen.queryByRole('alert')).toBeNull()
})
it.each(['zh-CN', 'en'])('returns from completed sign-in to the initial page after sign-out: %s', async (language) => {
const view = mount(language)
const publish = view.api.onAccountState.mock.calls[0]![0]
const links = { usageUrl: 'https://example.test/usage', topUpUrl: 'https://example.test/top_up' }
act(() => { publish({ status: 'credential-stored', links,
attempt: { id: 'completed' as NonNullable<AccountView['attempt']>['id'], phase: 'succeeded' } }) })
expect(view.document.querySelector<HTMLElement>('#auth-page')!.hidden).toBe(false)
act(() => { publish({ status: 'signed-out', links, attempt: null }) })
expect(view.button('#sign-in').closest('[hidden]')).toBeNull()
expect(view.document.querySelector<HTMLElement>('#auth-page')!.hidden).toBe(true)
await expect(view.copy()).toMatchFileSnapshot(`./expected/welcome/${language}.expected.txt`)
})
+59 -6
View File
@@ -14,6 +14,8 @@ const state = vi.hoisted(() => ({
beforeRead: vi.fn(async () => {}),
beforeWelcome: vi.fn(async () => {}),
copy: vi.fn(),
expiryListener: undefined as (() => void) | undefined,
accountListener: undefined as ((value: AccountView) => void) | undefined,
accountState: vi.fn<() => Promise<AccountView>>().mockResolvedValue({
status: 'signed-out', attempt: null, links: { usageUrl: '', topUpUrl: '' },
}),
@@ -22,9 +24,13 @@ const state = vi.hoisted(() => ({
stopHost: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
loadWorkspace: vi.fn<(url: string) => Promise<void>>().mockResolvedValue(undefined),
showWorkspace: vi.fn(),
showInactiveWorkspace: vi.fn(),
focusWorkspace: vi.fn(),
openDevTools: vi.fn(),
closeWelcome: vi.fn(),
welcomeLocale: undefined as DesktopLocale | undefined,
preference: 'zh',
hasApiKey: false,
handlers: new Map<string, (...args: unknown[]) => unknown>(),
listeners: new Map<string, (...args: unknown[]) => void>(),
contents: undefined as { mainFrame: { url: string } } | undefined,
@@ -62,16 +68,18 @@ vi.mock('electron', () => ({
BrowserWindow: class {
constructor(options: BrowserWindowConstructorOptions) { state.windowOptions = options }
private ready: (() => void) | undefined
webContents = { mainFrame: { url: 'dsh-app://app/' }, setWindowOpenHandler: vi.fn(), on: vi.fn(), once: vi.fn(), send: vi.fn(), openDevTools: vi.fn() }
webContents = { mainFrame: { url: 'dsh-app://app/' }, setWindowOpenHandler: vi.fn(),
on: vi.fn(), once: vi.fn(), send: vi.fn(), openDevTools: state.openDevTools }
static getAllWindows() { return [] }
once(name: string, callback: () => void) { if (name === 'ready-to-show') this.ready = callback; return this }
on() { return this }
isDestroyed() { return false }
isMinimized() { return false }
restore = vi.fn()
focus = vi.fn()
focus = state.focusWorkspace
hide = vi.fn()
show = state.showWorkspace
showInactive = state.showInactiveWorkspace
async loadURL(url: string) { state.contents = this.webContents; await state.loadWorkspace(url); this.ready?.() }
},
net: { fetch: vi.fn() },
@@ -98,7 +106,9 @@ vi.mock('../src/host-process.ts', () => ({
start = state.startHost
stop = state.stopHost
fetch() {
return Promise.resolve(Response.json({ loggedIn: false, hasApiKey: false, writable: true, localePreference: state.preference }))
return Promise.resolve(Response.json({
loggedIn: false, hasApiKey: state.hasApiKey, writable: true, localePreference: state.preference,
}))
}
},
}))
@@ -107,10 +117,17 @@ vi.mock('../src/welcome-backend.ts', () => ({
readLocalePreference: async () => state.preference,
read: async () => {
await state.beforeRead()
return { loggedIn: false, hasApiKey: false, writable: true, localePreference: state.preference }
return { loggedIn: false, hasApiKey: state.hasApiKey, writable: true, localePreference: state.preference }
},
save: async () => ({ ok: true }),
account: { watch: () => () => {}, state: state.accountState },
account: {
watch: (listener: (value: AccountView) => void, _failed: () => void, expired: () => void) => {
state.accountListener = listener
state.expiryListener = expired
return () => {}
},
state: state.accountState,
},
}),
}))
vi.mock('node:fs/promises', async importOriginal => ({
@@ -131,7 +148,8 @@ vi.mock('../src/welcome-window.ts', () => ({
state.welcomeLocale = locale
state.operations = operations
await state.beforeWelcome()
return { once: vi.fn(), close: state.closeWelcome }
return { once: vi.fn(), close: state.closeWelcome, isDestroyed: () => false,
show: vi.fn(), focus: vi.fn(), webContents: { send: vi.fn() } }
},
}))
@@ -180,6 +198,7 @@ it('starts the Host for welcome onboarding and opens the workspace on skip witho
expect(state.showWorkspace).not.toHaveBeenCalled()
state.loadWorkspace.mockClear()
expect(state.welcomeLocale).toMatchObject({ id: 'zh-CN' })
expect(await state.operations!.takeNotice()).toBeUndefined()
expect(state.dialogLocale!().id).toBe('zh-CN')
const attemptId = 'login' as NonNullable<AccountView['attempt']>['id']
const account: AccountView = { status: 'signed-out', links: { usageUrl: '', topUpUrl: '' },
@@ -223,4 +242,38 @@ it('starts the Host for welcome onboarding and opens the workspace on skip witho
expect(state.dialogLocale!().id).toBe('en')
expect(state.menu).toHaveBeenCalledTimes(initialMenus + 1)
expect(await bootstrap(event)).toEqual({ languages: ['en-US'], preference: 'en' })
const welcomeCount = state.beforeWelcome.mock.calls.length
state.hasApiKey = true
state.accountListener!({ ...account, status: 'credential-stored', attempt: null })
state.accountListener!({ ...account, status: 'signed-out', attempt: null })
state.expiryListener!()
await vi.advanceTimersByTimeAsync(0)
expect(state.beforeWelcome).toHaveBeenCalledTimes(welcomeCount)
expect(await state.operations!.takeNotice()).toBeUndefined()
state.hasApiKey = false
state.accountListener!({ ...account, status: 'credential-stored', attempt: null })
state.accountListener!({ ...account, status: 'signed-out', attempt: null })
state.expiryListener!()
await vi.waitFor(() => { expect(state.beforeWelcome).toHaveBeenCalledTimes(welcomeCount + 1) })
expect(await state.operations!.takeNotice()).toBe('session-expired')
expect(await state.operations!.takeNotice()).toBeUndefined()
state.accountListener!({ ...account, status: 'signed-out', attempt: null })
await vi.advanceTimersByTimeAsync(0)
expect(await state.operations!.takeNotice()).toBeUndefined()
state.accountListener!({ ...account, status: 'credential-stored', attempt: null })
state.accountListener!({ ...account, status: 'signed-out', attempt: null })
await vi.advanceTimersByTimeAsync(0)
expect(await state.operations!.takeNotice()).toBeUndefined()
state.showWorkspace.mockClear()
state.focusWorkspace.mockClear()
vi.stubEnv('DSH_DESKTOP_OPEN_DEVTOOLS', '1')
state.accountListener!({ ...account, status: 'credential-stored', attempt: { id: attemptId, phase: 'succeeded' } })
await vi.waitFor(() => { expect(state.showInactiveWorkspace).toHaveBeenCalledOnce() })
expect(state.showWorkspace).not.toHaveBeenCalled()
expect(state.focusWorkspace).not.toHaveBeenCalled()
expect(state.openDevTools).not.toHaveBeenCalled()
state.appListeners.get('open-url')!({ preventDefault: vi.fn() }, 'dsh://open')
expect(state.showWorkspace).toHaveBeenCalledOnce()
expect(state.focusWorkspace).toHaveBeenCalledOnce()
})
+8 -1
View File
@@ -40,6 +40,7 @@ function createWindow() {
beforeEach(() => { electron.create.mockReset(); electron.handlers.clear() })
const operations = {
takeNotice: async () => undefined,
startSignIn: async () => ({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out' as const, attempt: null }),
cancelSignIn: async () => ({ links: { usageUrl: 'http://localhost/usage', topUpUrl: 'http://localhost/top_up' }, status: 'signed-out' as const, attempt: null }),
copySignInLink: async () => undefined,
@@ -115,8 +116,14 @@ describe('desktop welcome window', () => {
const saveApiKey = vi.fn(operations.saveApiKey)
const skip = vi.fn(operations.skip)
const copySignInLink = vi.fn(operations.copySignInLink)
await openWelcomeWindow(resolveDesktopLocale('en'), { ...operations, saveApiKey, skip, copySignInLink })
const takeNotice = vi.fn(async () => 'session-expired' as const)
await openWelcomeWindow(resolveDesktopLocale('en'), { ...operations, saveApiKey, skip, copySignInLink, takeNotice })
const own = { sender: window.webContents, senderFrame: window.webContents.mainFrame }
const take = electron.handlers.get(WELCOME_IPC.takeNotice)!
await expect(take({ ...own, senderFrame: {} })).rejects.toThrow('unowned frame')
expect(takeNotice).not.toHaveBeenCalled()
expect(await take(own)).toBe('session-expired')
expect(takeNotice).toHaveBeenCalledOnce()
const save = electron.handlers.get(WELCOME_IPC.saveApiKey)!
await expect(save({ sender: {}, senderFrame: {} }, 'sk-test')).rejects.toThrow('unowned frame')
await expect(save({ ...own, senderFrame: {} }, 'sk-test')).rejects.toThrow('unowned frame')
@@ -84,17 +84,19 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('keeps a saved DeepSeek selection available after editing provider settings', async () => {
it('selects an available DeepSeek model after provider settings remove the default', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-deepseek-messages-default'))
await page.keyboard.press('Escape')
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: 'deepseek-official', model: 'deepseek-v4-flash' })
await page.reload({ waitUntil: 'load' })
const input = page.locator('[data-composer-input]').first()
await expect.poll(() => input.isEnabled()).toBe(true)
await page.getByRole('button', { name: /^选择模型/ }).click()
const trigger = page.getByRole('button', { name: /^选择模型.*deepseek-official\/deepseek-v4-flash/ })
await trigger.waitFor()
await expect.poll(() => input.getAttribute('contenteditable')).toBe('true')
await trigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).click()
await expect.poll(() => input.isEnabled()).toBe(true)
await expect.poll(() => input.getAttribute('contenteditable')).toBe('true')
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection().provider).toBe('deepseek-official')
const settings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(settings).toContain('provider: deepseek-official')
+15 -23
View File
@@ -17,7 +17,7 @@ import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { SessionId } from '@deepseek-ai/dsh-session'
import { launchWebScaffold, watchConsole, type WebScaffold } from './scaffold.ts'
import { launchWebScaffold, watchConsole, captureStableAria, compareOrRefreshGolden, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
/** Points the shipped shared Agent default at this scenario's own route. */
@@ -104,6 +104,7 @@ describe('web e2e: the composer model switch is the default for later sessions',
const trigger = page.getByRole('button', { name: /^选择模型/ })
await trigger.waitFor({ timeout: 15_000 })
expect(await trigger.evaluate(element => getComputedStyle(element).fontWeight)).toBe('400')
await trigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
await page.getByRole('menuitemradio', { name: 'Acme Large' }).click()
@@ -126,33 +127,24 @@ describe('web e2e: the composer model switch is the default for later sessions',
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('goes inert when the route the default names stops being served', async () => {
it('retains the saved route and editable composer when its provider is removed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-default-model-blocked'))
const box = page.locator('[data-composer-input]').first()
await expect.poll(async () => box.isEnabled(), { timeout: 10_000 }).toBe(true)
// What removing the provider on the Models page leaves behind: the saved
// default still names the route, and nothing serves it any more.
// `replace`, not `update`: a merge patch of `{providers: {}}` leaves every
// stored profile in place.
await scaffold.ctx.settings.replace('llm-pi-ai', { providers: {} })
// Removing the selected provider preserves the other route and the saved
// selection. A merge update would retain the removed profile.
await scaffold.ctx.settings.replace('llm-pi-ai', { providers: {
[START_ROUTE]: { displayName: 'Origin Gateway', api: 'openai-completions',
baseURL: 'https://gateway.origin.example/v1', models: [{ id: START_MODEL, name: 'Origin Large' }] },
} })
await expect.poll(async () => box.isEnabled(), { timeout: 15_000 }).toBe(false)
expect(await box.getAttribute('data-placeholder')).toBe('当前模型不可用,请先选择模型')
// The block is an affordance; the refusal is the Host's. A client that
// never disabled anything still cannot start a turn on a dead route.
await expect(scaffold.ctx.sessionController.prompt({
requestId: 'default-model-refused' as never,
sessionId: SessionId(await createSession('default-model-refusal')),
mode: 'queue',
content: [{ type: 'text', text: 'hi' }],
}, new AbortController().signal)).rejects.toMatchObject({ code: 'session/model-unavailable' })
// The way out stays open. Locking the model seat with everything else
// would leave the composer asking for the one thing it prevents.
const seat = page.getByRole('button', { name: /^选择模型/ })
expect(await seat.isEnabled()).toBe(true)
const seat = page.getByRole('button', { name: new RegExp(`^选择模型.*${ROUTE}/${MODEL}`) })
await seat.waitFor()
expect(await box.isEnabled()).toBe(true)
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toMatchObject({ provider: ROUTE, model: MODEL })
const aria = await captureStableAria(page, '[data-composer-card]', scaffold.workspaceCwd)
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/default-model/unselected.expected.md', import.meta.url)), aria, webSnapshotMode())
await seat.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
await page.getByRole('menuitemradio').first().click()
@@ -0,0 +1,5 @@
- textbox "描述你想要构建的内容, / 调用指令, @ 文件或对话"
- button "添加文件或调用指令"
- button "访问模式,当前:工作区内修改": 工作区内修改
- button "选择模型,当前 acme-gateway/acme-large": acme-gateway/acme-large
- button "发送消息" [disabled]
@@ -0,0 +1,4 @@
- menu:
- menuitem "设置"
- menuitem "联系我们"
- menuitem "登录"
@@ -35,6 +35,7 @@ const REPLY = 'Review complete: no actionable findings.'
/** Deterministic model response for the webhook-created Session. */
class ReviewAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
readonly requests: GenerateOptions[] = []
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
@@ -273,9 +273,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await page.keyboard.press('Escape')
// A connected Workspace is what puts a live composer — and its model
// trigger — on the page; the scaffold boots without one.
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd, 'model-fallback-e2e')
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd, 'model-fallback-e2e', false)
const modelTrigger = page.getByRole('button', { name: /^选择模型/ })
const modelTrigger = page.getByRole('button', { name: /^选择模型.*deepseek-official\// })
await modelTrigger.waitFor({ timeout: 10_000 })
await modelTrigger.click()
await page.getByRole('menuitem', { name: /模型/ }).click()
+28 -3
View File
@@ -11,6 +11,7 @@ import {
import { openSettings, ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/onboarding-native', import.meta.url))
const SIGNED_OUT_MENU_EXPECTED = join(SNAPSHOT_DIR, 'signed-out-menu.expected.md')
const MODE = webSnapshotMode()
describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credential onboarding (desktop marker: %s)', (desktop) => {
@@ -53,8 +54,32 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
}
const accountMenu = page.getByRole('button', { name: '账号菜单', exact: true })
if (desktop) await accountMenu.waitFor()
else {
if (desktop) {
await accountMenu.waitFor()
expect(await accountMenu.textContent()).toBe('更多')
const triggerBox = (await accountMenu.boundingBox())!
expect(Math.abs(triggerBox.height - 32)).toBeLessThan(1)
await accountMenu.click()
const menu = page.getByRole('menu')
await menu.waitFor()
expect(await menu.getByRole('menuitem').allTextContents()).toEqual(['设置', '联系我们', '登录'])
const menuBox = (await menu.boundingBox())!
expect(Math.abs(menuBox.width - 124)).toBeLessThan(1)
expect(Math.abs(menuBox.height - 128)).toBeLessThan(1)
expect(Math.abs(triggerBox.y - (menuBox.y + menuBox.height) - 4)).toBeLessThan(1)
for (const row of await menu.getByRole('menuitem').all()) {
const rowBox = (await row.boundingBox())!
expect(Math.abs(rowBox.height - 40)).toBeLessThan(1)
expect(Math.abs(menuBox.width - rowBox.width - 8)).toBeLessThan(1)
const glyph = (await row.locator('svg').first().boundingBox())!
expect(Math.abs(glyph.width - 16)).toBeLessThan(1)
expect(Math.abs(glyph.height - 16)).toBeLessThan(1)
}
const menuAria = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(SIGNED_OUT_MENU_EXPECTED, menuAria, MODE)
await page.keyboard.press('Escape')
await menu.waitFor({ state: 'detached' })
} else {
await page.getByRole('button', { name: '设置', exact: true }).waitFor()
expect(await accountMenu.count()).toBe(0)
expect(await page.getByRole('dialog', { name: '开始你的创作' }).count()).toBe(0)
@@ -72,6 +97,6 @@ describe.skipIf(MODE === 'record').each([false, true])('web e2e: native credenti
}
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['models.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['models.expected.md', 'signed-out-menu.expected.md'])
})
})
+1
View File
@@ -14,6 +14,7 @@ const DELTAS = ['First paragraph', `\nDetails\n\n\n${SUMMARY}`, '\nMore detail']
const UI_EXPECTED = fileURLToPath(new URL('./expected/reasoning-preview/running.expected.md', import.meta.url))
class PausedReasoningAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: 'paused', name: `${provider}/paused` }] }
readonly stages = DELTAS.map(text => ({
text,
arrived: Promise.withResolvers<undefined>(),
+3 -3
View File
@@ -24,6 +24,7 @@ import {
compareOrRefreshGolden,
launchWebScaffold,
seedSession,
readPersistedEvents,
watchConsole,
webSnapshotMode,
type WebScaffold,
@@ -403,9 +404,8 @@ describe.skipIf(MODE === 'record')('web e2e: file and session references through
await target.waitFor({ timeout: 15_000 })
await target.click()
await page.locator('[data-chat-flow-kind="user"]').filter({ hasText: 'Research notes' }).waitFor({ timeout: 15_000 })
const session = scaffold.ctx.sessions.get(SessionId(TARGET_SESSION_ID))
if (session === undefined) throw new Error('reference target session is unavailable')
const inputs = session.snapshotEvents().filter(event => event.type === 'user/message')
const events = await readPersistedEvents(scaffold, SessionId(TARGET_SESSION_ID))
const inputs = events.filter(event => event.type === 'user/message')
expect(inputs.map(event => event.data.source.kind)).toEqual(['user', 'session-reference'])
expect(inputs[0]?.seq).toBeLessThan(inputs[1]!.seq)
expect(JSON.stringify(inputs[1]?.data)).toContain('<referenced-sessions>snapshot</referenced-sessions>')
+3
View File
@@ -92,6 +92,7 @@ function textResponse(text: string): StreamChunk[] {
/** Deterministic model seam that turns one due reminder into ordinary assistant prose. */
class ReminderAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
readonly requests: GenerateOptions[] = []
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
@@ -102,6 +103,7 @@ class ReminderAdapter extends LlmAdapter {
/** Deterministic model seam for one multi-record fixed-rate batch. */
class EveryReminderAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
readonly requests: GenerateOptions[] = []
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
@@ -137,6 +139,7 @@ function localAt(epoch: number, timeZone: string): LocalAt {
/** Dynamic model seam proving request-local browser context becomes an explicit At selector. */
class BrowserZoneAtAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
readonly requests: GenerateOptions[] = []
selectedAt: LocalAt | undefined
scheduledAt: string | undefined
@@ -94,6 +94,7 @@ function textChunks(text: string): StreamChunk[] {
/** Scripted same-route main model and reviewer for the shipped Auto pipeline. */
class ShippedAutoAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: AUTO_MODEL, name: AUTO_MODEL }] }
readonly requests: GenerateOptions[] = []
constructor(private readonly targetPath: string) {
@@ -196,6 +197,7 @@ function topLevelText(options: GenerateOptions): string {
/** Same-route scripts for real one-shot, continuable, and cold-resumed children. */
class ShippedChildAutoAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: AUTO_MODEL, name: AUTO_MODEL }] }
readonly requests: GenerateOptions[] = []
readonly reviews: ChildReviewObservation[] = []
private readonly children = new Map<SessionId, ChildScriptState>()
@@ -30,6 +30,7 @@ const REPLY = `${OPEN_REPLY}\n\`\`\``
/** Deterministic model response held after each visible fence-growth frame. */
class StreamingFenceAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: MODEL, name: `${provider}/${MODEL}` }] }
private resolveFirstPaused!: () => void
private resolveFirstContinuation!: () => void
private resolveSecondPaused!: () => void
+4 -2
View File
@@ -166,8 +166,9 @@ export async function connectFreshWorkspace(page: Page, root: string, name = 'wo
* @param page - the browser page under test.
* @param root - workspace parent directory.
* @param name - directory created under `root` and connected.
* @param modelAvailable - require an editable composer when the selected model is available.
*/
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace'): Promise<void> {
export async function connectFreshWorkspaceZh(page: Page, root: string, name = 'workspace', modelAvailable = true): Promise<void> {
mkdirSync(join(root, name), { recursive: true })
await page.getByRole('textbox', { name: '选择工作区' }).click()
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
@@ -177,7 +178,8 @@ export async function connectFreshWorkspaceZh(page: Page, root: string, name = '
await pathInput.fill(join(root, name))
await pathInput.press('Enter')
await dialog.getByRole('button', { name: '打开', exact: true }).click()
await page.locator('[data-composer-input][contenteditable="true"][data-placeholder="描述你想要构建的内容, / 调用指令, @ 文件或对话"]')
const editable = modelAvailable ? '[contenteditable="true"]' : ''
await page.locator(`[data-composer-input]${editable}[data-placeholder="描述你想要构建的内容, / 调用指令, @ 文件或对话"]`)
.waitFor({ timeout: 15_000 })
}
+2 -2
View File
@@ -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 docs/architecture.md
architecture.md: e09bc38c291dd9a3450cfaef17212514cacdab13
architecture.zh.md: 53125c0d796680a05f190adcb848e2b5d5d00076
architecture.md: 634214943c14fe049f141350cad6518167f52bb1
architecture.zh.md: 6082c5c86a900c03017fbf15f6c4abd41a48a9f9
+1 -1
View File
@@ -112,7 +112,7 @@ One inbox feeds the driver; injected context waits for a waking message. AgentLo
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. The prepared call capability governs prompt admission, not the preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes can append non-empty updates after the cached prefix; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
The loop sends immutable requests while keeping cancellation live. It reuses message-freeze evidence only for identities it has fully frozen; [agent-loop](../packages/core/agent-loop/README.md) owns the request construction and cancellation-cause rules.
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-execution-pipeline.md), and [cancellation and error recovery](subsystems/core.md#the-agent-handle).
+1 -1
View File
@@ -116,7 +116,7 @@ turn/end
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
循环发送不可变请求,同时保留实时取消能力。只有已由该循环完整冻结的消息对象身份才能复用冻结证明;[agent-loop](../packages/core/agent-loop/README.zh.md)拥有请求构造与取消原因记录规则。
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
详情见[时序图](agent-lifecycle.zh.md)、[工具流水线](tool-execution-pipeline.zh.md)和[取消与错误恢复](subsystems/core.zh.md#the-agent-handle)。
+2 -2
View File
@@ -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 docs/config-catalog.md
config-catalog.md: eb63c85c48d91bc9a79ebd909f2e0acd3a406d26
config-catalog.zh.md: cdbfb07a2ecda96c8df282b8c49ec59ec34d8401
config-catalog.md: e3161d1e72e5a586b9b9662e1f8005caa8d320ad
config-catalog.zh.md: eeba33336e3a6d3d699068835521d13867a34427
+24 -81
View File
@@ -1434,96 +1434,38 @@ export interface Config {
Source: [`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
<a id="deepseek-aidsh-llm-deepseek"></a>
<a id="deepseek-aidsh-llm-deepseek-account"></a>
## `@deepseek-ai/dsh-llm-deepseek`
## `@deepseek-ai/dsh-llm-deepseek-account`
Requires: `llm`
```ts config-catalog
/**
* Plugin config, validated by the same-named schemastery schema and doubling
* as the `llm-deepseek` settings-section shape. Every field is optional in
* yml: a missing API key resolves through {@link Config.apiKeyEnv} at each
* request (a request without any key fails with `MISSING_CREDENTIAL`, not at
* plugin load), omitted thinking mode uses the provider default, and omitted
* reasoning effort resolves to `high`.
*/
export interface Config {
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
apiKeyEnv: Volatile<string>
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
baseURL: Volatile<string | undefined>
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
thinking: Volatile<'enabled' | 'disabled' | undefined>
/** Default thinking effort (default `high`); `off` disables thinking per request. */
reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
maxTokens: Volatile<number>
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
defaultContextWindow: Volatile<number>
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
models: Volatile<DeepSeekCatalogModel[]>
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
streamIdleTimeoutMs: Volatile<number>
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
maxRequestFilesBytes: Volatile<number>
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
maxInlineRequestImageBytes: Volatile<number>
/** Maximum number of represented images per chat request (default 600). */
maxImagesPerRequest: Volatile<number>
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
imageOffloadByteQuantum: Volatile<number>
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
inlineImageOffloadByteQuantum: Volatile<number>
/** Image-count removal step after the request exceeds its count bound (default 20). */
imageOffloadCountQuantum: Volatile<number>
/** Maximum duration of one request-image Files API resolution (default one minute). */
filesApiTimeoutMs: Volatile<number>
/** Explicit lifetime assigned to each uploaded image (default seven days). */
fileExpiresAfterSeconds: Volatile<number>
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
fileRefreshMarginSeconds: Volatile<number>
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
fileQuotaCleanupBatch: Volatile<number>
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy: Volatile<RetryPolicyConfig | undefined>
}
/** Account route configuration; authentication comes exclusively from the account service. */
export type Config = ProtocolConfig
```
/** One optional model entry advertised by the direct-fetch adapter. */
export interface DeepSeekCatalogModel {
/** Wire model id accepted by the configured endpoint. */
id: string
/** Selector label; defaults to {@link id}. */
name?: string
/** Optional selector detail for deployments with similar model variants. */
description?: string
/** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */
contextWindow?: number
/** Per-request output cap for this model; omission falls back to the profile's {@link DeepSeekConnectionOptions.maxTokens}. */
maxTokens?: number
/** Accepted request modalities; omission is text-only. */
inputModalities?: ModelModality[]
/**
* Total-pixel budget replacing the published token-grid projection for one
* deterministic request preview, or the 512-by-512 `low` preset; omission
* projects onto the token grid.
*/
imagePixelBudget?: number | 'low'
/** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */
imageMaxBytes?: number
/**
* `'in-history'` declares that the endpoint reads the latest `system`
* message at any position of the conversation as the complete effective
* system prompt; omission means only a leading system message is read.
*/
systemPromptUpdate?: SystemPromptUpdate
Depends on: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts)
Source: [`packages/llm/llm-deepseek-account/src/config.ts:5`](../packages/llm/llm-deepseek-account/src/config.ts)
<a id="deepseek-aidsh-llm-deepseek-api-key"></a>
## `@deepseek-ai/dsh-llm-deepseek-api-key`
Requires: `llm`
```ts config-catalog
/** Messages configuration with a per-request API-key reference. */
export interface Config extends ProtocolConfig {
/** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */
apiKeyEnv: Volatile<string>
}
```
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
Depends on: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
Source: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
Source: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts)
<a id="deepseek-aidsh-llm-pi-ai"></a>
@@ -4114,7 +4056,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-acp-app` — requires `cmdlineArgs` ([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts))
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-api-account-controller` — requires `deepseekAccount` ([`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts))
- `@deepseek-ai/dsh-api-account-controller` — requires `deepseekAccount` · `agents` ([`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts))
- `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
- `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
- `@deepseek-ai/dsh-browser-use` ([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
@@ -4259,6 +4201,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-http-proxy` ([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
- `@deepseek-ai/dsh-launch-environment` ([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
- `@deepseek-ai/dsh-lazy-require` ([`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts))
- `@deepseek-ai/dsh-llm-deepseek` ([`packages/llm/llm-deepseek/src/index.ts`](../packages/llm/llm-deepseek/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
- `@deepseek-ai/dsh-loader-smoke` ([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))
- `@deepseek-ai/dsh-native-command` ([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
+24 -81
View File
@@ -1436,96 +1436,38 @@ export interface Config {
来源: [`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
<a id="deepseek-aidsh-llm-deepseek"></a>
<a id="deepseek-aidsh-llm-deepseek-account"></a>
## `@deepseek-ai/dsh-llm-deepseek`
## `@deepseek-ai/dsh-llm-deepseek-account`
需要: `llm`
```ts config-catalog
/**
* Plugin config, validated by the same-named schemastery schema and doubling
* as the `llm-deepseek` settings-section shape. Every field is optional in
* yml: a missing API key resolves through {@link Config.apiKeyEnv} at each
* request (a request without any key fails with `MISSING_CREDENTIAL`, not at
* plugin load), omitted thinking mode uses the provider default, and omitted
* reasoning effort resolves to `high`.
*/
export interface Config {
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
apiKeyEnv: Volatile<string>
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
baseURL: Volatile<string | undefined>
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
thinking: Volatile<'enabled' | 'disabled' | undefined>
/** Default thinking effort (default `high`); `off` disables thinking per request. */
reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
maxTokens: Volatile<number>
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
defaultContextWindow: Volatile<number>
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
models: Volatile<DeepSeekCatalogModel[]>
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
streamIdleTimeoutMs: Volatile<number>
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
maxRequestFilesBytes: Volatile<number>
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
maxInlineRequestImageBytes: Volatile<number>
/** Maximum number of represented images per chat request (default 600). */
maxImagesPerRequest: Volatile<number>
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
imageOffloadByteQuantum: Volatile<number>
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
inlineImageOffloadByteQuantum: Volatile<number>
/** Image-count removal step after the request exceeds its count bound (default 20). */
imageOffloadCountQuantum: Volatile<number>
/** Maximum duration of one request-image Files API resolution (default one minute). */
filesApiTimeoutMs: Volatile<number>
/** Explicit lifetime assigned to each uploaded image (default seven days). */
fileExpiresAfterSeconds: Volatile<number>
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
fileRefreshMarginSeconds: Volatile<number>
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
fileQuotaCleanupBatch: Volatile<number>
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
retryPolicy: Volatile<RetryPolicyConfig | undefined>
}
/** Account route configuration; authentication comes exclusively from the account service. */
export type Config = ProtocolConfig
```
/** One optional model entry advertised by the direct-fetch adapter. */
export interface DeepSeekCatalogModel {
/** Wire model id accepted by the configured endpoint. */
id: string
/** Selector label; defaults to {@link id}. */
name?: string
/** Optional selector detail for deployments with similar model variants. */
description?: string
/** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */
contextWindow?: number
/** Per-request output cap for this model; omission falls back to the profile's {@link DeepSeekConnectionOptions.maxTokens}. */
maxTokens?: number
/** Accepted request modalities; omission is text-only. */
inputModalities?: ModelModality[]
/**
* Total-pixel budget replacing the published token-grid projection for one
* deterministic request preview, or the 512-by-512 `low` preset; omission
* projects onto the token grid.
*/
imagePixelBudget?: number | 'low'
/** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */
imageMaxBytes?: number
/**
* `'in-history'` declares that the endpoint reads the latest `system`
* message at any position of the conversation as the complete effective
* system prompt; omission means only a leading system message is read.
*/
systemPromptUpdate?: SystemPromptUpdate
依赖: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts)
来源: [`packages/llm/llm-deepseek-account/src/config.ts:5`](../packages/llm/llm-deepseek-account/src/config.ts)
<a id="deepseek-aidsh-llm-deepseek-api-key"></a>
## `@deepseek-ai/dsh-llm-deepseek-api-key`
需要: `llm`
```ts config-catalog
/** Messages configuration with a per-request API-key reference. */
export interface Config extends ProtocolConfig {
/** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */
apiKeyEnv: Volatile<string>
}
```
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
依赖: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
来源: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
来源: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts)
<a id="deepseek-aidsh-llm-pi-ai"></a>
@@ -4116,7 +4058,7 @@ export interface Config {
- `@deepseek-ai/dsh-acp-app` — 需要 `cmdlineArgs`([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts))
- `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-api-account-controller` — 需要 `deepseekAccount` ([`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts))
- `@deepseek-ai/dsh-api-account-controller` — 需要 `deepseekAccount` · `agents` ([`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts))
- `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
- `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
- `@deepseek-ai/dsh-browser-use`([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
@@ -4261,6 +4203,7 @@ export interface Config {
- `@deepseek-ai/dsh-http-proxy`([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
- `@deepseek-ai/dsh-launch-environment`([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
- `@deepseek-ai/dsh-lazy-require`([`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts))
- `@deepseek-ai/dsh-llm-deepseek`([`packages/llm/llm-deepseek/src/index.ts`](../packages/llm/llm-deepseek/src/index.ts))
- `@deepseek-ai/dsh-llm-mock-server`([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
- `@deepseek-ai/dsh-loader-smoke`([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))
- `@deepseek-ai/dsh-native-command`([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
+2 -2
View File
@@ -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 docs/event-producer-consumer.md
event-producer-consumer.md: b67f1c9b2ab5764e8be7be0bb584de9da4eea103
event-producer-consumer.zh.md: df371af272167b3b5d12ad9cc32abb27eaeea058
event-producer-consumer.md: ff9fc19ab364e61e3dd1847531bfac3377676a3b
event-producer-consumer.zh.md: f1f8d5cd80dd20037d0caba3b3061924c280fb55
+10 -7
View File
@@ -12,7 +12,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
@@ -21,11 +21,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:609`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:589`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:616`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:595`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:602`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
@@ -38,8 +38,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:398`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:368`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:59`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:55`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+15 -12
View File
@@ -14,7 +14,7 @@
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
@@ -23,11 +23,11 @@
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:609`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:589`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:616`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:595`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:602`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:52`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
@@ -40,13 +40,16 @@
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:398`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:368`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) |
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:59`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
| `deepseek-account/session-expired` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:55`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | `account-controller`, `remotes` |
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:150`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
@@ -97,7 +100,7 @@
| `internal/service` | - | [`agent-preset-registry`](../packages/preset/agent-preset-registry), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, [`web`](../packages/web/web) |
| `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |
| `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `speech-to-text` |
| `slots/changed` | `ui-renderer` (`emit`) | - |
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.
维护模式:自动生成:Cordis 事件声明及生产方/监听方关系从仓库 TypeScript Program 中解析。
+2 -2
View File
@@ -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 docs/module-graph.md
module-graph.md: ef70bdf4a002d764a1fca6ffd5e67163e93027aa
module-graph.zh.md: 729b76c88e6d5f3dc1c2abcea0c5852d1b39348f
module-graph.md: 946692fd4b0528444391fca6aa8bffec46eb455c
module-graph.zh.md: 5048c2deab70b62ce9795dc7f6f9e14bae2ca732
+31 -16
View File
@@ -29,6 +29,8 @@ flowchart TD
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
pkg_llm["llm"]
pkg_llm_deepseek["llm-deepseek"]
pkg_llm_deepseek_account["llm-deepseek-account"]
pkg_llm_deepseek_api_key["llm-deepseek-api-key"]
pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm_retry["llm-retry"]
pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
@@ -428,7 +430,6 @@ flowchart TD
pkg_browser_use --> pkg_brand
pkg_computer_use --> pkg_brand
pkg_credentials --> pkg_invariants
pkg_deepseek_account --> pkg_brand
pkg_experimental_inspector --> pkg_client_modules
pkg_experimental_inspector --> pkg_host_webserver
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
@@ -460,8 +461,6 @@ flowchart TD
pkg_web_search_exa --> pkg_web
pkg_web_search_perplexity --> pkg_launch_environment
pkg_web_search_perplexity --> pkg_web
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
pkg_api_remotes --> pkg_scope
pkg_api_terminal_controller --> pkg_subprocess
pkg_attachment_local --> pkg_attachment
@@ -488,10 +487,6 @@ flowchart TD
pkg_app_boot --> pkg_home_paths
pkg_app_boot --> pkg_launch_environment
pkg_app_boot --> pkg_system_prompt
pkg_deepseek_account_platform --> pkg_authorization
pkg_deepseek_account_platform --> pkg_credentials
pkg_deepseek_account_platform --> pkg_deepseek_account
pkg_deepseek_account_platform --> pkg_host_webserver
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
@@ -549,8 +544,6 @@ flowchart TD
pkg_llm_deepseek --> pkg_anonymous_user_id
pkg_llm_deepseek --> pkg_atomic_write
pkg_llm_deepseek --> pkg_attachment
pkg_llm_deepseek --> pkg_credentials
pkg_llm_deepseek --> pkg_deepseek_account
pkg_llm_deepseek --> pkg_deepseek_llm_api_extensions
pkg_llm_deepseek --> pkg_fs
pkg_llm_deepseek --> pkg_home_paths
@@ -609,6 +602,10 @@ flowchart TD
pkg_pwsh_local --> pkg_shell
pkg_pwsh_local --> pkg_subprocess
pkg_pwsh_local --> pkg_timeout
pkg_llm_deepseek_api_key --> pkg_credentials
pkg_llm_deepseek_api_key --> pkg_launch_environment
pkg_llm_deepseek_api_key --> pkg_llm
pkg_llm_deepseek_api_key --> pkg_llm_deepseek
pkg_llm_retry --> pkg_agent
pkg_llm_retry --> pkg_brand
pkg_llm_retry --> pkg_invariants
@@ -641,6 +638,9 @@ flowchart TD
pkg_tmux_context --> pkg_session
pkg_tmux_context --> pkg_session_projection
pkg_tmux_context --> pkg_shell
pkg_deepseek_account --> pkg_agent
pkg_deepseek_account --> pkg_brand
pkg_deepseek_account --> pkg_llm
pkg_commands --> pkg_agent
pkg_commands --> pkg_attachment
pkg_commands --> pkg_brand
@@ -689,6 +689,10 @@ flowchart TD
pkg_workflow --> pkg_invariants
pkg_workflow --> pkg_llm
pkg_workflow --> pkg_session
pkg_llm_deepseek_account --> pkg_deepseek_account
pkg_llm_deepseek_account --> pkg_launch_environment
pkg_llm_deepseek_account --> pkg_llm
pkg_llm_deepseek_account --> pkg_llm_deepseek
pkg_tools --> pkg_agent
pkg_tools --> pkg_invariants
pkg_tools --> pkg_llm
@@ -711,11 +715,18 @@ flowchart TD
pkg_fs_sandbox --> pkg_fs_local
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
pkg_api_account_controller --> pkg_agent
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
pkg_compaction --> pkg_brand
pkg_compaction --> pkg_commands
pkg_compaction --> pkg_invariants
pkg_compaction --> pkg_llm
pkg_compaction --> pkg_session
pkg_deepseek_account_platform --> pkg_authorization
pkg_deepseek_account_platform --> pkg_credentials
pkg_deepseek_account_platform --> pkg_deepseek_account
pkg_deepseek_account_platform --> pkg_host_webserver
pkg_command_feedback --> pkg_anonymous_user_id
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_session
@@ -1203,6 +1214,7 @@ flowchart TD
pkg_api_session_controller --> pkg_client_connection
pkg_api_session_controller --> pkg_client_file_upload
pkg_api_session_controller --> pkg_commands
pkg_api_session_controller --> pkg_credentials
pkg_api_session_controller --> pkg_file_reference
pkg_api_session_controller --> pkg_fs
pkg_api_session_controller --> pkg_llm
@@ -1214,6 +1226,7 @@ flowchart TD
pkg_api_session_controller --> pkg_session_projection_cache
pkg_api_session_controller --> pkg_session_query
pkg_api_session_controller --> pkg_session_title
pkg_api_session_controller --> pkg_settings
pkg_api_session_controller --> pkg_skill
pkg_api_session_controller --> pkg_subagent
pkg_api_session_controller --> pkg_typert_protocol
@@ -1309,7 +1322,7 @@ flowchart TD
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
@@ -1455,7 +1468,6 @@ flowchart TD
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
| [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`brand`](../packages/util/brand) |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) |
@@ -1472,7 +1484,6 @@ flowchart TD
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
@@ -1488,7 +1499,6 @@ flowchart TD
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
@@ -1512,7 +1522,7 @@ flowchart TD
| [`session-turn-outline`](../packages/session/session-turn-outline) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values), [`workspace`](../packages/workspace/workspace) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
@@ -1527,6 +1537,7 @@ flowchart TD
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
@@ -1534,6 +1545,7 @@ flowchart TD
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1544,11 +1556,14 @@ flowchart TD
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
| [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
@@ -1634,7 +1649,7 @@ flowchart TD
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) |
@@ -1647,6 +1662,6 @@ flowchart TD
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+31 -16
View File
@@ -31,6 +31,8 @@ flowchart TD
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
pkg_llm["llm"]
pkg_llm_deepseek["llm-deepseek"]
pkg_llm_deepseek_account["llm-deepseek-account"]
pkg_llm_deepseek_api_key["llm-deepseek-api-key"]
pkg_llm_pi_ai["llm-pi-ai"]
pkg_llm_retry["llm-retry"]
pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
@@ -430,7 +432,6 @@ flowchart TD
pkg_browser_use --> pkg_brand
pkg_computer_use --> pkg_brand
pkg_credentials --> pkg_invariants
pkg_deepseek_account --> pkg_brand
pkg_experimental_inspector --> pkg_client_modules
pkg_experimental_inspector --> pkg_host_webserver
pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
@@ -462,8 +463,6 @@ flowchart TD
pkg_web_search_exa --> pkg_web
pkg_web_search_perplexity --> pkg_launch_environment
pkg_web_search_perplexity --> pkg_web
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
pkg_api_remotes --> pkg_scope
pkg_api_terminal_controller --> pkg_subprocess
pkg_attachment_local --> pkg_attachment
@@ -490,10 +489,6 @@ flowchart TD
pkg_app_boot --> pkg_home_paths
pkg_app_boot --> pkg_launch_environment
pkg_app_boot --> pkg_system_prompt
pkg_deepseek_account_platform --> pkg_authorization
pkg_deepseek_account_platform --> pkg_credentials
pkg_deepseek_account_platform --> pkg_deepseek_account
pkg_deepseek_account_platform --> pkg_host_webserver
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
@@ -551,8 +546,6 @@ flowchart TD
pkg_llm_deepseek --> pkg_anonymous_user_id
pkg_llm_deepseek --> pkg_atomic_write
pkg_llm_deepseek --> pkg_attachment
pkg_llm_deepseek --> pkg_credentials
pkg_llm_deepseek --> pkg_deepseek_account
pkg_llm_deepseek --> pkg_deepseek_llm_api_extensions
pkg_llm_deepseek --> pkg_fs
pkg_llm_deepseek --> pkg_home_paths
@@ -611,6 +604,10 @@ flowchart TD
pkg_pwsh_local --> pkg_shell
pkg_pwsh_local --> pkg_subprocess
pkg_pwsh_local --> pkg_timeout
pkg_llm_deepseek_api_key --> pkg_credentials
pkg_llm_deepseek_api_key --> pkg_launch_environment
pkg_llm_deepseek_api_key --> pkg_llm
pkg_llm_deepseek_api_key --> pkg_llm_deepseek
pkg_llm_retry --> pkg_agent
pkg_llm_retry --> pkg_brand
pkg_llm_retry --> pkg_invariants
@@ -643,6 +640,9 @@ flowchart TD
pkg_tmux_context --> pkg_session
pkg_tmux_context --> pkg_session_projection
pkg_tmux_context --> pkg_shell
pkg_deepseek_account --> pkg_agent
pkg_deepseek_account --> pkg_brand
pkg_deepseek_account --> pkg_llm
pkg_commands --> pkg_agent
pkg_commands --> pkg_attachment
pkg_commands --> pkg_brand
@@ -691,6 +691,10 @@ flowchart TD
pkg_workflow --> pkg_invariants
pkg_workflow --> pkg_llm
pkg_workflow --> pkg_session
pkg_llm_deepseek_account --> pkg_deepseek_account
pkg_llm_deepseek_account --> pkg_launch_environment
pkg_llm_deepseek_account --> pkg_llm
pkg_llm_deepseek_account --> pkg_llm_deepseek
pkg_tools --> pkg_agent
pkg_tools --> pkg_invariants
pkg_tools --> pkg_llm
@@ -713,11 +717,18 @@ flowchart TD
pkg_fs_sandbox --> pkg_fs_local
pkg_fs_sandbox --> pkg_sandbox
pkg_fs_sandbox --> pkg_sandbox_policy
pkg_api_account_controller --> pkg_agent
pkg_api_account_controller --> pkg_deepseek_account
pkg_api_account_controller --> pkg_typert_protocol
pkg_compaction --> pkg_brand
pkg_compaction --> pkg_commands
pkg_compaction --> pkg_invariants
pkg_compaction --> pkg_llm
pkg_compaction --> pkg_session
pkg_deepseek_account_platform --> pkg_authorization
pkg_deepseek_account_platform --> pkg_credentials
pkg_deepseek_account_platform --> pkg_deepseek_account
pkg_deepseek_account_platform --> pkg_host_webserver
pkg_command_feedback --> pkg_anonymous_user_id
pkg_command_feedback --> pkg_commands
pkg_command_feedback --> pkg_session
@@ -1205,6 +1216,7 @@ flowchart TD
pkg_api_session_controller --> pkg_client_connection
pkg_api_session_controller --> pkg_client_file_upload
pkg_api_session_controller --> pkg_commands
pkg_api_session_controller --> pkg_credentials
pkg_api_session_controller --> pkg_file_reference
pkg_api_session_controller --> pkg_fs
pkg_api_session_controller --> pkg_llm
@@ -1216,6 +1228,7 @@ flowchart TD
pkg_api_session_controller --> pkg_session_projection_cache
pkg_api_session_controller --> pkg_session_query
pkg_api_session_controller --> pkg_session_title
pkg_api_session_controller --> pkg_settings
pkg_api_session_controller --> pkg_skill
pkg_api_session_controller --> pkg_subagent
pkg_api_session_controller --> pkg_typert_protocol
@@ -1311,7 +1324,7 @@ flowchart TD
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
@@ -1457,7 +1470,6 @@ flowchart TD
| [`browser-use`](../packages/browser-use/browser-use) | `browser-use` | [`brand`](../packages/util/brand) |
| [`computer-use`](../packages/computer-use/computer-use) | `computer-use` | [`brand`](../packages/util/brand) |
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`brand`](../packages/util/brand) |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
| [`anonymous-user-id`](../packages/identity/anonymous-user-id) | `identity` | [`brand`](../packages/util/brand), [`home-paths`](../packages/util/home-paths) |
@@ -1474,7 +1486,6 @@ flowchart TD
| [`web-fetch-http`](../packages/web/web-fetch-http) | `web` | [`http-proxy`](../packages/util/http-proxy), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`launch-environment`](../packages/util/launch-environment), [`web`](../packages/web/web) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`scope`](../packages/core/scope) |
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
@@ -1490,7 +1501,6 @@ flowchart TD
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection) |
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`client-connection`](../packages/client/connection), [`host-webserver`](../packages/host/webserver) |
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
@@ -1514,7 +1524,7 @@ flowchart TD
| [`session-turn-outline`](../packages/session/session-turn-outline) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values), [`workspace`](../packages/workspace/workspace) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
@@ -1529,6 +1539,7 @@ flowchart TD
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
@@ -1536,6 +1547,7 @@ flowchart TD
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
| [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1546,11 +1558,14 @@ flowchart TD
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`llm-deepseek-account`](../packages/llm/llm-deepseek-account) | `llm` | [`deepseek-account`](../packages/credentials/deepseek-account), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/interaction/user-approval) |
| [`command-goal`](../packages/goal/command-goal) | `goal` | [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm) |
| [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`api-account-controller`](../packages/api/account-controller) | `api` | [`agent`](../packages/core/agent), [`deepseek-account`](../packages/credentials/deepseek-account), [`typert-protocol`](../packages/typert/protocol) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | `credentials` | [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`deepseek-account`](../packages/credentials/deepseek-account), [`host-webserver`](../packages/host/webserver) |
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
| [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
@@ -1636,7 +1651,7 @@ flowchart TD
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) |
@@ -1649,6 +1664,6 @@ flowchart TD
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
+2 -2
View File
@@ -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 docs/persistence-catalog.md
persistence-catalog.md: 200e7156f65c4cba6fab6808a84326e85df69ddd
persistence-catalog.zh.md: fa48684d4456c6338b67d58bc051c3672d6ee2c9
persistence-catalog.md: e654091bce16893d1d0121add5d83cba88ccf859
persistence-catalog.zh.md: 744bc3e42db5ab98f9af3821a913f1d651cbd219
+2 -2
View File
@@ -5315,7 +5315,7 @@ Array of `number`.
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
Sources: [`packages/api/session-controller/src/types.ts:396`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -7248,7 +7248,7 @@ Sources: [`packages/core/tools/src/index.ts:34`](../packages/core/tools/src/inde
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
Sources: [`packages/api/session-controller/src/types.ts:401`](../packages/api/session-controller/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:403`](../packages/api/session-controller/src/types.ts)
| Property | Presence | Type |
|---|---|---|
+2 -2
View File
@@ -5317,7 +5317,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
来源:[`packages/api/session-controller/src/types.ts:396`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -7250,7 +7250,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
来源:[`packages/api/session-controller/src/types.ts:401`](../packages/api/session-controller/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:403`](../packages/api/session-controller/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+2 -2
View File
@@ -44608,7 +44608,7 @@
"packages/workflow/workflow/src/types.ts#WorkflowRunId"
],
"sources": [
"packages/api/session-controller/src/types.ts:396",
"packages/api/session-controller/src/types.ts:398",
"packages/attachment/attachment/src/brand.ts:6",
"packages/compaction/compaction/src/brand.ts:4",
"packages/core/session/src/types.ts:20",
@@ -58191,7 +58191,7 @@
},
"names": [],
"sources": [
"packages/api/session-controller/src/types.ts:401"
"packages/api/session-controller/src/types.ts:403"
]
},
{
+2 -2
View File
@@ -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 docs/subsystems/credentials.md
credentials.md: b4a6d035d5833539d033669ba1b6884b402d4fe7
credentials.zh.md: 6ebe1cfc196ca1b124ccd69fe3fcae8a539e8b0e
credentials.md: 341c554f3a59fa87a6a783edc379aeb1f9c1c985
credentials.zh.md: 33994d405fda20aae2bd5785e2a7a408cc40b370
+57 -1
View File
@@ -301,7 +301,7 @@ abstract startSignIn(locale: string, callbackOrigin: string, loginSource: 'web'
abstract cancelSignIn(id: SignInAttemptId): Promise<AccountView>
/**
* Remove the local grant while retaining API keys and tasks; the provider revokes it in the background.
* Remove the local grant while retaining API keys; the provider revokes it in the background.
* @returns the signed-out state after local removal; remote failures never restore the grant.
*/
abstract signOut(): Promise<AccountView>
@@ -320,6 +320,13 @@ abstract watch(signal: AbortSignal): AsyncIterable<AccountView>
*/
abstract resolveToken(url: string): Promise<string | undefined>
/**
* Remove an inference-rejected token only while it still matches the stored login.
* @param token - token captured by the rejected inference request.
* @returns after matching credentials are removed and the expiry notification is emitted.
*/
abstract rejectToken(token: string): Promise<void>
/**
* Read credentials for the configured Platform origin, bound to their issuing environment.
* @returns a Host-only snapshot, or null while signed out.
@@ -402,6 +409,55 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o
```
Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credentials/credentials/src/types.ts)
<a id="deepseek-account-events"></a>
### `deepseek-account/*` events
<a id="deepseek-accountmodel-sign-in-required--emit"></a>
#### `deepseek-account/model-sign-in-required` — emit
An account model request requires the user to sign in.
```ts cordis-catalog
/** An account model request requires the user to sign in.
* @mode emit
*/
'deepseek-account/model-sign-in-required'(): void
```
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
<a id="deepseek-accountsession-expired--emit"></a>
#### `deepseek-account/session-expired` — emit
Server rejection removed the current account credential; this notification is not replayed.
```ts cordis-catalog
/** Server rejection removed the current account credential; this notification is not replayed.
* @mode emit
*/
'deepseek-account/session-expired'(): void
```
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
<a id="deepseek-accountsigned-out--emit"></a>
#### `deepseek-account/signed-out` — emit
Local grant removal has completed.
```ts cordis-catalog
/** Local grant removal has completed.
* @mode emit
*/
'deepseek-account/signed-out'(): void
```
Source: [`packages/credentials/deepseek-account/src/index.ts`](../../packages/credentials/deepseek-account/src/index.ts)
<!-- END GENERATED cordis-surface -->
The account Service Definition exposes getState, getProfile, getBalance, startSignIn, cancelSignIn, signOut, watch, and Host-only resolveToken and getPlatformSession. The platform provider implements it with an AuthorizationFlow and a private GrantRecord. AccountView distinguishes stored presence from server validation; attempt IDs bind cancellation to one local flow. See [the account package](../../packages/credentials/deepseek-account/README.md).
+57 -1
View File
@@ -301,7 +301,7 @@ abstract startSignIn(locale: string, callbackOrigin: string, loginSource: 'web'
abstract cancelSignIn(id: SignInAttemptId): Promise<AccountView>
/**
* Remove the local grant while retaining API keys and tasks; the provider revokes it in the background.
* Remove the local grant while retaining API keys; the provider revokes it in the background.
* @returns the signed-out state after local removal; remote failures never restore the grant.
*/
abstract signOut(): Promise<AccountView>
@@ -320,6 +320,13 @@ abstract watch(signal: AbortSignal): AsyncIterable<AccountView>
*/
abstract resolveToken(url: string): Promise<string | undefined>
/**
* Remove an inference-rejected token only while it still matches the stored login.
* @param token - token captured by the rejected inference request.
* @returns after matching credentials are removed and the expiry notification is emitted.
*/
abstract rejectToken(token: string): Promise<void>
/**
* Read credentials for the configured Platform origin, bound to their issuing environment.
* @returns a Host-only snapshot, or null while signed out.
@@ -402,6 +409,55 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o
```
Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credentials/credentials/src/types.ts)
<a id="deepseek-account-events"></a>
### `deepseek-account/*` events
<a id="deepseek-accountmodel-sign-in-required--emit"></a>
#### `deepseek-account/model-sign-in-required` — emit
An account model request requires the user to sign in.
```ts cordis-catalog
/** An account model request requires the user to sign in.
* @mode emit
*/
'deepseek-account/model-sign-in-required'(): void
```
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
<a id="deepseek-accountsession-expired--emit"></a>
#### `deepseek-account/session-expired` — emit
Server rejection removed the current account credential; this notification is not replayed.
```ts cordis-catalog
/** Server rejection removed the current account credential; this notification is not replayed.
* @mode emit
*/
'deepseek-account/session-expired'(): void
```
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
<a id="deepseek-accountsigned-out--emit"></a>
#### `deepseek-account/signed-out` — emit
Local grant removal has completed.
```ts cordis-catalog
/** Local grant removal has completed.
* @mode emit
*/
'deepseek-account/signed-out'(): void
```
Source: [`packages/credentials/deepseek-account/src/index.ts`](../../packages/credentials/deepseek-account/src/index.ts)
<!-- END GENERATED cordis-surface -->
账号服务定义提供 getState、getProfile、getBalance、startSignIn、cancelSignIn、signOut、watch 及仅限 Host 的 resolveToken 和 getPlatformSession。平台提供者使用 AuthorizationFlow 和私有 GrantRecord 实现这些操作。AccountView 区分本地存在与服务器验证;尝试 ID 将取消绑定到单次本地流程。参见[账号包](../../packages/credentials/deepseek-account/README.zh.md)。
+2 -2
View File
@@ -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 docs/subsystems/llm-streaming.md
llm-streaming.md: 07fc6599bc5d4eb5a4af9b566032004932052585
llm-streaming.zh.md: 079a33599d1e5169af6402db2b2abdd7dc978369
llm-streaming.md: 6ff93698cb877591fb85bc1a05d1dabc95b6561b
llm-streaming.zh.md: 400fcd5a96de0097ec827055fe6f96fc3eef0846
+5 -3
View File
@@ -837,8 +837,9 @@ declare abstract class LlmAdapter {
imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
/**
* List models this adapter can currently advertise for one owned provider.
* The result is advisory: an adapter may accept unlisted model ids, and
* consumers must not turn absence into request rejection.
* Core routing accepts unlisted model ids; catalog-driven entry points such
* as the GUI may require membership. Adapters used there must advertise
* their available models; the base empty catalog offers no GUI selection.
* @param _provider - one provider route owned by this adapter.
* @returns discoverable models in adapter-preferred order.
*/
@@ -1014,7 +1015,8 @@ fileRequestText(ref: FileAttachmentRef): string
/**
* Discover models advertised by one registered provider. Catalog membership
* is advisory and never changes routing or request validation.
* does not constrain core routing. Catalog-driven entry points may restrict
* selection and submission to the advertised models.
* @param provider - registered provider route to inspect.
* @returns detached model metadata in adapter-preferred order.
*/
+5 -3
View File
@@ -843,8 +843,9 @@ declare abstract class LlmAdapter {
imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
/**
* List models this adapter can currently advertise for one owned provider.
* The result is advisory: an adapter may accept unlisted model ids, and
* consumers must not turn absence into request rejection.
* Core routing accepts unlisted model ids; catalog-driven entry points such
* as the GUI may require membership. Adapters used there must advertise
* their available models; the base empty catalog offers no GUI selection.
* @param _provider - one provider route owned by this adapter.
* @returns discoverable models in adapter-preferred order.
*/
@@ -1020,7 +1021,8 @@ fileRequestText(ref: FileAttachmentRef): string
/**
* Discover models advertised by one registered provider. Catalog membership
* is advisory and never changes routing or request validation.
* does not constrain core routing. Catalog-driven entry points may restrict
* selection and submission to the advertised models.
* @param provider - registered provider route to inspect.
* @returns detached model metadata in adapter-preferred order.
*/
+2 -2
View File
@@ -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 docs/subsystems/session.md
session.md: be48d4833bacbac31f3765e0b4d9cdcf6da9f4d7
session.zh.md: 093fc6d570e93dc5be42d8779fc4d5965085b7e0
session.md: e55ebf703c5bd256198687b868831f1741c30906
session.zh.md: 4143f2366c3b69395b72e70afa1552245337f8de
+6
View File
@@ -820,6 +820,12 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
*/
@Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>
/**
* Select the first available account model after login when no provider API key is configured.
* @returns after saving the first available model or retaining the existing default.
*/
@Remote async initializeDefaultModel(): Promise<void>
/**
* Describe every currently routable model for Host-generation selectors.
* @returns provider-grouped models, the deployment default, and isolated provider failures.
+6
View File
@@ -824,6 +824,12 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
*/
@Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>
/**
* Select the first available account model after login when no provider API key is configured.
* @returns after saving the first available model or retaining the existing default.
*/
@Remote async initializeDefaultModel(): Promise<void>
/**
* Describe every currently routable model for Host-generation selectors.
* @returns provider-grouped models, the deployment default, and isolated provider failures.
@@ -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/api/account-controller/README.md
README.md: eb6ef47f91d372a5a7ff895f343ff4a3dcfceb8d
README.zh.md: 3ab666d576e8c1d10b7f439f818a45d8a4ae23b6
README.md: 3cc6a7d9bb090868189cb0901ecdeb7045808394
README.zh.md: 3656b38149188924e7d71abceb52166a47033717
@@ -22,6 +22,10 @@ Account screens use authenticated Remote commands and a snapshot stream. The con
The account namespace exposes getState, getProfile / getBalance, startSignIn, cancelSignIn, signOut, and watch. watch emits an initial complete state and subsequent complete states; disconnecting stops observation, not the login attempt. Cancellation names the attempt ID so a stale screen cannot cancel a newer login.
`watchExpiry` delivers live credential-expiry notifications without an initial item or replay. Desktop uses this stream to hand off a one-shot toast when switching to Welcome.
`hasRunningAccountTasks` checks running Agents through the account-owned predicate over their latest logged request context, including tools and retries. Idle Agents and API-key contexts are excluded. The account provider independently cancels matching tasks when credentials are removed.
<a id="understand-the-implementation"></a>
## Understand the implementation
@@ -22,6 +22,10 @@ kind: "package-reference"
account 命名空间提供 getState、getProfile / getBalance、startSignIn、cancelSignIn、signOut 和 watch。watch 先发送完整初始状态,随后发送完整状态变化;断开连接只停止观察,不取消登录。取消操作必须指定尝试 ID,防止旧页面取消新登录。
`watchExpiry` 仅发送实时凭据失效通知,不发送初始值,也不重放历史通知。桌面端通过该流,在切换到 Welcome 时交接一次性 toast。
`hasRunningAccountTasks` 通过账号模块的判断函数,检查运行中 Agent 最近记录的请求上下文,包括工具和重试阶段。空闲 Agent 及 API key 上下文不计入。移除凭据时,账号提供方独立取消匹配任务。
<a id="understand-the-implementation"></a>
## 理解实现
+4 -2
View File
@@ -49,11 +49,13 @@
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*"
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*"
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
"@deepseek-ai/dsh-agent": "workspace:*"
}
}
+33 -2
View File
@@ -1,13 +1,14 @@
/** Authenticated Remote operations for account UI consumers. */
import { Context } from '@deepseek-ai/cordis'
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import type {} from '@deepseek-ai/dsh-deepseek-account'
import { isRunningAccountTask } from '@deepseek-ai/dsh-deepseek-account'
import type {} from '@deepseek-ai/dsh-agent'
import type { AccountDetails } from '@deepseek-ai/dsh-deepseek-account/types'
import type { AccountView, SignInAttemptId } from './types.ts'
/** Account commands and reconnect-safe state stream. */
export class AccountController extends TypertRemoteService {
static inject = ['deepseekAccount']
static inject = ['deepseekAccount', 'agents']
/** @param ctx - Host with the account provider mounted. */
constructor(ctx: Context) { super(ctx, 'accountController', { namespace: 'account' }) }
/**
@@ -44,12 +45,42 @@ export class AccountController extends TypertRemoteService {
*/
@Remote
cancelSignIn(attemptId: SignInAttemptId): Promise<AccountView> { return this.ctx.deepseekAccount.cancelSignIn(attemptId) }
/**
* Inspect the latest logged request providers of running tasks, including tools and retries.
* @returns whether running work has a latest request context on the account route.
*/
@Remote
hasRunningAccountTasks(): boolean {
return this.ctx.agents.list().some(isRunningAccountTask)
}
/**
* Remove the local account grant and revoke it through Platform in the background, without deleting API keys.
* @returns state after removing the local account grant.
*/
@Remote
signOut(): Promise<AccountView> { return this.ctx.deepseekAccount.signOut() }
/**
* Subscribe to credential expiry without replaying prior notifications.
* @param signal - stream lifetime.
* @returns notifications emitted while subscribed.
*/
@Remote({ mode: 'stream' })
async *watchExpiry(signal: AbortSignal): AsyncIterable<'session-expired'> {
let pending = 0
let wake: (() => void) | undefined
const stop = this.ctx.on('deepseek-account/session-expired', () => { pending++; wake?.() })
const abort = (): void => { wake?.() }
signal.addEventListener('abort', abort, { once: true })
try {
while (!signal.aborted) {
if (pending > 0) { pending--; yield 'session-expired'; continue }
await new Promise<void>((resolve) => { wake = resolve })
}
} finally {
stop()
signal.removeEventListener('abort', abort)
}
}
/**
* Stream the safe account projection.
* @param signal - stream lifetime.
@@ -22,7 +22,7 @@ function fixture() {
watch: vi.fn<DeepSeekAccount['watch']>(),
}
ctx.provide('deepseekAccount', provider as never)
return { provider, controller: new AccountController(ctx) }
return { ctx, provider, controller: new AccountController(ctx) }
}
it('delegates account operations without coupling profile and balance queries', async () => {
@@ -54,3 +54,20 @@ it('passes the subscriber lifetime to the provider and returns its state stream'
for await (const value of stream) received.push(value)
expect(received).toEqual([state])
})
it('delivers expiry only to current subscribers and stops on disconnect', async () => {
const { ctx, controller } = fixture()
ctx.emit('deepseek-account/session-expired')
const lifetime = new AbortController()
const iterator = controller.watchExpiry(lifetime.signal)[Symbol.asyncIterator]()
const first = iterator.next()
ctx.emit('deepseek-account/session-expired')
expect(await first).toEqual({ done: false, value: 'session-expired' })
const next = iterator.next()
lifetime.abort()
expect(await next).toEqual({ done: true, value: undefined })
const reconnect = new AbortController()
const reopened = controller.watchExpiry(reconnect.signal)[Symbol.asyncIterator]().next()
reconnect.abort()
expect(await reopened).toEqual({ done: true, value: undefined })
})
@@ -0,0 +1,24 @@
/** Sign-out impact follows the latest logged provider of running tasks. */
import { Context } from '@deepseek-ai/cordis'
import { expect, it } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { AccountController } from '../src/index.ts'
it('reports running account requests and excludes idle or unbound tasks', async () => {
const ctx = new Context()
const active: Array<Pick<Agent, 'status' | 'session'>> = []
const task = (status: Agent['status'], provider?: string): Pick<Agent, 'status' | 'session'> => ({
status, session: { requestContext: () => provider === undefined ? undefined : { provider, model: 'model' } } as Agent['session'],
})
ctx.provide('agents', { list: () => active } as Context['agents'])
const controller = new AccountController(ctx)
try {
expect(controller.hasRunningAccountTasks()).toBe(false)
active.push(task('running'), task('running', 'deepseek-official'), task('idle', 'deepseek-account'))
expect(controller.hasRunningAccountTasks()).toBe(false)
active.push(task('running', 'deepseek-account'))
expect(controller.hasRunningAccountTasks()).toBe(true)
active.pop()
expect(controller.hasRunningAccountTasks()).toBe(false)
} finally { await ctx.fiber.dispose() }
})
@@ -19,6 +19,9 @@
},
{
"path": "../../typert/protocol"
},
{
"path": "../../core/agent"
}
]
}
+2 -1
View File
@@ -97,6 +97,7 @@
"@deepseek-ai/dsh-plugin-manager": "workspace:*",
"@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*",
"@deepseek-ai/dsh-office-to-pdf": "workspace:*",
"@deepseek-ai/dsh-api-account-controller": "workspace:*"
"@deepseek-ai/dsh-api-account-controller": "workspace:*",
"@deepseek-ai/dsh-deepseek-account": "workspace:*"
}
}
@@ -7,6 +7,7 @@
*/
import type {} from '@deepseek-ai/dsh-api-session-controller/remote-events'
import type {} from '@deepseek-ai/dsh-deepseek-account/types'
import type {} from '@deepseek-ai/dsh-permission-presets/types'
import type {} from '@deepseek-ai/dsh-plugin-manager/types'
import type { TypertForwardableEventEntry } from '@deepseek-ai/dsh-typert-protocol'
@@ -24,6 +25,9 @@ export const API_REMOTE_FORWARDED_EVENTS = [
{ event: 'api-session/removed', mode: 'emit' },
{ event: 'api-session/status', mode: 'emit' },
{ event: 'commands/change', mode: 'emit' },
{ event: 'deepseek-account/session-expired', mode: 'emit' },
{ event: 'deepseek-account/model-sign-in-required', mode: 'emit' },
{ event: 'credentials/record-updated', mode: 'emit' },
{ event: 'credentials/reference-updated', mode: 'emit' },
{ event: 'goal/activation-changed', mode: 'emit' },
{ event: 'cordis/request-run', mode: 'emit' },
@@ -100,6 +100,9 @@
},
{
"path": "../account-controller"
},
{
"path": "../../credentials/deepseek-account"
}
]
}
+3
View File
@@ -82,6 +82,9 @@
},
{
"path": "../account-controller"
},
{
"path": "../../credentials/deepseek-account"
}
]
}
@@ -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/api/session-controller/README.md
README.md: 728d98b90aa96829a878b071c03af3ec961c839e
README.zh.md: 9a2c54b6865302e69daa5f67ec9baa17255f8100
README.md: 476eb9bb1acc356000380d0e4e50dfcc30c91f34
README.zh.md: 91e563c3a833e3a2922b318e342f2de1f3a55cc2
+1 -1
View File
@@ -78,7 +78,7 @@ References keep local Session data, scoped Contexts, and history streams alive,
`SessionMediaReferences` mounts `GET|HEAD /api/file?path=<absolute path>` on the authenticated `connection.fetch` channel when `connection`, `fs`, and `attachments` are composed. It reads ordinary files through `ctx.fs`, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; `mime-types` supplies the response type, with `application/octet-stream` for unknown extensions. GET reuses `readBytes` for preflight and ongoing byte limits; HEAD reads metadata only. All files use `ctx.attachments.imageLimits.maxImageBytes` (normally 20 MiB); exceeding this limit returns 413. Responses contain the complete file, ignore Range, and carry `private, no-store`, `nosniff`, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in `ui-chat` (`AssistantMarkdown`); audio/video responses are available, while Markdown audio/video player nodes remain separate work.
`workspacePathApplications({ path })` returns the serving desktop’s file handlers after the same filesystem mapping check used by `openWorkspacePath`. An optional `application` in the open request selects a currently registered handler without changing the system default. Application names, default selection, icons, and platform coverage come from [native-command](../../util/native-command/README.md). These operations do not activate an Agent or append Session events. Native failures return bounded messages; original command errors remain Host-side causes.
GUI model selection requires the exact provider/model pair in the available catalog; unavailable choices reject with `session/model-unavailable`. Prompt admission retains the saved route without a catalog availability gate, so request execution reports missing credentials or unavailable models. `initializeDefaultModel()` saves the first available account model after login when no other provider has a configured API key; credential checks use configured references independently of model availability. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection.
-----
+1 -1
View File
@@ -78,7 +78,7 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以
当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 20 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。
`workspacePathApplications({ path })` 使用与 `openWorkspacePath` 相同的文件系统映射校验,返回服务端桌面上该文件的关联应用。打开请求中的可选 `application` 指定当前关联的应用,不修改系统默认应用。应用名称、默认项、图标和平台支持范围由 [native-command](../../util/native-command/README.zh.md) 提供。这些操作不激活 Agent,也不追加会话事件。原生操作失败时返回简短消息,原始命令异常作为 Host 端原因保留。
GUI 模型选择要求确切提供方/模型对出现在可用目录中;不可用的选择以 `session/model-unavailable` 拒绝。提示词准入保留已保存的路由,不按目录可用性阻断发送,由请求执行报告凭据缺失或模型不可用。`initializeDefaultModel()` 在账号登录后、其他提供方均未配置 API key 时,将第一个可用账号模型保存为默认模型;凭据检查使用已配置的引用,不依赖模型是否可用。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。
-----
@@ -85,6 +85,7 @@
"@deepseek-ai/dsh-client-connection": "workspace:*",
"@deepseek-ai/dsh-client-file-upload": "workspace:*",
"@deepseek-ai/dsh-commands": "workspace:*",
"@deepseek-ai/dsh-credentials": "workspace:*",
"@deepseek-ai/dsh-file-reference": "workspace:*",
"@deepseek-ai/dsh-fs": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
@@ -96,6 +97,7 @@
"@deepseek-ai/dsh-session-projection-cache": "workspace:*",
"@deepseek-ai/dsh-session-query": "workspace:*",
"@deepseek-ai/dsh-session-title": "workspace:*",
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-skill": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
@@ -111,6 +113,9 @@
},
"@deepseek-ai/dsh-session-projection-cache": {
"optional": true
},
"@deepseek-ai/dsh-settings": {
"optional": true
}
},
"devDependencies": {
@@ -127,6 +132,7 @@
"@deepseek-ai/dsh-client-store": "workspace:*",
"@deepseek-ai/dsh-client-test-runtime": "workspace:*",
"@deepseek-ai/dsh-commands": "workspace:*",
"@deepseek-ai/dsh-credentials": "workspace:*",
"@deepseek-ai/dsh-file-reference": "workspace:*",
"@deepseek-ai/dsh-fs": "workspace:*",
"@deepseek-ai/dsh-fs-local": "workspace:*",
@@ -141,6 +147,7 @@
"@deepseek-ai/dsh-session-projection-cache": "workspace:*",
"@deepseek-ai/dsh-session-query": "workspace:*",
"@deepseek-ai/dsh-session-title": "workspace:*",
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-skill": "workspace:*",
"@deepseek-ai/dsh-storage": "workspace:*",
"@deepseek-ai/dsh-storage-domain": "workspace:*",
+52 -3
View File
@@ -1,5 +1,9 @@
/** Shared projection of the live LLM registry into the browser model catalog. */
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import type {} from '@deepseek-ai/dsh-settings'
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm'
import type { Context } from '@deepseek-ai/cordis'
import type {
ModelCatalog,
@@ -57,11 +61,56 @@ export async function buildModelCatalog(
}
}
}))
const groups = catalog.flatMap(item => item.kind === 'group' ? [item.group] : [])
.filter(group => group.models.length > 0)
return {
default: { ...defaultSelection },
routableProviders: providers.map(provider => provider.id),
groups: catalog.flatMap(item => item.kind === 'group' ? [item.group] : [])
.filter(group => group.models.length > 0),
routableProviders: groups.map(group => group.id),
groups,
failures: catalog.flatMap(item => item.kind === 'failure' ? [item.failure] : []),
}
}
/**
* Check a GUI selection against the current available provider catalog.
* @param ctx - Host LLM registry.
* @param selection - stored or explicitly requested selection.
* @returns whether the exact model is currently advertised as available.
*/
export async function modelAvailable(ctx: Context, selection: ModelSelection): Promise<boolean> {
if (!ctx.llm.listProviders().some(provider => provider.id === selection.provider)) return false
let models: readonly LlmModelInfo[]
try { models = await ctx.llm.listModels(selection.provider) }
catch (error) {
throw new RemoteError('session/model-unavailable',
error instanceof Error ? error.message : String(error),
{ provider: selection.provider, model: selection.model })
}
return models.some(model => model.id === selection.model)
}
/**
* Check configured provider API-key references independently of model availability.
* @param ctx - Host registry, settings, and credential services.
* @returns whether any API-key provider has a configured credential.
*/
export async function hasProviderApiKey(ctx: Context): Promise<boolean> {
const settings = ctx.get('settings')
const credentials = ctx.get('credentials')
if (settings === undefined || credentials === undefined) {
throw new RemoteError('session/provider-credentials-unavailable', 'provider credentials are unavailable', {})
}
const namespaces = settings.describe({ redactSecrets: true })
for (const provider of ctx.llm.listConfigurableProviders()) {
if (provider.provider === 'deepseek-account') continue
let profile = namespaces.find(namespace => namespace.ns === provider.settingsNs)?.value
for (const key of provider.settingsPath) {
profile = typeof profile === 'object' && profile !== null ? Reflect.get(profile, key) : undefined
}
if (typeof profile !== 'object' || profile === null) continue
const ref: unknown = Reflect.get(profile, 'apiKeyEnv')
if (typeof ref === 'string' && ref.length > 0
&& (await credentials.describe(credentialRef(ref))).configured) return true
}
return false
}
@@ -1,5 +1,6 @@
/** Session commands whose activation policy is explicit at each Remote method. */
import { modelAvailable } from './catalog.ts'
import { randomUUID } from 'node:crypto'
import type { Context } from '@deepseek-ai/cordis'
import { brandString } from '@deepseek-ai/dsh-brand'
@@ -151,6 +152,7 @@ export class SessionCommandController {
const agent = await this.resolveAgent(request.sessionId)
return this.agents.serializeImageAdmission(agent, async () => {
try {
await this.requireModel(request)
const resolved = await this.ctx.llm.resolveCallConfig({
provider: request.provider,
model: request.model,
@@ -328,14 +330,6 @@ export class SessionCommandController {
}
const agent = await this.resolveAgent(request.sessionId)
if (hasPromptRequest(agent, request.requestId)) return { accepted: true }
const selection = this.agents.selectionFor(agent).current
if (!routeServed(this.ctx, selection.provider)) {
throw new RemoteError(
'session/model-unavailable',
`no adapter serves provider "${selection.provider}"; select a model for this session`,
{ provider: selection.provider, model: selection.model },
)
}
const source: MessageSource = {
kind: 'user',
rpcId: request.requestId,
@@ -384,6 +378,13 @@ export class SessionCommandController {
return hasImage ? this.agents.serializeImageAdmission(agent, admit) : admit()
}
private async requireModel(selection: Pick<AgentModelSelection, 'provider' | 'model'>): Promise<void> {
if (!await modelAvailable(this.ctx, selection)) {
throw new RemoteError('session/model-unavailable', 'Select an available model before sending a message.',
{ provider: selection.provider, model: selection.model })
}
}
/**
* Read one durable image after proving the Session log references it.
* @param request - Session and attachment identities used for authorization.
@@ -690,7 +691,3 @@ function referencedImage(
}
return undefined
}
function routeServed(ctx: Context, provider: string): boolean {
return ctx.llm.listProviders().some(entry => entry.id === provider)
}
+20 -2
View File
@@ -6,7 +6,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-fs'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { errorChain } from '@deepseek-ai/dsh-llm'
import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-client-file-upload'
import { canOpenNativePath, nativeFileManager, nativeFileApplications, openNativeFileApplication, openNativeAssociatedPath, revealNativePath } from '@deepseek-ai/dsh-native-command'
import type { SessionId } from '@deepseek-ai/dsh-session'
@@ -23,7 +23,7 @@ import { SessionControlController } from './control.ts'
import { SessionHistoryController } from './history.ts'
import { SessionFileReferences } from './file-references.ts'
import { ApiSessionList } from './list.ts'
import { buildModelCatalog } from './catalog.ts'
import { buildModelCatalog, hasProviderApiKey } from './catalog.ts'
import { installModelSelectionProjection } from './model-selection-projection.ts'
import { SessionSkillCatalog } from './skill-catalog.ts'
import { SessionMediaReferences } from './media-references.ts'
@@ -284,6 +284,24 @@ export class SessionController extends TypertRemoteService {
return this.commands.selectModel(request)
}
/**
* Select the first available account model after login when no provider API key is configured.
* @returns after saving the first available model or retaining the existing default.
*/
@Remote
async initializeDefaultModel(): Promise<void> {
const provider = 'deepseek-account'
if (await hasProviderApiKey(this.ctx)) return
const catalog = await buildModelCatalog(this.ctx)
const model = catalog.groups.find(group => group.id === provider)?.models[0]
if (model === undefined) throw new RemoteError('session/provider-models-unavailable',
`provider "${provider}" has no available models`, { provider })
const selection = { provider, model: model.id,
...model.reasoning?.defaultEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(model.reasoning.defaultEffort) },
}
await this.ctx.agentDefaultModel.saveSelection(selection)
}
/**
* Describe every currently routable model for Host-generation selectors.
* @returns provider-grouped models, the deployment default, and isolated provider failures.
+3 -1
View File
@@ -157,7 +157,7 @@ export interface ModelCatalogFailure {
/** Host-generation model catalog and the default used by unconfigured Sessions. */
export interface ModelCatalog {
readonly default: ModelSelection
/** Provider routes currently able to serve a request, including empty catalogs. */
/** Provider routes with at least one currently available catalog model. */
readonly routableProviders: readonly string[]
readonly groups: readonly ModelProviderGroup[]
readonly failures: readonly ModelCatalogFailure[]
@@ -201,6 +201,8 @@ export const SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS = 240
declare module '@deepseek-ai/dsh-typert-protocol' {
interface RemoteErrorDetailsMap {
'session/provider-credentials-unavailable': Record<string, never>
'session/provider-models-unavailable': { readonly provider: string }
'session/model-unavailable': { readonly provider: string; readonly model: string }
'session/conflict': {
readonly sessionId: SessionId
@@ -83,6 +83,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
},
} as never)
ctx.provide('llm', {
listModels: async () => [{ id: 'fixture-model', name: 'Model' }],
listProviders: () => [{ id: 'fixture', name: 'Fixture' }],
resolveModelInfo: () => Promise.resolve({ provider: 'fixture', id: 'fixture-model', name: 'Fixture' }),
} as never)
@@ -830,6 +830,7 @@ describe('sessions.prompt synchronous rejection', () => {
AttachmentStore.prototype,
) as never)
ctx.provide('llm', {
listModels: async () => [{ id: 'm', name: 'Model' }],
listProviders: () => [{ id: 'p', name: 'Provider' }],
resolveModelInfo: () => Promise.resolve({
provider: 'p', id: 'm', name: 'Model', inputModalities: ['text', 'image'],
@@ -1,7 +1,7 @@
/**
* Session Controller model-directory and selection behavior: dynamic provider grouping,
* provider-local catalog failures, logged-selection restoration without stale
* catalog injection, advisory pass-through models, and the prompt-assembly
* catalog injection, unavailable-model refusals, and the prompt-assembly
* boundary for a running selection change.
*/
@@ -20,10 +20,10 @@ import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
import { ApiSessionAgentController } from '../src/agent.ts'
import { buildModelCatalog } from '../src/catalog.ts'
import { buildModelCatalog, hasProviderApiKey } from '../src/catalog.ts'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import { createSessionTestRemote } from './test-remote.ts'
import { createSessionTestController, createSessionTestRemote } from './test-remote.ts'
function request<P>(payload: P): P {
return payload
@@ -88,12 +88,11 @@ async function harness(logged?: {
model: string
reasoningEffort?: ReasoningEffortId
adapterDefaults?: LlmCallConfigAdapterDefaults
}): Promise<{
}, ctx = new Context()): Promise<{
ctx: Context
agent: Agent
sessionId: SessionId
}> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(LlmRuntime)
@@ -146,7 +145,7 @@ function registerTextOnly(ctx: Context): void {
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
return Promise.resolve({ provider, id: model, name: model, inputModalities: ['text'] })
}
}('Text Only', []))
}('Text Only', [{ provider: 'text-only', id: 'plain', name: 'Plain' }]))
}
/** Resolve the Client-visible next selection from durable state and the Host default. */
@@ -266,7 +265,6 @@ describe('Web session model selection', () => {
],
}))
expect(result.ok).toBe(true)
expect(followup).not.toHaveBeenCalled()
expect((steer.mock.calls[0]?.[0] as UserMessage).content).toEqual([
{ type: 'text', text: 'look at this' },
{
@@ -347,6 +345,66 @@ describe('Web session model selection', () => {
expect(readImage).toHaveBeenCalledOnce()
await ctx.fiber.dispose()
})
it('checks configured key references even for empty catalogs and skips profiles without keys', async () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
const describe = vi.fn(async () => ({ configured: true, writable: true }))
ctx.provide('credentials', { describe } as never)
ctx.provide('settings', { describe: () => [{ ns: 'profiles', value: {
providers: { blank: { apiKeyEnv: '' }, absent: {}, configured: { apiKeyEnv: 'CUSTOM_KEY' } },
} }] } as never)
const routes = ['missing', 'blank', 'absent', 'configured']
ctx.effect(() => ctx.llm.registerConfigurableProviders(routes.map(provider => ({
provider, displayName: provider, settingsNs: 'profiles',
settingsPath: provider === 'missing' ? ['missing', 'nested'] : ['providers', provider],
}))))
try {
expect(await hasProviderApiKey(ctx)).toBe(true)
expect(describe).toHaveBeenCalledExactlyOnceWith('CUSTOM_KEY')
describe.mockRejectedValueOnce(new Error('credential read failed'))
await expect(hasProviderApiKey(ctx)).rejects.toThrow('credential read failed')
} finally { await ctx.fiber.dispose() }
})
it.each(['settings', 'credentials'] as const)('refuses account initialization without %s inspection', async (missing) => {
const ctx = new Context()
if (missing !== 'settings') ctx.provide('settings', {} as never)
if (missing !== 'credentials') ctx.provide('credentials', {} as never)
try {
await expect(hasProviderApiKey(ctx)).rejects.toMatchObject({ code: 'session/provider-credentials-unavailable' })
} finally { await ctx.fiber.dispose() }
})
it.each([false, true])('account login replaces a saved default only without another API key: %s', async (configuredKey) => {
const { configurationFixture } = await import('../../../settings/settings/tests/configuration-fixture.ts')
const configured = await configurationFixture({ hmr: false })
const { ctx } = await harness(undefined, configured.ctx)
ctx.effect(() => ctx.llm.registerAdapter(['deepseek-account'], new CatalogAdapter('Account', [
{ provider: 'deepseek-account', id: 'first-model', name: 'First' },
{ provider: 'deepseek-account', id: 'second-model', name: 'Second' },
], REASONING)))
const describe = vi.fn(async () => ({ configured: configuredKey, writable: true }))
ctx.provide('credentials', { describe } as never)
ctx.effect(() => ctx.llm.registerConfigurableProviders([
{ provider: 'deepseek-account', displayName: 'Account', settingsNs: 'account', settingsPath: [] },
{ provider: 'removed-model-provider', displayName: 'Custom', settingsNs: 'first', settingsPath: ['providers', 'custom'] },
]))
vi.spyOn(ctx.settings, 'describe').mockReturnValue([{
ns: 'first' as never, autoGenerate: false, schema: {}, revision: 0, applies: 'live',
value: { providers: { custom: { apiKeyEnv: 'CUSTOM_API_KEY', models: [] } } },
}])
await ctx.agentDefaultModel.saveSelection({ provider: 'removed', model: 'saved' })
const controller = createSessionTestController(ctx, {
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(), cwd: '/tmp',
})
await controller.initializeDefaultModel()
expect(describe).toHaveBeenCalledWith('CUSTOM_API_KEY')
expect(ctx.agentDefaultModel.currentSelection()).toEqual(configuredKey
? { provider: 'removed', model: 'saved' }
: { provider: 'deepseek-account', model: 'first-model', reasoningEffort: 'high' })
await ctx.fiber.dispose()
})
it('groups successful providers and leaves an unlisted current selection out of the catalog', async () => {
const { ctx, sessionId } = await harness({
provider: 'deepseek-official',
@@ -428,7 +486,7 @@ describe('Web session model selection', () => {
await ctx.fiber.dispose()
})
it('accepts an advisory-unlisted model, rejects an unavailable provider, and switches only after the next assembly', async () => {
it('rejects unlisted models and switches available models only after the next assembly', async () => {
const { ctx, agent, sessionId } = await harness()
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }
@@ -440,12 +498,12 @@ describe('Web session model selection', () => {
const selected = expectValue(await remote.selectModel(request({
sessionId,
provider: 'deepseek-official',
model: 'private-preview',
model: 'deepseek-reasoner',
reasoningEffort: 'max',
})))
expect(selected.selected).toEqual({
provider: 'deepseek-official',
model: 'private-preview',
model: 'deepseek-reasoner',
reasoningEffort: 'max',
})
await expect(agentEvents(ctx, agent).waterfall(
@@ -453,26 +511,26 @@ describe('Web session model selection', () => {
)).resolves.toEqual(seed)
expect((await ctx.systemPrompt.assemble()).variables)
.toMatchObject({ provider: 'deepseek-official', model: 'private-preview' })
.toMatchObject({ provider: 'deepseek-official', model: 'deepseek-reasoner' })
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', { turn: 1, step: 1, signal }, () => Promise.resolve(seed),
)).resolves.toMatchObject({
provider: 'deepseek-official',
model: 'private-preview',
model: 'deepseek-reasoner',
reasoningEffort: 'max',
})
const unsupported = await remote.selectModel(request({
sessionId,
provider: 'deepseek-official',
model: 'private-preview',
model: 'deepseek-reasoner',
reasoningEffort: 'medium',
}))
expect(unsupported).toMatchObject({
ok: false,
error: {
code: 'session/model-unavailable',
message: 'provider "deepseek-official" model "private-preview" does not support reasoning effort "medium"',
message: 'provider "deepseek-official" model "deepseek-reasoner" does not support reasoning effort "medium"',
},
})
@@ -485,7 +543,7 @@ describe('Web session model selection', () => {
ok: false,
error: {
code: 'session/model-unavailable',
message: 'no adapter registered for provider "missing"',
message: 'Select an available model before sending a message.',
details: { provider: 'missing', model: 'model' },
},
})
@@ -496,13 +554,13 @@ describe('Web session model selection', () => {
}))).toMatchObject({
ok: false,
error: {
code: 'gateway/internal',
message: 'fixture rejected the selection',
details: {},
code: 'session/model-unavailable',
message: 'Select an available model before sending a message.',
details: { provider: 'remote-rejected', model: 'model' },
},
})
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'private-preview', reasoningEffort: 'max' })
.toEqual({ provider: 'deepseek-official', model: 'deepseek-reasoner', reasoningEffort: 'max' })
await ctx.fiber.dispose()
})
@@ -595,34 +653,81 @@ describe('Web session model selection', () => {
await ctx.fiber.dispose()
})
it('refuses a prompt no adapter can route, and reports it on the directory', async () => {
it('admits saved selections without querying the advisory model catalog', async () => {
const { ctx, sessionId, agent } = await harness()
const followup = vi.fn()
Object.assign(agent, { followup })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp',
})
const list = vi.spyOn(ctx.llm, 'listModels')
list.mockRejectedValueOnce(new Error('credential storage offline'))
try {
expect(await remote.prompt(promptRequest({
sessionId, mode: 'queue', content: [{ type: 'text', text: 'hello' }],
}))).toMatchObject({ ok: true, value: { accepted: true } })
expect(list).not.toHaveBeenCalled()
expect(followup).toHaveBeenCalledOnce()
} finally {
list.mockRestore()
await ctx.fiber.dispose()
}
})
it('leaves uncatalogued adapters usable by core callers but unavailable to GUI selection', async () => {
const { ctx, sessionId } = await harness()
ctx.llm.registerAdapter(['uncatalogued'], new class extends LlmAdapter {
override async *stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {}
}())
try {
await expect(ctx.llm.resolveModelInfo('uncatalogued', 'custom-model')).resolves.toMatchObject({ id: 'custom-model' })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'uncatalogued', model: 'custom-model' }), cwd: '/tmp',
})
expect(await remote.selectModel({ sessionId, provider: 'uncatalogued', model: 'custom-model' }))
.toMatchObject({ ok: false, error: { code: 'session/model-unavailable' } })
} finally {
await ctx.fiber.dispose()
}
})
it('admits an unavailable saved route for execution without rewriting the selection', async () => {
const { ctx, sessionId, agent } = await harness()
const followup = vi.fn()
Object.assign(agent, { followup })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }),
cwd: '/tmp',
})
// The client disabling its input is an affordance; this method stays
// callable, so the refusal has to live here.
const refused = await remote.prompt(promptRequest({
const admitted = await remote.prompt(promptRequest({
sessionId, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'hi' }],
}))
expect(refused).toMatchObject({
ok: false,
error: { code: 'session/model-unavailable', details: { provider: 'deleted-gateway', model: 'deleted-model' } },
})
expect(admitted).toMatchObject({ ok: true, value: { accepted: true } })
expect(followup).toHaveBeenCalledOnce()
const unavailableCatalog = await buildModelCatalog(ctx)
expect(unavailableCatalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(false)
// An advisory-unlisted model on a live route is NOT this: the route
// serves it, so the prompt goes through and nothing blocks.
expectValue(await remote.selectModel(request({
expect(await remote.selectModel(request({
sessionId, provider: 'deepseek-official', model: 'unlisted-but-served',
})))
const catalog = await buildModelCatalog(ctx)
expect(catalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(true)
expect(catalog.groups.flatMap(group => group.models.map(model => model.id)))
.not.toContain('unlisted-but-served')
}))).toMatchObject({ ok: false, error: { code: 'session/model-unavailable' } })
expect(currentSelection(ctx, sessionId)).toEqual({ provider: 'deleted-gateway', model: 'deleted-model' })
await ctx.fiber.dispose()
})
it('admits a removed model without changing its saved selection', async () => {
const { ctx, sessionId, agent } = await harness({ provider: 'deepseek-official', model: 'removed-model' })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp',
})
const events = [...agent.session.snapshotEvents()]
const followup = vi.fn()
Object.assign(agent, { followup })
expect(await remote.prompt(promptRequest({ sessionId, mode: 'queue', content: [{ type: 'text', text: 'hi' }] })))
.toMatchObject({ ok: true, value: { accepted: true } })
expect(followup).toHaveBeenCalledOnce()
expect(agent.session.snapshotEvents()).toEqual(events)
expect(currentSelection(ctx, sessionId)).toMatchObject({ provider: 'deepseek-official', model: 'removed-model' })
await ctx.fiber.dispose()
})
@@ -654,13 +759,13 @@ describe('Web session model selection', () => {
provider, id: model, name: model, inputModalities: ['text', 'image'],
})
}
}('Image Capable', []))
}('Image Capable', [{ provider: 'image-capable', id: 'vision', name: 'Vision' }]))
ctx.llm.registerAdapter(['string-error'], new class extends CatalogAdapter {
override resolveModel(): Promise<LlmResolvedModelInfo> {
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- non-Error provider normalization is the scenario.
return Promise.reject('string selection failure')
}
}('String Error', []))
}('String Error', [{ provider: 'string-error', id: 'plain', name: 'Plain' }]))
let saveMode: 'success' | 'error' | 'remote' = 'success'
const savedRef = {
attachmentId: 'saved-image', mediaType: 'image/png' as const, bytes: 1, width: 1, height: 1,
@@ -725,12 +830,12 @@ describe('Web session model selection', () => {
sessionId, provider: 'image-capable', model: 'vision',
})))
expect(await remote.selectModel(request({
sessionId, provider: 'metadata-broken', model: 'broken',
sessionId, provider: 'metadata-broken', model: 'listed',
}))).toMatchObject({
ok: false, error: { code: 'session/model-unavailable', message: 'reasoning metadata offline' },
})
expect(await remote.selectModel(request({
sessionId, provider: 'string-error', model: 'broken',
sessionId, provider: 'string-error', model: 'plain',
}))).toMatchObject({
ok: false,
error: { code: 'session/model-unavailable', message: 'string selection failure' },
@@ -738,3 +843,32 @@ describe('Web session model selection', () => {
await ctx.fiber.dispose()
})
})
it('initializes the account model without reasoning metadata and rejects an empty account catalog', async () => {
const { configurationFixture } = await import('../../../settings/settings/tests/configuration-fixture.ts')
const configured = await configurationFixture({ hmr: false })
const { ctx } = await harness(undefined, configured.ctx)
ctx.provide('credentials', { describe: vi.fn() } as never)
const dispose = ctx.llm.registerAdapter(['deepseek-account'], new CatalogAdapter('Account', [
{ provider: 'deepseek-account', id: 'basic', name: 'Basic' },
]))
const controller = createSessionTestController(ctx, {
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(), cwd: '/tmp',
})
await controller.initializeDefaultModel()
expect(ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-account', model: 'basic' })
dispose()
await expect(controller.initializeDefaultModel()).rejects.toMatchObject({
code: 'session/provider-models-unavailable', details: { provider: 'deepseek-account' },
})
})
it.each([new Error('catalog disconnected'), 'catalog disconnected'])('reports catalog rejection as an unavailable selection: %s', async (failure) => {
const { ctx } = await harness()
const { modelAvailable } = await import('../src/catalog.ts')
const read = vi.spyOn(ctx.llm, 'listModels').mockRejectedValueOnce(failure)
try {
await expect(modelAvailable(ctx, { provider: 'deepseek-official', model: 'deepseek-chat' }))
.rejects.toMatchObject({ code: 'session/model-unavailable', message: 'catalog disconnected' })
} finally { read.mockRestore(); await ctx.fiber.dispose() }
})
@@ -255,6 +255,10 @@ function installControllers(
}
if (ctx.get('llm') === undefined) {
ctx.provide('llm', {
listModels: async () => {
const selection = defaults.defaultModelSelection()
return [{ id: selection.model, name: selection.model }]
},
listProviders: () => {
const selection = defaults.defaultModelSelection()
return [{ id: selection.provider, name: selection.provider }]
+4 -1
View File
@@ -522,4 +522,7 @@
# key coming from the credential store below. Thinking defaults are a deployment
# choice.
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
- id: llm-deepseek-account
name: '@deepseek-ai/dsh-llm-deepseek-account'
+2 -1
View File
@@ -59,7 +59,8 @@
"@deepseek-ai/dsh-goal-round-driver": "workspace:*",
"@deepseek-ai/dsh-api-gateway": "workspace:*",
"@deepseek-ai/dsh-llm": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
"@deepseek-ai/dsh-llm-deepseek-account": "workspace:*",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
"@deepseek-ai/dsh-llm-retry": "workspace:*",
"@deepseek-ai/dsh-mcp-resources": "workspace:*",
Loaded 100 of 366 files, more files were not shown because too many files have changed in this diff. Show more