Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]


### Changed

- **请求路径与账户读取的一批性能优化(不改变线上字节)。**
- `streamAttempt()` 的凭据解析与正文装配改为并发:两者互不依赖(正文从不携带密钥,健康账号池的解析只读内存),从前却是严格串行,带图请求因此把图片读取与 JSON 装配整个排在 `resolveApiKey()` 之后。与密钥无关的通道判断先算成 `provisionalProtocol`,凭据返回后目标一致即复用,不一致才重建一次(与既有协议回退、账号轮换同价)。图片预算的 `withSurfaceOffload()` 仍先于任何 I/O 执行;CLI 专属的「已看过图片」缓存项在拿到密钥后按真实通道再判一次(只可能抛错,不改变档位)。
- `listModels()` 在**本进程已经加载过**该网关目录时不再等一次联网刷新,改用内存中的行立即返回、后台刷新留给下次打开;进程首次加载仍照旧等待。新判据 `GatewayFacts.loaded()` 刻意与 `snapshot()` 区分——后者读磁盘而磁盘缓存没有年龄校验,拿它当首屏会画出不知道多久以前的目录。`resolveModel()` 与 `stream()` 不变,仍等真实刷新。
- `gradeHeaderTimeout()` 的指纹改用**已经序列化好的实发字符串**求摘要(`sha256Hex()`),不再对 `body` 重新 `JSON.stringify`:超时是最常重复的路径,而带图历史可达数 MB。这也更准确——实发字符串就是网关看到的字节,重新 stringify 会在 surrogate 清洗真的删掉过字符时描述到不同的字节,把一次连续超时拆成两条 streak。
- `toolParametersSchema()` 的根节点结果按 `parameters` 对象身份记忆化(`WeakMap`)。每个通道在每次尝试时都重建工具列表,而账号轮换、协议回退、降档重试都会让同一批 schema 再走一遍。同时纠正原注释「每条路径都返回副本」——`type: 'object'` 快路径一直返回调用方自己的对象,调用方靠它拿到同一个已冻结的 schema。
- 用量卡片与选择器的套餐过滤共享账户端点响应:两处读的是同一批端点、同一个账号,成功响应按 `[apiBase, key, 完整 URL]` 记入短命表(`ACCOUNT_FACTS_TTL_MS` 30 秒)。只缓存成功响应(失败必须重试,否则报告的 `failures` 与 `classifyTotalFailure` 结论会描述一次没真正发出的请求);`/alpha/usage/summary` 明确不共享——那是用户正盯着、旁边写着读取时间的数字。
- 用量刷新区分自动与显式路径:`refresh({ auto: true })` 用于账户卡片首屏、面板首屏与面板 tick(injected 面因此分成 `refreshUsageAuto()` 与 `refreshUsage()` 两个入口),可在 `USAGE_AUTO_REFRESH_COALESCE_MS`(5 秒)内复用刚刚读到的结果;按钮、保存、换凭据等显式调用永远重新读取,`fetchedAt` 因此仍如实反映读取时刻。
- 面板自动刷新在文档隐藏时跳过读取但照旧重排定时器,重新可见立即补读一次;首屏仍无条件读取。可见性做成可注入接缝(`AutoRefreshVisibility`),非浏览器环境视为始终可见。

用例:`tests/adapter.test.ts` 新增「凭据解析与图片读取并行」「目录已在内存时不再等一次联网刷新」「超时定级复用已序列化的正文」「同一份工具 schema 只归一化一次」「用量卡片与选择器共享同一份账户事实」;`tests/panel.test.ts` 与 `tests/usage-client.test.ts` 新增自动/显式刷新与可见性用例。上述新增用例在对应实现回退时均失败。**验证边界**:`npm run typecheck` 退出码 0;`npm ci` 干净安装后 `npm test` **1120/1120** 通过;`npm run build` 重建 `lib/` 后无差异(说明提交的 bundle 与源码一致)。未在真实宿主中人工验证。(此前一次本机运行的 16 条 `tests/web-search.test.ts` 失败源于 `node_modules` 与 lockfile 漂移——实装 `@deepseek-ai/dsh-web` `0.2.1-alpha.3` 而 lockfile 钉 `0.2.1-rc.2`,按 lockfile 重装即消失,与本批改动无关。)
## [0.15.2] - 2026-10-23

### Changed
Expand Down
5 changes: 5 additions & 0 deletions docs/agent-reference/accounts-and-retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ Task-specific reference moved from the former root `AGENTS.md`. All source and t
自动回收,改为容量上限 200 条 + 先进先出淘汰。无 `sessionId` 的一次性调用退回旧的单槽全局行为。
`tests/adapter.test.ts` 新增并发会话独立累计、无会话回退两个用例。参见
[《决策记录》第一章第 3 条](../决策记录.md#一请求失败恢复)。
指纹取自**已经序列化好的实发字符串**(`serializedBodies` 里的那份,用 `sha256Hex()` 求摘要),
不再对 `body` 重新 `JSON.stringify`:超时是最常重复的路径,而带图历史可达数 MB,重走整棵 body
只为算第二个意见。这也更准确——实发字符串就是网关看到的字节,重新 stringify 会在 surrogate
清洗真的删掉过字符时描述到不同的字节,把一次连续超时拆成两条 streak。没有实发字符串时
(正文从未上过线)才回落到对象哈希。
- **设置页账号卡片 / 侧边栏统计的"当前使用"徽章必须和真实请求路由共用同一套自愈逻辑,否则它显示的是过期标记**
(`CommandCodeAccountPool.activeAccount()` in `src/accounts.ts`; issue #51 的后续报告:账号卡片和侧边栏
统计一起被"切到另一个账号",且比真实请求恢复得慢得多)。`resolveKey()`(真实请求路径)对**显式选中**
Expand Down
12 changes: 8 additions & 4 deletions docs/agent-reference/adapter-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,14 @@ Task-specific reference moved from the former root `AGENTS.md`. All source and t
a root `$ref` is inlined from its local `$defs`/`definitions`; an `allOf`/`anyOf`/`oneOf` root is
flattened (branch properties unioned, `required` kept only where every alternative demands it); anything
else degrades to a permissive free-form object, since a refused request helps no one. Only the root is
touched and every path returns a copy (the harness may deep-freeze tool schemas), and the walk is
depth-bounded so a self-referential JS schema cannot spin. `toolParametersSchema()` is applied at all
three call sites, so the CLI `input_schema`, the OpenAI `function.parameters` and the Messages
`input_schema` cannot drift apart.
touched; the object-root fast path returns the caller's own object (the harness may deep-freeze tool
schemas, and callers rely on that identity), while everything the function CONSTRUCTS is a fresh object.
The walk is depth-bounded so a self-referential JS schema cannot spin. The depth-0 result is memoized
per `parameters` object in a `WeakMap`, because every transport rebuilds its tool list on every attempt:
a rotation, a protocol fallback or a downshift re-walks the same handful of schemas for an answer that
cannot have changed. Harness tool objects are stable for the life of a turn, so identity is an exact key.
`toolParametersSchema()` is applied at all three call sites, so the CLI `input_schema`, the OpenAI
`function.parameters` and the Messages `input_schema` cannot drift apart.
- Image parts on `/alpha/generate` use the official CLI's FINAL `toWireMessages()` shape: `{ type:
'image', image: 'data:<mime>;base64,...', mimeType: '<mime>' }`. The `{ type: 'image', source: { type:
'base64', media_type, data } }` shape is an internal CLI input block, not what it posts. OpenAI
Expand Down
2 changes: 2 additions & 0 deletions docs/agent-reference/client-and-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Task-specific reference moved from the former root `AGENTS.md`. All source and t
- **凭据事实与销毁**:凭据返回成功即为写入确认,描述仅提供存在性;描述失败或缺失保留最后确认事实,首次尚无确认时明确显示状态未知,重试入口只读状态。按引用及读取世代屏蔽迟到描述。普通切页不关闭共享控制器;插件销毁停止未开始任务,已发调用自然收束,不进入后续阶段。
- **凭据写入不再被环境变量遮蔽(alpha.3)**:凭据服务的 `describe()` 恒返回 `writable: true`,写入不再因环境变量遮蔽而被拒,服务内部的层序同步反转为「托管存储 → 继承环境 → 项目 `.env` → `$DSH_HOME/.env`」。**headless(无头)profile 有意不挂载 `credentials`/`authorization`/`deepseek-account` 三行**,没有凭据服务可读写,一次性任务只认启动环境与 `.env`。密钥解析侧见[账号与重试](accounts-and-retry.md)。
- **自动轮换**:取消固定账号时基础层仍固定账号,则显式写 `activeAccount: ''`;仅清除用户覆盖会重新露出基础层固定账号,不等于自动轮换。普通字段重置仍恢复继承。
- **用量读取的自动与显式路径**:一份用量报告是四次上游调用,而面板与账户卡片都会在无人操作时各自读取。`CommandCodeUsageController.refresh({ auto: true })` 标记「没人点过」的刷新,只有它在 `USAGE_AUTO_REFRESH_COALESCE_MS`(5 秒)内复用刚刚读取的结果;首屏 effect 与面板 tick 都走这条路径。按钮、保存、换/清凭据等显式调用不传 `auto`,永远真的去读。窗口刻意很小,`fetchedAt` 因此仍如实反映数字被读取的时刻。
- **隐藏页面不轮询**:`startPanelAutoRefresh()` 的 tick 先问 `AutoRefreshVisibility.isVisible()`(默认读 `document.visibilityState` 并订阅 `visibilitychange`),隐藏时跳过读取但照旧重排定时器;重新可见立即补读一次。首屏那次无条件读取 —— 那一刻没有数据可显示。可见性是可注入接缝,非浏览器环境视为始终可见。停止最后一个挂载时解绑监听。
- **运行中请求的配置生效**:已确认的网关、账号列表、固定账号和模型规则用于新调用,已经开始的适配器调用继续使用自己的快照。引用凭据每次账号选择仍重新解析;清除后可能按既有优先级使用默认账号的授权文件兜底,不撤回已发请求或当前尝试已取得的密钥。
- **账户移除与清理**:`accounts`、`modelAccountRules`、固定账户及 `credentialCleanupRefs` 在一次带版本检查的 `remote.settings.mutate` 中提交,再清理凭据。失败队列持久化,页面刷新后仍显示重试;重新引用、继承配置和默认密钥受保护。无法确认凭据事实时保留队列。宿主配置与凭据服务并无共同事务,不能承诺任意外部并发重建与凭据删除跨服务原子化。

Expand Down
15 changes: 15 additions & 0 deletions docs/agent-reference/model-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,21 @@ Task-specific reference moved from the former root `AGENTS.md`. All source and t
one rejected attempt), and it costs three requests per account per TTL, cached per key exactly like the
single-account path (an unknown account counts as 'may include it', the same fail-open rule, and a host
without the seam falls back to the serving key).
- **Reading the catalog never blocks on the network once this process has it.** `listModels()` asks
`GatewayFacts.loaded()` for the rows already in memory; only a source this process has never loaded
awaits `refresh()`. A warm source refreshes in the background for the NEXT open. The distinction is
memory-only on purpose: `snapshot()` reads the on-disk cache, which carries no age check, so serving it
as a first paint would draw a catalog of unknown vintage. `resolveModel()` and `stream()` still await a
real refresh — they must not route on a guess.
- **The picker and the usage card share the account endpoints.** `getUsage()` and `fetchBillingAccess()`
both read `/alpha/whoami`, then credits + subscriptions in parallel, for the same key. Successful
responses are memoized per `[apiBase, key, exact URL]` for `ACCOUNT_FACTS_TTL_MS` (30 s), so whichever
surface asked last pays for the duplicate. Only successes are shared — a failed endpoint must be retried
rather than replayed, or the report's `failures` and `classifyTotalFailure` verdict would describe a
request this process never made. `/alpha/usage/summary` is deliberately NOT shared (`share: false`): its
numbers are what the card displays next to a read timestamp, so replaying an earlier answer would
misreport both. The two URL shapes differ (`orgId` is added by `getUsage()`'s scoped path), so a `credits`
read is only ever reused by the surface asking with the identical query.
- The picker also supports a **visible-model allowlist** (`Config.visibleModels: string[]`, the settings
page's Visible models card): a non-empty list narrows `listModels()` to those catalog ids AFTER the plan
filter; empty/unset shows everything. It never gates named requests (`resolveModel` still serves every
Expand Down
68 changes: 54 additions & 14 deletions lib/client.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading