@mars-sea/dsh-commandcode-provider 0.15.4 → 0.15.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/client.js +134 -73
- package/lib/index.d.ts +57 -5
- package/lib/index.js +273 -51
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
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).
|
|
6
6
|
|
|
7
|
+
## [0.15.5] - 2026-10-27
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **请求路径与账户读取的一批性能优化(线上字节不变)。**
|
|
13
|
+
- `streamAttempt()` 的凭据解析与正文装配改为并发:两者互不依赖(正文从不携带密钥,健康账号池的解析只读内存),从前却是严格串行,带图请求因此把图片读取与 JSON 装配整个排在 `resolveApiKey()` 之后。与密钥无关的通道判断先算成 `provisionalProtocol`,凭据返回后目标一致即复用,不一致才重建一次(与既有协议回退、账号轮换同价)。代价是 Go 计划/已学 CLI 通道的账号每次请求会多装配一次正文(线上字节不变,纯 CPU 开销)。图片预算的**会计**仍先于任何 I/O 执行,但**抛错**推迟到凭据与真实通道落定之后:预算门与缓存门一次算全、一次抛出。
|
|
14
|
+
- `listModels()` 在**本进程联网成功过**该网关目录时不再等一次联网刷新,改用内存中的行立即返回、后台刷新留给下次打开;进程首次加载仍照旧等待。新判据 `GatewayFacts.loaded()` 以「来源」为准而不是「内存里有行」:`snapshot()` 从磁盘灌入的行、以及刷新失败后的空目录都不算热数据(磁盘副本没有年龄校验)。`resolveModel()` 与 `stream()` 不变,仍等真实刷新。
|
|
15
|
+
- `gradeHeaderTimeout()` 的指纹改用**已经序列化好的实发字符串**求摘要(`sha256Hex()`),不再对 `body` 重新 `JSON.stringify`:超时是最常重复的路径,而带图历史可达数 MB。这也更准确——实发字符串就是网关看到的字节,重新 stringify 会在 surrogate 清洗真的删掉过字符时描述到不同的字节,把一次连续超时拆成两条 streak。
|
|
16
|
+
- `toolParametersSchema()` 的根节点结果按 `parameters` 对象身份记忆化(`WeakMap`)。每个通道在每次尝试时都重建工具列表,而账号轮换、协议回退、降档重试都会让同一批 schema 再走一遍。同时纠正原注释「每条路径都返回副本」——`type: 'object'` 快路径一直返回调用方自己的对象,调用方靠它拿到同一个已冻结的 schema。
|
|
17
|
+
- 用量卡片与选择器的套餐过滤共享账户端点响应:两处读的是同一批端点、同一个账号,成功响应按 `[apiBase, key, 完整 URL]` 记入短命表(`ACCOUNT_FACTS_TTL_MS` 30 秒)。实际共享的是 `whoami` 与 `subscriptions`——`credits` 的 URL 形状带 `orgId`,两处不同,不会互相命中。只缓存成功响应(失败必须重试,否则报告的 `failures` 与 `classifyTotalFailure` 结论会描述一次没真正发出的请求);`/alpha/usage/summary` 明确不共享——那是用户正盯着、旁边写着读取时间的数字。
|
|
18
|
+
- 用量刷新区分自动与显式路径:`refresh({ auto: true })` 用于账户卡片首屏、面板首屏与面板 tick(injected 面因此分成 `refreshUsageAuto()` 与 `refreshUsage()` 两个入口),可在 `USAGE_AUTO_REFRESH_COALESCE_MS`(5 秒)内复用刚刚读到的结果;按钮、保存、换凭据等显式调用不查合并窗口(请求在途时仍按既有 `inFlight` 规则折叠),`fetchedAt` 因此仍如实反映读取时刻。
|
|
19
|
+
- 面板自动刷新在文档隐藏时跳过读取但照旧重排定时器,重新可见立即补读一次(该补读走 auto,5 秒内仍可能被合并);首屏仍无条件读取。可见性做成可注入接缝(`AutoRefreshVisibility`),非浏览器环境视为始终可见;监听句柄随循环状态放在模块层,任意卸载顺序都能解绑。
|
|
20
|
+
- **设置页账户卡片:账期与更新时间并入余额标签行。** 「月额度剩余」标签右接账期区间、同行右侧显示「更新于」;原单独一行的「官方账期用量 …」与底部重复的「账期截止」删除。账期未对齐时标签改接订阅自身的「账期截止」日期,不再显示「统计周期未确认」(额度面板不变);余额没取到时该行仍保留,区间前补「官方账期用量」。底部只在部分接口失败时显示「部分数据未取到」。
|
|
21
|
+
|
|
22
|
+
- **同步上游两处:Command Code 目录新增 `gpt-6.1-terra`,DSH `0.2.1-rc.4` 补入验证矩阵。**
|
|
23
|
+
- **Command Code(CLI 版本不变,仍 `1.88.0`)**:2026-10-27 复核 npm `latest` 与官网 changelog 均无新 CLI 版本(最新条目仍是 `1.87.3`);服务端目录 `?view=client` 的 `catalogVersion` 由 `f41a48ed…` 变为 `7f405e24…`,默认视图 93 → 94、client 视图 105 → 106,唯一新增行是 `gpt-6.1-terra`(GPT-6.1 Terra):1,050,000 上下文、文本 + 图像、推理档位 `low`/`medium`/`high`/`xhigh`/`max`、最低计划 Pro、`$1/$5`(≤272K 上下文)与 `$2/$7.5`(>272K)、缓存读取 `$0.05`/`$0.1`、缓存写入 `$1.25`/`$2.5`、月度额度 Go `$6`/GOAT `$20`/Pro `$20`,ZDR 默认覆盖,无促销、无峰值时段、无输出上限公布值。官方 models 页与 pricing 页均已上架,`1.88.0` 的 CLI 注册表尚未收录(审计 `incomplete`,能力字段以 `?view=client` 目录为准)。快照 +1:`KNOWN_PLANS` 93 → 94、`KNOWN_EFFORTS` 66 → 67、`KNOWN_IMAGE_MODELS` 72 → 73、内置价格行 93 → 94;ZDR 例外集(18)、自动思考模型表(16)、订阅表(9)、促销(4)、输出上限表(79)不变。`scripts/sync-model-prices.mjs` 重跑(页面 94 行,diff 仅新增该行)。
|
|
24
|
+
- **DSH `0.2.1-rc.4`**:npm `next` 于北京时间 2026-10-26 23:28(`2026-10-26T15:28:13Z`)从 `0.2.1-rc.3` 前进到 `0.2.1-rc.4`,`latest` 仍是 `0.2.1-rc.2`;GitHub 无对应 tag/release(均 404),`master` 停在 10-23,产物内构建哈希 `ec17a2c` 在公开仓库不可见——官方没有发布说明。产物级全量比对 235 个双版本包,实质变化仅 4 处:`dsh-mcp-client` 的资源能力探测(服务器未声明 `resources` 时直接返回空列表)、`dsh-client-ui-conversation` 的 View 标签提示框(按字符宽度估算、仅溢出时显示)与「更多会话视图」文案精简、`dsh-llm-deepseek-account` 的一条日志级别(`console.info` → `console.error`)、`dsh-mcp-resources` 的文档;另有 3 个包仅构建元数据(commit 短哈希、版本串)变化。插件导入的宿主包(`llm`/`settings`/`credentials`/`client-*`/`web`/`typert`/`api-remotes`/`attachment`/`commands`/`timeout`/`launch-environment`/`app-boot`)全部零功能变化。`dsh.compatibility.dshReleases` 补入 `0.2.1-rc.4`(三条实测记录),`test:engine`/`test:install` 矩阵随之变为 3 个引擎。
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **图片预算门不再抢在凭据之前抛错。** 预算门只做会计,凭据解析失败时先报 `MISSING_CREDENTIAL`:避免宿主对一次根本发不出去的请求先做不可逆的持久化图片卸载。预算门与缓存门在凭据与真实通道落定后一次算全(`max(预算, 缓存)`),不再退化成两步卸载,也不会把 cache 溢出报成 budget。(`src/adapter.ts`)
|
|
29
|
+
- **`loaded()` 只认联网来源。** `snapshot()` 灌入的磁盘行与刷新失败后的空目录都不再被当成选择器首屏:冷启动"先聊天再开选择器"不会画出无年龄校验的旧目录,刷新失败后也不会先闪一次空列表。(`src/gateway-facts.ts`)
|
|
30
|
+
- **可见性监听按任意卸载顺序解绑。** `unsubscribe` 句柄从函数局部提升到模块层,启动循环的 surface 先卸载时不再泄漏 `visibilitychange` 监听。(`src/client/panel.ts`)
|
|
31
|
+
- **账户卡片首屏接线补真实渲染测试。** 新增 `tests/accounts-card-render.test.ts`(`react-test-renderer`):首屏必须调 `onRefreshAuto()`、刷新按钮必须调 `onRefresh()`;把组件改回旧接线时该用例失败。此前只有"两个入口存在"的断言,回退后测试仍全绿。
|
|
32
|
+
- **非安全上下文(`http://` 局域网访问)下设置页不再整块空白(issue #102)。** 页面身份原来直接取 `crypto.randomUUID()`,而该 API 只在安全上下文存在:以 `http://<局域网 IP>:3080` 打开时它是 `undefined`,控制器又在 `AccountsCard` 的 `useState(createEnrollment)` 惰性初始化里构造,异常被设置页插槽的错误边界接住——左侧「Command Code」标签正常、右侧内容区空白且无任何提示。新增 `src/client/random-id.ts` 的 `randomId()`:`randomUUID` 可用时照旧,否则退回非安全上下文同样可用的 `crypto.getRandomValues`,按 RFC 4122 拼出同形 v4;`AccountEnrollmentController` 的默认 `newId` 改用它。页面身份只是宿主把开通过程绑到单个浏览器页面的分组键,不参与鉴权,因此不要求 HTTPS。(`src/client/random-id.ts`、`src/client/enrollment.ts`)
|
|
33
|
+
|
|
34
|
+
用例:`tests/adapter.test.ts` 新增「凭据解析与图片读取并行」「目录已在内存时不再等一次联网刷新」「超时定级复用已序列化的正文」「同一份工具 schema 只归一化一次」「用量卡片与选择器共享同一份账户事实」「凭据缺失时预算门不抢跑」「双超限一次报数」;`tests/gateway-facts.test.ts` 新增 4 条 `loaded()` 来源判据;`tests/panel.test.ts` 与 `tests/usage-client.test.ts` 新增自动/显式刷新、可见性与任意卸载顺序解绑用例;`tests/accounts-card-render.test.ts` 新增 2 条真实渲染接线用例。**验证边界**:`npm run typecheck` 退出码 0;`npm ci` 干净安装后 `npm test` **1125/1125** 通过;`npm run build` 重建 `lib/`。未在真实宿主中人工验证。
|
|
35
|
+
|
|
36
|
+
issue #102 的用例:`tests/random-id.test.ts` 3 条——安全上下文走平台 `randomUUID`(哨兵值分辨分支,不走自行拼装)、非安全上下文退回 `getRandomValues` 时 100 个身份互不相同且为 v4、非安全上下文下构造 `AccountEnrollmentController` 并把页面身份带给宿主。三处定向变异各命中一条:关闭快速路径只失败第一条,把版本位置 0 只失败后两条,把默认 `newId` 改回 `() => crypto.randomUUID()` 只失败第三条。**验证边界**:`npm run typecheck` 退出码 0;`npm ci` 干净安装后 `npm test` **1129/1129** 通过;`npm run build` 重建 `lib/`(`lib/client.js` 含 `randomId`,`lib/index.js` 与 `lib/index.d.ts` 字节不变)。未在真实 NAS/局域网环境人工验证。
|
|
37
|
+
|
|
38
|
+
**本轮(2026-10-27 上游同步)验证**:`npm run typecheck` 退出码 0;`npm test` **1126/1126** 通过、0 失败;`npm run build` 两段 `Build complete`,`lib/index.js` 已重建(含 `gpt-6.1-terra`);`npm run test:engine` **3/3**(`0.2.1-rc.2`/`0.2.1-rc.3`/`0.2.1-rc.4`,宿主准入 preflight 全部放行,末行 `engine-load smoke passed on all 3 declared-compatible engines`);`npm run test:install` **3/3**(pnpm 10.34.5);`npm run test:pack` 16 文件(版本 0.15.4)。未做真实账号登录、付费生成、服务端协议验证与浏览器渲染。
|
|
39
|
+
|
|
7
40
|
## [0.15.4] - 2026-10-24
|
|
8
41
|
|
|
9
42
|
### Changed
|
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ Unofficial [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harnes
|
|
|
61
61
|
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@latest
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
This build admits **dsh `>=0.2.1-rc.2 <0.2.2-0`** — every RC of the `0.2.1` channel from `0.2.1-rc.2` onward. `0.2.1-rc.2` is what npm `latest` runs and `0.2.1-rc.
|
|
64
|
+
This build admits **dsh `>=0.2.1-rc.2 <0.2.2-0`** — every RC of the `0.2.1` channel from `0.2.1-rc.2` onward. `0.2.1-rc.2` is what npm `latest` runs and `0.2.1-rc.4` ships on `next`; `0.2.1-rc.2`, `0.2.1-rc.3` and `0.2.1-rc.4` are all covered by the full engine matrix (`npm run test:engine`, `npm run test:install`). A later `0.2.1-rc.5` is admitted without a plugin update, but stays unverified until a record is added to `dsh.compatibility.dshReleases`. The next minor is deliberately excluded, `0.2.2-rc.*` included — the upper bound is written `<0.2.2-0` for exactly that reason. A profile on `0.2.0-rc.2`, `0.2.1-alpha.5` or older must upgrade its engine first. Compatibility details — the `engines.dsh` admission rule, package pairings, evidence — live in [engine compatibility](docs/agent-reference/engine-compatibility.md).
|
|
65
65
|
|
|
66
66
|
**Older dsh releases** are no longer supported: the last plugin release for dsh 0.1.7 is `0.11.17`, for 0.1.2–0.1.6 it is `0.11.11`, and for the 0.5.0-era line `0.9.1` — all installed by exact version, none maintained.
|
|
67
67
|
|
package/README.zh-CN.md
CHANGED
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@latest
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
本构建准入 **dsh `>=0.2.1-rc.2 <0.2.2-0`** —— `0.2.1` 通道中 `0.2.1-rc.2` 及以后的每个 RC。`0.2.1-rc.2` 是 npm `latest` 指向的版本,`0.2.1-rc.
|
|
64
|
+
本构建准入 **dsh `>=0.2.1-rc.2 <0.2.2-0`** —— `0.2.1` 通道中 `0.2.1-rc.2` 及以后的每个 RC。`0.2.1-rc.2` 是 npm `latest` 指向的版本,`0.2.1-rc.4` 挂在 `next`;`0.2.1-rc.2`、`0.2.1-rc.3` 与 `0.2.1-rc.4` 都跑过完整引擎矩阵(`npm run test:engine`、`npm run test:install`)。更晚的 `0.2.1-rc.5` 无需更新插件即可准入,但在 `dsh.compatibility.dshReleases` 补上记录之前不算已验证。下一个 minor 被刻意排除(含 `0.2.2-rc.*`)—— 区间上界写成 `<0.2.2-0` 正是为了这一点。使用 `0.2.0-rc.2`、`0.2.1-alpha.5` 或更早版本的配置需先升级引擎,再安装本构建。兼容性细节 —— `engines.dsh` 准入规则、按引擎配对的包版本、验证证据 —— 见[引擎兼容](docs/agent-reference/engine-compatibility.md)。
|
|
65
65
|
|
|
66
66
|
**更早的 dsh 版本**已不再支持:覆盖 dsh 0.1.7 的最后一个插件版本是 `0.11.17`,0.1.2–0.1.6 线的最后一个版本是 `0.11.11`,0.5.0 时代 Harness 线的最后一个版本是 `0.9.1` —— 三者都按精确版本安装,且都不再维护。
|
|
67
67
|
|
package/lib/client.js
CHANGED
|
@@ -1856,6 +1856,8 @@ window.__ModuleLoader__.load({
|
|
|
1856
1856
|
generation = 0;
|
|
1857
1857
|
inFlight = false;
|
|
1858
1858
|
disposed = false;
|
|
1859
|
+
/** When the last fetch SETTLED successfully, for {@link USAGE_AUTO_REFRESH_COALESCE_MS}. */
|
|
1860
|
+
settledAt;
|
|
1859
1861
|
constructor(remote) {
|
|
1860
1862
|
this.remote = remote;
|
|
1861
1863
|
}
|
|
@@ -1877,9 +1879,16 @@ window.__ModuleLoader__.load({
|
|
|
1877
1879
|
/**
|
|
1878
1880
|
* Fetch (or refetch) the report. Concurrent refreshes collapse onto one
|
|
1879
1881
|
* request; a superseded fetch's late result is dropped, never published.
|
|
1882
|
+
*
|
|
1883
|
+
* `auto` marks a refresh nobody asked for — a surface appearing, or the
|
|
1884
|
+
* background tick. Those also reuse a result fetched within
|
|
1885
|
+
* {@link USAGE_AUTO_REFRESH_COALESCE_MS}, because two surfaces mounting a
|
|
1886
|
+
* second apart are asking the same question. Every other caller is a user
|
|
1887
|
+
* action and always goes to the wire.
|
|
1880
1888
|
*/
|
|
1881
|
-
async refresh() {
|
|
1889
|
+
async refresh(options) {
|
|
1882
1890
|
if (this.disposed || this.inFlight) return;
|
|
1891
|
+
if (options?.auto === true && this.current.status === "ready" && this.settledAt !== void 0 && Date.now() - this.settledAt < 5e3) return;
|
|
1883
1892
|
const generation = ++this.generation;
|
|
1884
1893
|
this.inFlight = true;
|
|
1885
1894
|
this.current = {
|
|
@@ -1891,13 +1900,16 @@ window.__ModuleLoader__.load({
|
|
|
1891
1900
|
try {
|
|
1892
1901
|
const response = await this.remote.report();
|
|
1893
1902
|
if (this.disposed || generation !== this.generation) return;
|
|
1894
|
-
if (response.ok)
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1903
|
+
if (response.ok) {
|
|
1904
|
+
const fetchedAt = Date.now();
|
|
1905
|
+
this.settledAt = fetchedAt;
|
|
1906
|
+
this.current = {
|
|
1907
|
+
status: "ready",
|
|
1908
|
+
report: response.value,
|
|
1909
|
+
error: void 0,
|
|
1910
|
+
fetchedAt
|
|
1911
|
+
};
|
|
1912
|
+
} else this.current = {
|
|
1901
1913
|
...this.current,
|
|
1902
1914
|
status: "error",
|
|
1903
1915
|
error: response.error.message
|
|
@@ -2887,6 +2899,23 @@ window.__ModuleLoader__.load({
|
|
|
2887
2899
|
]
|
|
2888
2900
|
};
|
|
2889
2901
|
//#endregion
|
|
2902
|
+
//#region src/client/random-id.ts
|
|
2903
|
+
/**
|
|
2904
|
+
* 页面身份(issue #102)。`crypto.randomUUID` 是安全上下文专属:局域网里用
|
|
2905
|
+
* `http://<ip>:3080` 打开设置页时它是 `undefined`,而这里在控制器构造期就调用,
|
|
2906
|
+
* 异常会落到设置页插槽的错误边界上,整块内容变成空白。`crypto.getRandomValues`
|
|
2907
|
+
* 在非安全上下文同样可用,因此只在 `randomUUID` 不可用时按 RFC 4122 自行拼一个 v4。
|
|
2908
|
+
*/
|
|
2909
|
+
function randomId() {
|
|
2910
|
+
const source = globalThis.crypto;
|
|
2911
|
+
if (typeof source.randomUUID === "function") return source.randomUUID();
|
|
2912
|
+
const bytes = source.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
2913
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
2914
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
2915
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2916
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
2917
|
+
}
|
|
2918
|
+
//#endregion
|
|
2890
2919
|
//#region src/client/enrollment.ts
|
|
2891
2920
|
const live = (state) => [
|
|
2892
2921
|
"creating",
|
|
@@ -2912,7 +2941,7 @@ window.__ModuleLoader__.load({
|
|
|
2912
2941
|
watch;
|
|
2913
2942
|
ready;
|
|
2914
2943
|
openedPage = false;
|
|
2915
|
-
constructor(remote, changed, pollMs = 750, newId =
|
|
2944
|
+
constructor(remote, changed, pollMs = 750, newId = randomId, openPage = openLoginPage) {
|
|
2916
2945
|
this.remote = remote;
|
|
2917
2946
|
this.changed = changed;
|
|
2918
2947
|
this.pollMs = pollMs;
|
|
@@ -3243,7 +3272,6 @@ window.__ModuleLoader__.load({
|
|
|
3243
3272
|
usageCost: "花费",
|
|
3244
3273
|
usageCompleted: "成功",
|
|
3245
3274
|
usageBillingPeriod: "官方账期用量",
|
|
3246
|
-
usagePeriodUnknown: "官方用量汇总,统计周期未确认",
|
|
3247
3275
|
usageTokens: "Token",
|
|
3248
3276
|
usageTokensIn: "入",
|
|
3249
3277
|
usageTokensOut: "出",
|
|
@@ -3330,7 +3358,7 @@ window.__ModuleLoader__.load({
|
|
|
3330
3358
|
accountEnrollmentCleanupRetry: "重试清理",
|
|
3331
3359
|
accountRefSeparator: ":",
|
|
3332
3360
|
accountModels: "专用模型",
|
|
3333
|
-
accountModelsHint: "
|
|
3361
|
+
accountModelsHint: "选中即专属:优先此账户处理,不可用时自动切换",
|
|
3334
3362
|
accountModelOwner: "属于 {name}",
|
|
3335
3363
|
modelPick: "选择模型…",
|
|
3336
3364
|
modelCount: "已选 {count} 个模型",
|
|
@@ -3515,7 +3543,6 @@ window.__ModuleLoader__.load({
|
|
|
3515
3543
|
usageCost: "Spend",
|
|
3516
3544
|
usageCompleted: "Completed",
|
|
3517
3545
|
usageBillingPeriod: "Official billing-period usage",
|
|
3518
|
-
usagePeriodUnknown: "Official usage summary; the billing period is unconfirmed",
|
|
3519
3546
|
usageTokens: "Tokens",
|
|
3520
3547
|
usageTokensIn: "in",
|
|
3521
3548
|
usageTokensOut: "out",
|
|
@@ -3602,7 +3629,7 @@ window.__ModuleLoader__.load({
|
|
|
3602
3629
|
accountEnrollmentCleanupRetry: "Retry cleanup",
|
|
3603
3630
|
accountRefSeparator: ": ",
|
|
3604
3631
|
accountModels: "Dedicated models",
|
|
3605
|
-
accountModelsHint: "
|
|
3632
|
+
accountModelsHint: "Selected models are dedicated to this account and still switch when unavailable",
|
|
3606
3633
|
accountModelOwner: "on {name}",
|
|
3607
3634
|
modelPick: "Choose models…",
|
|
3608
3635
|
modelCount: "{count} models selected",
|
|
@@ -3837,7 +3864,7 @@ window.__ModuleLoader__.load({
|
|
|
3837
3864
|
}
|
|
3838
3865
|
//#endregion
|
|
3839
3866
|
//#region package.json
|
|
3840
|
-
var version = "0.15.
|
|
3867
|
+
var version = "0.15.5";
|
|
3841
3868
|
var repository = {
|
|
3842
3869
|
"type": "git",
|
|
3843
3870
|
"url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
|
|
@@ -4877,6 +4904,19 @@ window.__ModuleLoader__.load({
|
|
|
4877
4904
|
]
|
|
4878
4905
|
});
|
|
4879
4906
|
}
|
|
4907
|
+
/**
|
|
4908
|
+
* Left text of the balance label row: the billing period, stated once per card.
|
|
4909
|
+
* 用量汇总与订阅起点对齐时给完整区间;对不上(或用量汇总没取到)时退回订阅
|
|
4910
|
+
* 自身的截止日,它不依赖对齐也可靠。余额没取到时没有「月额度剩余」可挂,完整
|
|
4911
|
+
* 区间前补回「官方账期用量」,避免一串裸日期没有上下文。
|
|
4912
|
+
*/
|
|
4913
|
+
function balanceLabelText(report, t) {
|
|
4914
|
+
const plan = report.plan;
|
|
4915
|
+
const usagePeriod = formatUsageBillingPeriod(report.usage?.periodStart, plan?.currentPeriodStart, plan?.currentPeriodEnd);
|
|
4916
|
+
const periodText = usagePeriod !== "" ? usagePeriod : plan !== void 0 && plan.currentPeriodEnd > 0 ? `${t("usagePeriodEnd")} ${new Date(plan.currentPeriodEnd).toLocaleDateString()}` : "";
|
|
4917
|
+
if (report.credits !== void 0) return periodText !== "" ? `${t("usageMonthlyLeft")} · ${periodText}` : t("usageMonthlyLeft");
|
|
4918
|
+
return usagePeriod !== "" ? `${t("usageBillingPeriod")} ${usagePeriod}` : periodText;
|
|
4919
|
+
}
|
|
4880
4920
|
/** One pool account's facts (identity, totals, credits, window limits). */
|
|
4881
4921
|
function AccountReport({ entry, fetchedAt, t, headless = false }) {
|
|
4882
4922
|
const report = entry.report;
|
|
@@ -4886,9 +4926,12 @@ window.__ModuleLoader__.load({
|
|
|
4886
4926
|
const plan = report.plan;
|
|
4887
4927
|
const planName = plan?.name ?? "";
|
|
4888
4928
|
const planStatus = plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "";
|
|
4889
|
-
const showPeriod = plan !== void 0 && plan.currentPeriodEnd > 0;
|
|
4890
4929
|
const showPartial = report.failures.length > 0 && report.blocked === void 0;
|
|
4891
|
-
const
|
|
4930
|
+
const label = balanceLabelText(report, t);
|
|
4931
|
+
const updatedText = fetchedAt === void 0 ? "" : `${t("usageUpdated")} ${new Date(fetchedAt).toLocaleTimeString(void 0, {
|
|
4932
|
+
hour: "2-digit",
|
|
4933
|
+
minute: "2-digit"
|
|
4934
|
+
})}`;
|
|
4892
4935
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4893
4936
|
className: "cc-accountReport",
|
|
4894
4937
|
children: [
|
|
@@ -4927,26 +4970,33 @@ window.__ModuleLoader__.load({
|
|
|
4927
4970
|
children: report.failures.join(" · ")
|
|
4928
4971
|
}) : null]
|
|
4929
4972
|
}) : null,
|
|
4930
|
-
|
|
4931
|
-
const monthlyKnown = credits.monthlyReported !== false;
|
|
4932
|
-
const depleted = monthlyKnown && credits.monthlyCredits <= 0;
|
|
4973
|
+
(() => {
|
|
4974
|
+
const monthlyKnown = credits !== void 0 && credits.monthlyReported !== false;
|
|
4975
|
+
const depleted = credits !== void 0 && monthlyKnown && credits.monthlyCredits <= 0;
|
|
4976
|
+
if (label === "" && updatedText === "") return null;
|
|
4933
4977
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4934
4978
|
className: depleted ? "cc-balance cc-balanceEmpty" : "cc-balance",
|
|
4935
4979
|
children: [
|
|
4936
4980
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4937
|
-
className: "cc-
|
|
4981
|
+
className: "cc-balanceRow",
|
|
4938
4982
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4939
4983
|
className: "cc-balanceLabel",
|
|
4940
|
-
children:
|
|
4941
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4984
|
+
children: label
|
|
4985
|
+
}), updatedText !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4986
|
+
className: "cc-balanceLabel",
|
|
4987
|
+
children: updatedText
|
|
4988
|
+
}) : null]
|
|
4989
|
+
}),
|
|
4990
|
+
credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4991
|
+
className: "cc-balanceRow",
|
|
4992
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4942
4993
|
className: "cc-balanceValue",
|
|
4943
4994
|
children: monthlyKnown ? formatMoney(Math.max(0, credits.monthlyCredits)) : "—"
|
|
4995
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4996
|
+
className: "cc-balanceSub",
|
|
4997
|
+
children: `${t("usagePurchased")} ${credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)} · ${t("usageFree")} ${credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)}`
|
|
4944
4998
|
})]
|
|
4945
|
-
}),
|
|
4946
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4947
|
-
className: "cc-balanceSub",
|
|
4948
|
-
children: `${t("usagePurchased")} ${credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)} · ${t("usageFree")} ${credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)}`
|
|
4949
|
-
}),
|
|
4999
|
+
}) : null,
|
|
4950
5000
|
depleted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4951
5001
|
className: "cc-balanceNote",
|
|
4952
5002
|
role: "status",
|
|
@@ -4954,11 +5004,7 @@ window.__ModuleLoader__.load({
|
|
|
4954
5004
|
}) : null
|
|
4955
5005
|
]
|
|
4956
5006
|
});
|
|
4957
|
-
})()
|
|
4958
|
-
report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4959
|
-
className: "cc-usageUpdated",
|
|
4960
|
-
children: usagePeriod ? `${t("usageBillingPeriod")} ${usagePeriod}` : t("usagePeriodUnknown")
|
|
4961
|
-
}) : null,
|
|
5007
|
+
})(),
|
|
4962
5008
|
report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4963
5009
|
className: "cc-usageStats cc-usageStatsActivity",
|
|
4964
5010
|
children: [
|
|
@@ -5000,35 +5046,10 @@ window.__ModuleLoader__.load({
|
|
|
5000
5046
|
t
|
|
5001
5047
|
}) : null]
|
|
5002
5048
|
}) : null,
|
|
5003
|
-
|
|
5004
|
-
className: "cc-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
className: "cc-usageUpdated",
|
|
5008
|
-
children: [
|
|
5009
|
-
t("usagePeriodEnd"),
|
|
5010
|
-
" ",
|
|
5011
|
-
new Date(plan.currentPeriodEnd).toLocaleDateString()
|
|
5012
|
-
]
|
|
5013
|
-
}) : null,
|
|
5014
|
-
showPartial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5015
|
-
className: "cc-usagePartial",
|
|
5016
|
-
title: report.failures.join("; "),
|
|
5017
|
-
children: t("usagePartial")
|
|
5018
|
-
}) : null,
|
|
5019
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cc-usageMetaSpacer" }),
|
|
5020
|
-
fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
5021
|
-
className: "cc-usageUpdated",
|
|
5022
|
-
children: [
|
|
5023
|
-
t("usageUpdated"),
|
|
5024
|
-
" ",
|
|
5025
|
-
new Date(fetchedAt).toLocaleTimeString(void 0, {
|
|
5026
|
-
hour: "2-digit",
|
|
5027
|
-
minute: "2-digit"
|
|
5028
|
-
})
|
|
5029
|
-
]
|
|
5030
|
-
}) : null
|
|
5031
|
-
]
|
|
5049
|
+
showPartial ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5050
|
+
className: "cc-usagePartial",
|
|
5051
|
+
title: report.failures.join("; "),
|
|
5052
|
+
children: t("usagePartial")
|
|
5032
5053
|
}) : null
|
|
5033
5054
|
]
|
|
5034
5055
|
});
|
|
@@ -6459,8 +6480,13 @@ window.__ModuleLoader__.load({
|
|
|
6459
6480
|
* The unified account card: every account the pool rotates through, one row
|
|
6460
6481
|
* each, with its status and quota inline — so one account's facts no longer
|
|
6461
6482
|
* have to be cross-read across separate surfaces.
|
|
6483
|
+
*
|
|
6484
|
+
* Exported for the render test (`tests/accounts-card-render.test.ts`) that pins
|
|
6485
|
+
* the first-paint auto-refresh wiring against a real React tree: the page mounts
|
|
6486
|
+
* this card, and a test that only checks the injected function exists cannot see
|
|
6487
|
+
* the card reaching for it.
|
|
6462
6488
|
*/
|
|
6463
|
-
function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh, openAnalytics, createEnrollment, expanded, onToggle }) {
|
|
6489
|
+
function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh, onRefreshAuto, openAnalytics, createEnrollment, expanded, onToggle }) {
|
|
6464
6490
|
const [adding, setAdding] = (0, react.useState)(false);
|
|
6465
6491
|
const [enrollment] = (0, react.useState)(createEnrollment);
|
|
6466
6492
|
const snapshot = (0, react.useSyncExternalStore)(enrollment.store.subscribe, enrollment.store.getSnapshot);
|
|
@@ -6471,8 +6497,8 @@ window.__ModuleLoader__.load({
|
|
|
6471
6497
|
const closeAdd = (0, react.useCallback)(() => setAdding(false), []);
|
|
6472
6498
|
const { loading, shouldRefresh } = usageCardState(usage);
|
|
6473
6499
|
(0, react.useEffect)(() => {
|
|
6474
|
-
if (shouldRefresh)
|
|
6475
|
-
}, [shouldRefresh,
|
|
6500
|
+
if (shouldRefresh) onRefreshAuto();
|
|
6501
|
+
}, [shouldRefresh, onRefreshAuto]);
|
|
6476
6502
|
const rows = accountRows(state, usage, t).filter((row) => row.ref === void 0 || !state.accountEnrollmentRefs.includes(row.ref));
|
|
6477
6503
|
const multi = rows.length > 1;
|
|
6478
6504
|
const pinnedRow = rows.find((row) => row.id === state.activeAccount);
|
|
@@ -6960,6 +6986,7 @@ window.__ModuleLoader__.load({
|
|
|
6960
6986
|
disabled,
|
|
6961
6987
|
actions,
|
|
6962
6988
|
onRefresh: props.refreshUsage,
|
|
6989
|
+
onRefreshAuto: props.refreshUsageAuto,
|
|
6963
6990
|
openAnalytics: props.openAnalytics,
|
|
6964
6991
|
createEnrollment: props.createEnrollment,
|
|
6965
6992
|
...foldProps("accounts")
|
|
@@ -7899,6 +7926,14 @@ window.__ModuleLoader__.load({
|
|
|
7899
7926
|
set: (callback, ms) => setTimeout(callback, ms),
|
|
7900
7927
|
clear: (handle) => clearTimeout(handle)
|
|
7901
7928
|
};
|
|
7929
|
+
const DOCUMENT_VISIBILITY = {
|
|
7930
|
+
isVisible: () => typeof document === "undefined" ? true : document.visibilityState !== "hidden",
|
|
7931
|
+
subscribe: (onChange) => {
|
|
7932
|
+
if (typeof document === "undefined") return () => void 0;
|
|
7933
|
+
document.addEventListener("visibilitychange", onChange);
|
|
7934
|
+
return () => document.removeEventListener("visibilitychange", onChange);
|
|
7935
|
+
}
|
|
7936
|
+
};
|
|
7902
7937
|
/** Live mount count of {@link startPanelAutoRefresh}. */
|
|
7903
7938
|
let references = 0;
|
|
7904
7939
|
/** Owner ticket of the running loop, if any. */
|
|
@@ -7908,6 +7943,16 @@ window.__ModuleLoader__.load({
|
|
|
7908
7943
|
/** Pending timer handle of the running loop. */
|
|
7909
7944
|
let handle;
|
|
7910
7945
|
/**
|
|
7946
|
+
* Unsubscribe of the visibility listener owned by the RUNNING loop.
|
|
7947
|
+
*
|
|
7948
|
+
* Module-level on purpose: only the surface that started the loop receives the
|
|
7949
|
+
* subscribe handle, but that surface is not necessarily the last to unmount.
|
|
7950
|
+
* Keeping the handle on the same layer as `activeTicket`/`handle` lets any
|
|
7951
|
+
* teardown order release it; a function-local handle leaked the listener
|
|
7952
|
+
* whenever the starter unmounted first.
|
|
7953
|
+
*/
|
|
7954
|
+
let visibilityUnsubscribe;
|
|
7955
|
+
/**
|
|
7911
7956
|
* Start the shared auto refresh. One fetch when the surface appears (the sidebar
|
|
7912
7957
|
* row is the point of the panel — it must be current, not wait for a click), then
|
|
7913
7958
|
* a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface stays mounted.
|
|
@@ -7917,22 +7962,37 @@ window.__ModuleLoader__.load({
|
|
|
7917
7962
|
* tick goes through `usage.refresh()`, which collapses a concurrent fetch onto
|
|
7918
7963
|
* the in-flight one. Returns the disposer that drops this surface's reference.
|
|
7919
7964
|
*
|
|
7965
|
+
* A hidden document is still mounted, so its ticks are SKIPPED: a background
|
|
7966
|
+
* tab must not keep paying for a four-call report nobody reads. The tick is
|
|
7967
|
+
* rescheduled either way, so the loop survives, and becoming visible again
|
|
7968
|
+
* fetches at once — the data on screen is by then older than a tick would have
|
|
7969
|
+
* left it. The first paint still fetches unconditionally: it is the moment the
|
|
7970
|
+
* row appears, and there is nothing to show without it.
|
|
7971
|
+
*
|
|
7920
7972
|
* @param usage - the shared usage controller.
|
|
7921
7973
|
* @param isConfigured - whether a credential exists right now (re-read per tick).
|
|
7922
7974
|
* @param timer - timer seam for tests.
|
|
7975
|
+
* @param visibility - visibility seam; see {@link AutoRefreshVisibility}.
|
|
7923
7976
|
*/
|
|
7924
|
-
function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
|
|
7977
|
+
function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER, visibility = DOCUMENT_VISIBILITY) {
|
|
7925
7978
|
references += 1;
|
|
7926
7979
|
const ticket = ++ticketSeq;
|
|
7980
|
+
const fetch = () => {
|
|
7981
|
+
if (isConfigured()) usage.refresh({ auto: true });
|
|
7982
|
+
};
|
|
7927
7983
|
const tick = () => {
|
|
7928
7984
|
if (activeTicket !== ticket) return;
|
|
7929
|
-
if (
|
|
7985
|
+
if (visibility.isVisible()) fetch();
|
|
7930
7986
|
handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
|
|
7931
7987
|
};
|
|
7932
7988
|
if (activeTicket === void 0) {
|
|
7933
7989
|
activeTicket = ticket;
|
|
7934
7990
|
handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
|
|
7935
|
-
|
|
7991
|
+
fetch();
|
|
7992
|
+
visibilityUnsubscribe = visibility.subscribe?.(() => {
|
|
7993
|
+
if (activeTicket !== ticket || !visibility.isVisible()) return;
|
|
7994
|
+
fetch();
|
|
7995
|
+
});
|
|
7936
7996
|
}
|
|
7937
7997
|
let stopped = false;
|
|
7938
7998
|
return () => {
|
|
@@ -7941,6 +8001,8 @@ window.__ModuleLoader__.load({
|
|
|
7941
8001
|
references -= 1;
|
|
7942
8002
|
if (references > 0) return;
|
|
7943
8003
|
activeTicket = void 0;
|
|
8004
|
+
visibilityUnsubscribe?.();
|
|
8005
|
+
visibilityUnsubscribe = void 0;
|
|
7944
8006
|
if (handle !== void 0) {
|
|
7945
8007
|
timer.clear(handle);
|
|
7946
8008
|
handle = void 0;
|
|
@@ -10463,13 +10525,14 @@ window.__ModuleLoader__.load({
|
|
|
10463
10525
|
.cc-usageStats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;display:grid}
|
|
10464
10526
|
.cc-usageStatsActivity{grid-template-columns:repeat(4,minmax(0,1fr))}
|
|
10465
10527
|
@container (max-width:460px){.cc-usageStatsActivity{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
10466
|
-
.cc-balance{flex-
|
|
10467
|
-
|
|
10528
|
+
.cc-balance{flex-direction:column;gap:2px;display:flex}
|
|
10529
|
+
/* 两行左右分栏:标签行左账期、右更新时间;数值行左剩余、右已购与赠送。 */
|
|
10530
|
+
.cc-balanceRow{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:4px 16px;display:flex}
|
|
10468
10531
|
.cc-balanceLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
10469
10532
|
.cc-balanceValue{color:var(--dsw-alias-label-primary);font-size:20px;font-weight:600;line-height:28px;font-variant-numeric:tabular-nums}
|
|
10470
10533
|
.cc-balanceSub{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}
|
|
10471
10534
|
.cc-balanceEmpty .cc-balanceValue{color:var(--dsw-alias-state-error-primary)}
|
|
10472
|
-
.cc-balanceNote{
|
|
10535
|
+
.cc-balanceNote{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
|
|
10473
10536
|
.cc-usageStat{min-width:0;flex-direction:column;gap:2px;display:flex;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform)}
|
|
10474
10537
|
.cc-usageStatLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
|
|
10475
10538
|
.cc-usageStatValue{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:500;line-height:24px;font-variant-numeric:tabular-nums}
|
|
@@ -10484,9 +10547,6 @@ window.__ModuleLoader__.load({
|
|
|
10484
10547
|
.cc-usageBarFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
|
|
10485
10548
|
.cc-usageBarFillWarn{background:var(--dsw-alias-state-error-primary)}
|
|
10486
10549
|
.cc-usageWindowReset{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
|
|
10487
|
-
.cc-usageMeta{align-items:center;gap:8px;display:flex}
|
|
10488
|
-
.cc-usageMetaSpacer{flex:1}
|
|
10489
|
-
.cc-usageUpdated{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
|
|
10490
10550
|
.cc-usagePartial{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}
|
|
10491
10551
|
.cc-notices{flex-direction:column;gap:8px;display:flex;margin-bottom:12px}
|
|
10492
10552
|
.cc-section>.cc-notice{margin-top:12px}
|
|
@@ -11176,6 +11236,7 @@ window.__ModuleLoader__.load({
|
|
|
11176
11236
|
discard: () => controller.discard(),
|
|
11177
11237
|
refreshCredentials: () => void controller.refreshCredentials(),
|
|
11178
11238
|
refreshUsage: () => void usageController.refresh(),
|
|
11239
|
+
refreshUsageAuto: () => void usageController.refresh({ auto: true }),
|
|
11179
11240
|
...typeof ctx.get("layout")?.selectPanel === "function" ? { openAnalytics } : {},
|
|
11180
11241
|
beginLogin: (targetRef) => void loginController.begin(targetRef),
|
|
11181
11242
|
cancelLogin: () => void loginController.cancel(),
|
package/lib/index.d.ts
CHANGED
|
@@ -382,6 +382,25 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
|
|
|
382
382
|
private readonly surfaceOffload;
|
|
383
383
|
private readonly billingAccess;
|
|
384
384
|
private readonly billingAccessInflight;
|
|
385
|
+
/**
|
|
386
|
+
* Successful account-endpoint responses, keyed by gateway + key + exact URL.
|
|
387
|
+
*
|
|
388
|
+
* The settings card and the picker's advisory plan filter ask the SAME three
|
|
389
|
+
* endpoints about the SAME account (`/alpha/whoami`, then credits and
|
|
390
|
+
* subscriptions in parallel). Here that is four round trips per surface for
|
|
391
|
+
* two answers — and the card refreshes on a timer, so the picker keeps paying
|
|
392
|
+
* for rows it already has. Sharing the raw responses removes the duplicate
|
|
393
|
+
* without either surface having to know about the other: whoever asks second
|
|
394
|
+
* within {@link ACCOUNT_FACTS_TTL_MS} reads the same record instead of the
|
|
395
|
+
* wire.
|
|
396
|
+
*
|
|
397
|
+
* Only SUCCESSFUL records are kept. A failure must be retried rather than
|
|
398
|
+
* replayed, or an endpoint that recovered a second ago would keep reporting
|
|
399
|
+
* yesterday's outage — and the usage report's per-endpoint `failures` and
|
|
400
|
+
* {@link classifyTotalFailure} verdict would describe a request this process
|
|
401
|
+
* never made.
|
|
402
|
+
*/
|
|
403
|
+
private readonly accountJson;
|
|
385
404
|
private readonly protocolCache;
|
|
386
405
|
/** Namespace account-derived facts by gateway without exposing the key. */
|
|
387
406
|
private accountCacheKey;
|
|
@@ -488,8 +507,24 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
|
|
|
488
507
|
*/
|
|
489
508
|
allowanceTier(): Promise<'go' | 'goat' | 'pro' | undefined>;
|
|
490
509
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
510
|
+
/**
|
|
511
|
+
* Resolve and validate the key one account-endpoint call will use.
|
|
512
|
+
*
|
|
513
|
+
* Every account endpoint runs the chat path's own `assertUsableApiKey` check,
|
|
514
|
+
* or a key carrying a character no HTTP header can hold throws inside `fetch`
|
|
515
|
+
* BEFORE any I/O — once per endpoint — and the report reads as "the network is
|
|
516
|
+
* down". The credential reference is read structurally: it belongs to the
|
|
517
|
+
* host's resolved options, not to this adapter's connection type, and it only
|
|
518
|
+
* names the key in the rejection message.
|
|
519
|
+
*
|
|
520
|
+
* Separate from {@link accountHeadersFor} so a caller that also needs to key a
|
|
521
|
+
* cache by the same identity does not have to resolve the key twice.
|
|
522
|
+
*/
|
|
523
|
+
private usableAccountKey;
|
|
491
524
|
/** The headers every authenticated account endpoint shares. */
|
|
492
525
|
private accountHeaders;
|
|
526
|
+
/** Headers for an already-validated key; see {@link usableAccountKey}. */
|
|
527
|
+
private accountHeadersFor;
|
|
493
528
|
/**
|
|
494
529
|
* Fetch one JSON GET and return its HTTP status with a parsed record. The
|
|
495
530
|
* catalog, billing probe, and usage report each apply their own failure
|
|
@@ -524,6 +559,23 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
|
|
|
524
559
|
* everything" (fail-open).
|
|
525
560
|
*/
|
|
526
561
|
private loadBillingAccessForKey;
|
|
562
|
+
/**
|
|
563
|
+
* One account-endpoint GET, served from {@link accountJson} when `share` and
|
|
564
|
+
* the same URL answered successfully within {@link ACCOUNT_FACTS_TTL_MS}.
|
|
565
|
+
*
|
|
566
|
+
* `timeoutMs` is the CALLER's budget; a cache hit spends none of it. Failures
|
|
567
|
+
* are never cached, so this can only ever save a request that would have
|
|
568
|
+
* re-read an answer this process already had.
|
|
569
|
+
*
|
|
570
|
+
* `share: false` marks a VOLATILE endpoint — one whose answer is the number
|
|
571
|
+
* the user is actually looking at, and which must therefore never be replayed
|
|
572
|
+
* from an earlier request. The usage card's summary is exactly that: its
|
|
573
|
+
* headline counts and cost are the reason the card exists, and stamping a
|
|
574
|
+
* cached copy with the current time would misreport when they were read.
|
|
575
|
+
* Identity and billing facts are the opposite: a plan tier or an org id that
|
|
576
|
+
* is 30 s old is still the truth, and those are what the picker duplicates.
|
|
577
|
+
*/
|
|
578
|
+
private accountJsonGet;
|
|
527
579
|
/**
|
|
528
580
|
* The billing facts behind the picker's plan filter, mirroring the CLI's
|
|
529
581
|
* `createBilling` flow: whoami yields the org id, then the subscriptions
|
|
@@ -1165,10 +1217,9 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
|
|
|
1165
1217
|
* `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
|
|
1166
1218
|
* list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
1167
1219
|
* Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
1168
|
-
* Provider-tier. Re-verified at command-code@1.88.0 (2026-10-
|
|
1169
|
-
* live `?view=client` catalog:
|
|
1170
|
-
* Large 4
|
|
1171
|
-
* (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
|
|
1220
|
+
* Provider-tier. Re-verified at command-code@1.88.0 (2026-10-27) against the
|
|
1221
|
+
* live `?view=client` catalog: 106 ids, still a strict superset chain. Mistral
|
|
1222
|
+
* Large 4 (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
|
|
1172
1223
|
* added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
|
|
1173
1224
|
* 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
|
|
1174
1225
|
* Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
|
|
@@ -1177,7 +1228,8 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
|
|
|
1177
1228
|
* which left the live catalog. Every other
|
|
1178
1229
|
* tier is unchanged. The 1.86.1 sync (2026-10-21) adds only the Ornith 1.5 122B
|
|
1179
1230
|
* launch pair at Go; the 1.87.1 check (2026-10-22) adds no model and moves no
|
|
1180
|
-
* tier,
|
|
1231
|
+
* tier, the 2026-10-24 check adds only Kimi K3.1 Pro at Go, and the 2026-10-27
|
|
1232
|
+
* check adds only GPT-6.1 Terra at Pro.
|
|
1181
1233
|
* The only removals since 1.49.0 are the two retired stealth previews (Pixel
|
|
1182
1234
|
* Canary at 1.73.1, Space Bunny Alpha at 1.74.3), Ling 3.0 Flash Sante
|
|
1183
1235
|
* (2026-10-16) and the retired Cat Jam preview (2026-10-20). Per-entry tags
|
package/lib/index.js
CHANGED
|
@@ -139,7 +139,10 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
|
|
|
139
139
|
* efforts `low/high/max`, Go tier, `$2/$10/$0.2`). The 1.88.0 artifact does not
|
|
140
140
|
* name that row yet; the default catalog, `?view=client`, the models docs page
|
|
141
141
|
* and the pricing page all do, so the capability fields come from the catalog
|
|
142
|
-
* as usual.
|
|
142
|
+
* as usual. The 2026-10-27 check finds the CLI still at 1.88.0 and one more
|
|
143
|
+
* catalog-only addition, GPT-6.1 Terra (`gpt-6.1-terra`): 1.05M context, text +
|
|
144
|
+
* image, the same five-level effort set, Pro tier, `$1/$5` up to a 272K context
|
|
145
|
+
* step and `$2/$7.5` beyond. Keeping the snapshot in its own module
|
|
143
146
|
* confines those frequent sync diffs here: src/adapter.ts holds only the stable
|
|
144
147
|
* wire/runtime logic. The read helpers live here too, for the same reason.
|
|
145
148
|
*
|
|
@@ -499,6 +502,13 @@ const KNOWN_EFFORTS = {
|
|
|
499
502
|
"xhigh",
|
|
500
503
|
"max"
|
|
501
504
|
],
|
|
505
|
+
"gpt-6.1-terra": [
|
|
506
|
+
"low",
|
|
507
|
+
"medium",
|
|
508
|
+
"high",
|
|
509
|
+
"xhigh",
|
|
510
|
+
"max"
|
|
511
|
+
],
|
|
502
512
|
"inclusionai/ling-3.1-flash": [
|
|
503
513
|
"low",
|
|
504
514
|
"medium",
|
|
@@ -581,6 +591,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
581
591
|
"gpt-6-luna",
|
|
582
592
|
"gpt-6-sol",
|
|
583
593
|
"gpt-6.1-sol",
|
|
594
|
+
"gpt-6.1-terra",
|
|
584
595
|
"meta/muse-spark-1.1",
|
|
585
596
|
"meta/muse-spark-1.2",
|
|
586
597
|
"meta/muse-spark-1.2-contributor",
|
|
@@ -934,10 +945,9 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
|
|
|
934
945
|
* `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
|
|
935
946
|
* list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
936
947
|
* Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
937
|
-
* Provider-tier. Re-verified at command-code@1.88.0 (2026-10-
|
|
938
|
-
* live `?view=client` catalog:
|
|
939
|
-
* Large 4
|
|
940
|
-
* (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
|
|
948
|
+
* Provider-tier. Re-verified at command-code@1.88.0 (2026-10-27) against the
|
|
949
|
+
* live `?view=client` catalog: 106 ids, still a strict superset chain. Mistral
|
|
950
|
+
* Large 4 (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
|
|
941
951
|
* added Claude Haiku 5.5 at Pro and the free Glyph Cluster preview at Go.
|
|
942
952
|
* 1.79.2 lowers Haiku 5.5's minimum plan to GOAT; 1.80.0 adds Muse Wonder at
|
|
943
953
|
* Max; 1.82.0 adds Gemini 4 Argon at Pro and nothing else moves. The 1.82.4
|
|
@@ -946,7 +956,8 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
|
|
|
946
956
|
* which left the live catalog. Every other
|
|
947
957
|
* tier is unchanged. The 1.86.1 sync (2026-10-21) adds only the Ornith 1.5 122B
|
|
948
958
|
* launch pair at Go; the 1.87.1 check (2026-10-22) adds no model and moves no
|
|
949
|
-
* tier,
|
|
959
|
+
* tier, the 2026-10-24 check adds only Kimi K3.1 Pro at Go, and the 2026-10-27
|
|
960
|
+
* check adds only GPT-6.1 Terra at Pro.
|
|
950
961
|
* The only removals since 1.49.0 are the two retired stealth previews (Pixel
|
|
951
962
|
* Canary at 1.73.1, Space Bunny Alpha at 1.74.3), Ling 3.0 Flash Sante
|
|
952
963
|
* (2026-10-16) and the retired Cat Jam preview (2026-10-20). Per-entry tags
|
|
@@ -1043,6 +1054,7 @@ const KNOWN_PLANS = {
|
|
|
1043
1054
|
"gpt-5.5": "pro",
|
|
1044
1055
|
"gpt-5.6-terra": "pro",
|
|
1045
1056
|
"gpt-6-sol": "pro",
|
|
1057
|
+
"gpt-6.1-terra": "pro",
|
|
1046
1058
|
"meta/muse-spark-1.1": "pro",
|
|
1047
1059
|
"claude-fable-5-1": "provider",
|
|
1048
1060
|
"claude-fable-5": "provider",
|
|
@@ -2106,6 +2118,34 @@ const MODEL_PRICE_ROWS = [
|
|
|
2106
2118
|
5
|
|
2107
2119
|
] }]
|
|
2108
2120
|
},
|
|
2121
|
+
{
|
|
2122
|
+
id: "gpt-6.1-terra",
|
|
2123
|
+
rates: [
|
|
2124
|
+
1,
|
|
2125
|
+
5,
|
|
2126
|
+
.05,
|
|
2127
|
+
1.25
|
|
2128
|
+
],
|
|
2129
|
+
allowance: {
|
|
2130
|
+
go: 6,
|
|
2131
|
+
goat: 20,
|
|
2132
|
+
pro: 20
|
|
2133
|
+
},
|
|
2134
|
+
contextTiers: [{
|
|
2135
|
+
"maxContext": 272e3,
|
|
2136
|
+
"rates": [
|
|
2137
|
+
1,
|
|
2138
|
+
5,
|
|
2139
|
+
.05,
|
|
2140
|
+
1.25
|
|
2141
|
+
]
|
|
2142
|
+
}, { "rates": [
|
|
2143
|
+
2,
|
|
2144
|
+
7.5,
|
|
2145
|
+
.1,
|
|
2146
|
+
2.5
|
|
2147
|
+
] }]
|
|
2148
|
+
},
|
|
2109
2149
|
{
|
|
2110
2150
|
id: "grok-4.5",
|
|
2111
2151
|
rates: [
|
|
@@ -6369,7 +6409,8 @@ var GatewayFacts = class {
|
|
|
6369
6409
|
if (this.state?.source !== identity) this.state = {
|
|
6370
6410
|
source: identity,
|
|
6371
6411
|
rows: [],
|
|
6372
|
-
expiresAt: 0
|
|
6412
|
+
expiresAt: 0,
|
|
6413
|
+
origin: "none"
|
|
6373
6414
|
};
|
|
6374
6415
|
return this.state;
|
|
6375
6416
|
}
|
|
@@ -6378,6 +6419,20 @@ var GatewayFacts = class {
|
|
|
6378
6419
|
const row = this.forSource(source).rows.find((candidate) => candidate.id === model);
|
|
6379
6420
|
return row === void 0 ? void 0 : copyRows([row])[0];
|
|
6380
6421
|
}
|
|
6422
|
+
/**
|
|
6423
|
+
* 本进程**联网成功过**的目录;否则返回 undefined。
|
|
6424
|
+
*
|
|
6425
|
+
* 判据是来源而不是「内存里有行」:`snapshot()` 会把磁盘缓存写进同一份
|
|
6426
|
+
* `state.rows`(生成路径需要它),若只看行数,冷启动先发过一次聊天再打开
|
|
6427
|
+
* 选择器时,首屏拿到的就是没有年龄校验的磁盘副本——正是本方法要排除的东西。
|
|
6428
|
+
* 空目录同样返回 undefined:刷新失败后的空行(`expiresAt` 已进入重试窗口)
|
|
6429
|
+
* 不能让选择器把「暂时读不到」画成「没有模型」,调用方应回落到 `refresh()`。
|
|
6430
|
+
*/
|
|
6431
|
+
loaded(source) {
|
|
6432
|
+
const state = this.forSource(source);
|
|
6433
|
+
if (state.origin !== "network" || state.rows.length === 0) return void 0;
|
|
6434
|
+
return copyRows(state.rows);
|
|
6435
|
+
}
|
|
6381
6436
|
/** 生成只读取本地目录;捕获状态后不再转向实例的新来源。 */
|
|
6382
6437
|
async snapshot(source) {
|
|
6383
6438
|
const state = this.forSource(source);
|
|
@@ -6385,7 +6440,10 @@ var GatewayFacts = class {
|
|
|
6385
6440
|
state.disk ??= readCache(source).catch(() => []);
|
|
6386
6441
|
const rows = await state.disk;
|
|
6387
6442
|
if (state.rows.length > 0 || state.expiresAt > 0) return copyRows(state.rows);
|
|
6388
|
-
if (state.inflight === void 0)
|
|
6443
|
+
if (state.inflight === void 0) {
|
|
6444
|
+
state.rows = rows;
|
|
6445
|
+
if (rows.length > 0 && state.origin === "none") state.origin = "disk";
|
|
6446
|
+
}
|
|
6389
6447
|
return copyRows(rows);
|
|
6390
6448
|
}
|
|
6391
6449
|
async refresh(source, signal) {
|
|
@@ -6398,8 +6456,10 @@ var GatewayFacts = class {
|
|
|
6398
6456
|
const mayPublish = () => this.state === state && generations.get(source.apiBase) === generation;
|
|
6399
6457
|
state.inflight = (async () => {
|
|
6400
6458
|
let rows, ttl = MODEL_CATALOG_TTL_MS;
|
|
6459
|
+
let network = false;
|
|
6401
6460
|
try {
|
|
6402
6461
|
rows = parseCatalog$1(await this.fetchCatalog(source), source.apiBase);
|
|
6462
|
+
network = true;
|
|
6403
6463
|
if (mayPublish()) {
|
|
6404
6464
|
publish(source.apiBase, rows);
|
|
6405
6465
|
await writeCache(source, rows, mayPublish).catch(() => void 0);
|
|
@@ -6411,6 +6471,8 @@ var GatewayFacts = class {
|
|
|
6411
6471
|
}
|
|
6412
6472
|
state.rows = rows;
|
|
6413
6473
|
state.expiresAt = Date.now() + ttl;
|
|
6474
|
+
if (network) state.origin = "network";
|
|
6475
|
+
else if (state.origin === "none" && rows.length > 0) state.origin = "disk";
|
|
6414
6476
|
return rows;
|
|
6415
6477
|
})().finally(() => {
|
|
6416
6478
|
delete state.inflight;
|
|
@@ -6461,6 +6523,17 @@ const MODELS_TIMEOUT_MS = 1e4;
|
|
|
6461
6523
|
/** How long the picker's plan-filter billing facts stay cached before refetching. */
|
|
6462
6524
|
const BILLING_ACCESS_TTL_MS = 3e5;
|
|
6463
6525
|
/**
|
|
6526
|
+
* How long the raw account facts captured by one usage-report run stay usable
|
|
6527
|
+
* for the picker's advisory plan filter.
|
|
6528
|
+
*
|
|
6529
|
+
* This is much shorter than {@link BILLING_ACCESS_TTL_MS} because it only ever
|
|
6530
|
+
* removes a DUPLICATE read: the picker and the settings card ask the same three
|
|
6531
|
+
* endpoints about the same key, and whichever ran last already paid for them.
|
|
6532
|
+
* Anything older than this window is simply fetched again — the filter is
|
|
6533
|
+
* fail-open advisory data, so a miss costs one round trip and nothing else.
|
|
6534
|
+
*/
|
|
6535
|
+
const ACCOUNT_FACTS_TTL_MS = 3e4;
|
|
6536
|
+
/**
|
|
6464
6537
|
* How long a learned CLI-only protocol preference stays cached per account.
|
|
6465
6538
|
* After this TTL a request may probe Provider API again, so an upgraded Go
|
|
6466
6539
|
* account can recover without a restart.
|
|
@@ -6552,9 +6625,13 @@ function fingerprintValue(value) {
|
|
|
6552
6625
|
}
|
|
6553
6626
|
return {
|
|
6554
6627
|
bytes: Buffer.byteLength(serialized),
|
|
6555
|
-
sha256:
|
|
6628
|
+
sha256: sha256Hex(serialized)
|
|
6556
6629
|
};
|
|
6557
6630
|
}
|
|
6631
|
+
/** Digest of already-serialized text: no second stringify, no retained copy. */
|
|
6632
|
+
function sha256Hex(serialized) {
|
|
6633
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
6634
|
+
}
|
|
6558
6635
|
/**
|
|
6559
6636
|
* Structural request facts for issue #64's cache diagnosis.
|
|
6560
6637
|
*
|
|
@@ -6670,6 +6747,16 @@ function headersTimeoutError(input) {
|
|
|
6670
6747
|
/** How deep a root `$ref` / combinator chain is followed while normalizing. */
|
|
6671
6748
|
const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
|
|
6672
6749
|
/**
|
|
6750
|
+
* Depth-0 normalization results, keyed by the caller's own `parameters` object.
|
|
6751
|
+
*
|
|
6752
|
+
* {@link toolParametersSchema} only ever reads the root, so a schema that has
|
|
6753
|
+
* already been normalized returns the identical answer however many times it is
|
|
6754
|
+
* asked. The tool list is rebuilt for EVERY attempt — a rotation or a protocol
|
|
6755
|
+
* fallback rebuilds it again — and the objects come from the harness, so they
|
|
6756
|
+
* stay identical for the life of a turn: identity is an exact key.
|
|
6757
|
+
*/
|
|
6758
|
+
const TOOL_SCHEMA_CACHE = /* @__PURE__ */ new WeakMap();
|
|
6759
|
+
/**
|
|
6673
6760
|
* Whether a type-less node is object-shaped enough to be one with the type
|
|
6674
6761
|
* declared: `properties`/`required`/`additionalProperties` only make sense on
|
|
6675
6762
|
* an object, and a schema carrying them was meant to be one.
|
|
@@ -6715,7 +6802,7 @@ function mergeCombinatorBranches(node, depth) {
|
|
|
6715
6802
|
const objectBranches = [];
|
|
6716
6803
|
for (const branch of branches) {
|
|
6717
6804
|
if (!isRecord(branch)) continue;
|
|
6718
|
-
objectBranches.push(
|
|
6805
|
+
objectBranches.push(normalizeToolParameters(branch, depth + 1));
|
|
6719
6806
|
}
|
|
6720
6807
|
if (objectBranches.length === 0) continue;
|
|
6721
6808
|
sawBranch = true;
|
|
@@ -6745,10 +6832,32 @@ function mergeCombinatorBranches(node, depth) {
|
|
|
6745
6832
|
* resolved/merged; anything else (an array/scalar root, or no schema at all)
|
|
6746
6833
|
* degrades to a permissive free-form object, because a request the provider
|
|
6747
6834
|
* refuses helps no one and the tool's own description is still in the prompt.
|
|
6748
|
-
*
|
|
6749
|
-
*
|
|
6750
|
-
|
|
6751
|
-
function
|
|
6835
|
+
*
|
|
6836
|
+
* Only the ROOT is touched. The object-root fast path deliberately returns the
|
|
6837
|
+
* caller's own object: an already-normalized schema is the common case, the
|
|
6838
|
+
* function has always answered with that same identity, and callers rely on it
|
|
6839
|
+
* to get the same frozen schema they handed in. Everything this function
|
|
6840
|
+
* CONSTRUCTS is a fresh object, so no caller can mutate another's schema.
|
|
6841
|
+
*
|
|
6842
|
+
* The ROOT is memoized per `parameters` object, because every transport rebuilds
|
|
6843
|
+
* its tool list on every attempt — on a rotating or downshifting request the
|
|
6844
|
+
* same handful of tool schemas is walked four, five, six times for an answer
|
|
6845
|
+
* that cannot have changed. The tool objects come from the harness and are
|
|
6846
|
+
* stable for the life of a turn, so a WeakMap keyed on identity is exact: a
|
|
6847
|
+
* genuinely different schema is a different object and misses. Nested
|
|
6848
|
+
* `$ref`/combinator branches are reached through {@link normalizeToolParameters}
|
|
6849
|
+
* and are not memoized; only the root is asked for on a hot path.
|
|
6850
|
+
*/
|
|
6851
|
+
function toolParametersSchema(parameters) {
|
|
6852
|
+
if (!isRecord(parameters)) return normalizeToolParameters(parameters, 0);
|
|
6853
|
+
const cached = TOOL_SCHEMA_CACHE.get(parameters);
|
|
6854
|
+
if (cached !== void 0) return cached;
|
|
6855
|
+
const normalized = normalizeToolParameters(parameters, 0);
|
|
6856
|
+
TOOL_SCHEMA_CACHE.set(parameters, normalized);
|
|
6857
|
+
return normalized;
|
|
6858
|
+
}
|
|
6859
|
+
/** Depth-0 normalization of {@link toolParametersSchema}; see its memo there. */
|
|
6860
|
+
function normalizeToolParameters(parameters, depth) {
|
|
6752
6861
|
if (!isRecord(parameters)) return {
|
|
6753
6862
|
type: "object",
|
|
6754
6863
|
properties: {},
|
|
@@ -6764,7 +6873,7 @@ function toolParametersSchema(parameters, depth = 0) {
|
|
|
6764
6873
|
const target = resolveLocalRef(parameters, parameters.$ref);
|
|
6765
6874
|
if (target !== void 0) {
|
|
6766
6875
|
const { $ref: _ref, ...rest } = parameters;
|
|
6767
|
-
return
|
|
6876
|
+
return normalizeToolParameters({
|
|
6768
6877
|
...target,
|
|
6769
6878
|
...rest
|
|
6770
6879
|
}, depth + 1);
|
|
@@ -7000,26 +7109,47 @@ function coreBudget(budget) {
|
|
|
7000
7109
|
*/
|
|
7001
7110
|
const imageVersionBytes = (block) => block.attachment.bytes;
|
|
7002
7111
|
/**
|
|
7112
|
+
* The accounting half of the image gate: project the surface's durable offload
|
|
7113
|
+
* marks and count what the byte budget still needs — WITHOUT throwing.
|
|
7114
|
+
*
|
|
7115
|
+
* The throw has to wait until the credential is known. A request that cannot
|
|
7116
|
+
* authenticate must not trigger a durable (irreversible) image offload before
|
|
7117
|
+
* the caller learns the key is missing; keeping the count pure lets
|
|
7118
|
+
* `streamAttempt()` run it before ANY I/O and report ONE error later, once the
|
|
7119
|
+
* real transport is known.
|
|
7120
|
+
*/
|
|
7121
|
+
function projectSurfaceBudget(options, policy, budget) {
|
|
7122
|
+
const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
|
|
7123
|
+
const messages = marked === options.messages ? options.messages : [...marked];
|
|
7124
|
+
return {
|
|
7125
|
+
options: messages === options.messages ? options : {
|
|
7126
|
+
...options,
|
|
7127
|
+
messages
|
|
7128
|
+
},
|
|
7129
|
+
budgetMissing: policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes)
|
|
7130
|
+
};
|
|
7131
|
+
}
|
|
7132
|
+
/**
|
|
7003
7133
|
* Request options under one budget rung: the surface's durable marks become
|
|
7004
7134
|
* placeholder text, and an over-budget history FAILS with the count to offload
|
|
7005
7135
|
* instead of being evicted here. Throws rather than returning, so no caller can
|
|
7006
7136
|
* accidentally send a body this route already knows is too large.
|
|
7007
7137
|
*
|
|
7138
|
+
* This is the one-shot form (budget + cache in a single error) used by the 413
|
|
7139
|
+
* downshift and the protocol fallback. `streamAttempt()`'s first pass uses
|
|
7140
|
+
* {@link projectSurfaceBudget} instead, because there the throw must wait for
|
|
7141
|
+
* the credential.
|
|
7142
|
+
*
|
|
7008
7143
|
* The next rung is applied to the CURRENT projection, never to the raw history —
|
|
7009
7144
|
* otherwise the 413 retry could reintroduce images the first rung had already
|
|
7010
7145
|
* evicted.
|
|
7011
7146
|
*/
|
|
7012
7147
|
function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
|
|
7013
|
-
const
|
|
7014
|
-
const
|
|
7015
|
-
const
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
|
|
7019
|
-
return messages === options.messages ? options : {
|
|
7020
|
-
...options,
|
|
7021
|
-
messages
|
|
7022
|
-
};
|
|
7148
|
+
const projected = projectSurfaceBudget(options, policy, budget);
|
|
7149
|
+
const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(projected.options.messages, cacheOffloadModel);
|
|
7150
|
+
const missing = Math.max(projected.budgetMissing, cacheMissing);
|
|
7151
|
+
if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
|
|
7152
|
+
return projected.options;
|
|
7023
7153
|
}
|
|
7024
7154
|
/**
|
|
7025
7155
|
* Count retained images already followed by an answer from this same model.
|
|
@@ -7977,6 +8107,25 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
7977
8107
|
surfaceOffload;
|
|
7978
8108
|
billingAccess = /* @__PURE__ */ new Map();
|
|
7979
8109
|
billingAccessInflight = /* @__PURE__ */ new Map();
|
|
8110
|
+
/**
|
|
8111
|
+
* Successful account-endpoint responses, keyed by gateway + key + exact URL.
|
|
8112
|
+
*
|
|
8113
|
+
* The settings card and the picker's advisory plan filter ask the SAME three
|
|
8114
|
+
* endpoints about the SAME account (`/alpha/whoami`, then credits and
|
|
8115
|
+
* subscriptions in parallel). Here that is four round trips per surface for
|
|
8116
|
+
* two answers — and the card refreshes on a timer, so the picker keeps paying
|
|
8117
|
+
* for rows it already has. Sharing the raw responses removes the duplicate
|
|
8118
|
+
* without either surface having to know about the other: whoever asks second
|
|
8119
|
+
* within {@link ACCOUNT_FACTS_TTL_MS} reads the same record instead of the
|
|
8120
|
+
* wire.
|
|
8121
|
+
*
|
|
8122
|
+
* Only SUCCESSFUL records are kept. A failure must be retried rather than
|
|
8123
|
+
* replayed, or an endpoint that recovered a second ago would keep reporting
|
|
8124
|
+
* yesterday's outage — and the usage report's per-endpoint `failures` and
|
|
8125
|
+
* {@link classifyTotalFailure} verdict would describe a request this process
|
|
8126
|
+
* never made.
|
|
8127
|
+
*/
|
|
8128
|
+
accountJson = /* @__PURE__ */ new Map();
|
|
7980
8129
|
protocolCache = /* @__PURE__ */ new Map();
|
|
7981
8130
|
/** Namespace account-derived facts by gateway without exposing the key. */
|
|
7982
8131
|
accountCacheKey(apiBase, apiKey) {
|
|
@@ -8016,10 +8165,10 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8016
8165
|
* @returns the error to surface: unchanged for any other failure, the
|
|
8017
8166
|
* original timeout below the threshold, and a terminal one at it.
|
|
8018
8167
|
*/
|
|
8019
|
-
gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
|
|
8168
|
+
gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId, sent) {
|
|
8020
8169
|
if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
|
|
8021
8170
|
const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
|
|
8022
|
-
const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
|
|
8171
|
+
const fingerprint = `${protocol}:${sent === void 0 ? fingerprintValue(body).sha256 : sha256Hex(sent)}`;
|
|
8023
8172
|
const previous = this.headerTimeoutStreaks.get(key);
|
|
8024
8173
|
const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
|
|
8025
8174
|
if (previous === void 0) {
|
|
@@ -8122,7 +8271,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8122
8271
|
}
|
|
8123
8272
|
async listModels(provider, opts) {
|
|
8124
8273
|
const connection = { ...this.deps.options() };
|
|
8125
|
-
const
|
|
8274
|
+
const cached = this.gatewayFacts.loaded(connection);
|
|
8275
|
+
const catalog = cached ?? await this.gatewayFacts.refresh(connection);
|
|
8276
|
+
if (cached !== void 0) this.gatewayFacts.refresh(connection).catch(() => void 0);
|
|
8126
8277
|
const toInfo = (model) => {
|
|
8127
8278
|
const vision = KNOWN_IMAGE_MODELS.has(model.id);
|
|
8128
8279
|
return {
|
|
@@ -8187,13 +8338,33 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8187
8338
|
})) } } : {}
|
|
8188
8339
|
};
|
|
8189
8340
|
}
|
|
8190
|
-
/**
|
|
8191
|
-
|
|
8341
|
+
/**
|
|
8342
|
+
* Resolve and validate the key one account-endpoint call will use.
|
|
8343
|
+
*
|
|
8344
|
+
* Every account endpoint runs the chat path's own `assertUsableApiKey` check,
|
|
8345
|
+
* or a key carrying a character no HTTP header can hold throws inside `fetch`
|
|
8346
|
+
* BEFORE any I/O — once per endpoint — and the report reads as "the network is
|
|
8347
|
+
* down". The credential reference is read structurally: it belongs to the
|
|
8348
|
+
* host's resolved options, not to this adapter's connection type, and it only
|
|
8349
|
+
* names the key in the rejection message.
|
|
8350
|
+
*
|
|
8351
|
+
* Separate from {@link accountHeadersFor} so a caller that also needs to key a
|
|
8352
|
+
* cache by the same identity does not have to resolve the key twice.
|
|
8353
|
+
*/
|
|
8354
|
+
async usableAccountKey(apiKey) {
|
|
8192
8355
|
const connection = this.deps.options();
|
|
8193
8356
|
const raw = apiKey ?? await this.deps.resolveApiKey(connection);
|
|
8194
8357
|
const named = connection.apiKeyEnv;
|
|
8358
|
+
return assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential");
|
|
8359
|
+
}
|
|
8360
|
+
/** The headers every authenticated account endpoint shares. */
|
|
8361
|
+
async accountHeaders(apiKey) {
|
|
8362
|
+
return this.accountHeadersFor(await this.usableAccountKey(apiKey));
|
|
8363
|
+
}
|
|
8364
|
+
/** Headers for an already-validated key; see {@link usableAccountKey}. */
|
|
8365
|
+
accountHeadersFor(key) {
|
|
8195
8366
|
return {
|
|
8196
|
-
Authorization: `Bearer ${
|
|
8367
|
+
Authorization: `Bearer ${key}`,
|
|
8197
8368
|
...IDENTITY_ENCODING_HEADER,
|
|
8198
8369
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
8199
8370
|
"x-cli-environment": "production",
|
|
@@ -8313,6 +8484,42 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8313
8484
|
return inflight;
|
|
8314
8485
|
}
|
|
8315
8486
|
/**
|
|
8487
|
+
* One account-endpoint GET, served from {@link accountJson} when `share` and
|
|
8488
|
+
* the same URL answered successfully within {@link ACCOUNT_FACTS_TTL_MS}.
|
|
8489
|
+
*
|
|
8490
|
+
* `timeoutMs` is the CALLER's budget; a cache hit spends none of it. Failures
|
|
8491
|
+
* are never cached, so this can only ever save a request that would have
|
|
8492
|
+
* re-read an answer this process already had.
|
|
8493
|
+
*
|
|
8494
|
+
* `share: false` marks a VOLATILE endpoint — one whose answer is the number
|
|
8495
|
+
* the user is actually looking at, and which must therefore never be replayed
|
|
8496
|
+
* from an earlier request. The usage card's summary is exactly that: its
|
|
8497
|
+
* headline counts and cost are the reason the card exists, and stamping a
|
|
8498
|
+
* cached copy with the current time would misreport when they were read.
|
|
8499
|
+
* Identity and billing facts are the opposite: a plan tier or an org id that
|
|
8500
|
+
* is 30 s old is still the truth, and those are what the picker duplicates.
|
|
8501
|
+
*/
|
|
8502
|
+
async accountJsonGet(apiBase, apiKey, path, timeoutMs, share = true) {
|
|
8503
|
+
const cacheKey = JSON.stringify([
|
|
8504
|
+
apiBase,
|
|
8505
|
+
apiKey,
|
|
8506
|
+
path
|
|
8507
|
+
]);
|
|
8508
|
+
if (share) {
|
|
8509
|
+
const hit = this.accountJson.get(cacheKey);
|
|
8510
|
+
if (hit !== void 0 && Date.now() - hit.at < ACCOUNT_FACTS_TTL_MS) return {
|
|
8511
|
+
status: 200,
|
|
8512
|
+
record: hit.record
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
8515
|
+
const answer = await this.fetchJson(`${apiBase}${path}`, this.accountHeadersFor(apiKey), timeoutMs);
|
|
8516
|
+
if (share && answer.record !== void 0) this.accountJson.set(cacheKey, {
|
|
8517
|
+
record: answer.record,
|
|
8518
|
+
at: Date.now()
|
|
8519
|
+
});
|
|
8520
|
+
return answer;
|
|
8521
|
+
}
|
|
8522
|
+
/**
|
|
8316
8523
|
* The billing facts behind the picker's plan filter, mirroring the CLI's
|
|
8317
8524
|
* `createBilling` flow: whoami yields the org id, then the subscriptions
|
|
8318
8525
|
* and credits endpoints answer in parallel. The plan id is honored only
|
|
@@ -8323,8 +8530,8 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8323
8530
|
*/
|
|
8324
8531
|
async fetchBillingAccess(apiBase, apiKey) {
|
|
8325
8532
|
try {
|
|
8326
|
-
const
|
|
8327
|
-
const getJson = async (path) => (await this.
|
|
8533
|
+
const key = await this.usableAccountKey(apiKey);
|
|
8534
|
+
const getJson = async (path) => (await this.accountJsonGet(apiBase, key, path, MODELS_TIMEOUT_MS)).record;
|
|
8328
8535
|
const whoami = await getJson("/alpha/whoami");
|
|
8329
8536
|
const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
|
|
8330
8537
|
const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
|
|
@@ -8431,9 +8638,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8431
8638
|
async getUsage(apiKey, capturedConnection) {
|
|
8432
8639
|
const connection = capturedConnection ?? { ...this.deps.options() };
|
|
8433
8640
|
const base = connection.apiBase;
|
|
8434
|
-
let
|
|
8641
|
+
let key;
|
|
8435
8642
|
try {
|
|
8436
|
-
|
|
8643
|
+
key = await this.usableAccountKey(apiKey);
|
|
8437
8644
|
} catch (error) {
|
|
8438
8645
|
if (error instanceof LlmError && error.code === "INVALID_CREDENTIAL") return {
|
|
8439
8646
|
failures: [error.message],
|
|
@@ -8443,9 +8650,9 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8443
8650
|
}
|
|
8444
8651
|
const failures = [];
|
|
8445
8652
|
const failed = [];
|
|
8446
|
-
const getJson = async (path) => {
|
|
8653
|
+
const getJson = async (path, share = true) => {
|
|
8447
8654
|
try {
|
|
8448
|
-
const { status, record, responseFailure } = await this.
|
|
8655
|
+
const { status, record, responseFailure } = await this.accountJsonGet(base, key, path, connection.requestTimeoutMs, share);
|
|
8449
8656
|
if (record === void 0) {
|
|
8450
8657
|
failures.push(`${path}: HTTP ${status}${responseFailure ? `;${responseFailure}` : ""}`);
|
|
8451
8658
|
failed.push(responseFailure ? {
|
|
@@ -8479,7 +8686,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8479
8686
|
const rawStart = subData?.currentPeriodStart;
|
|
8480
8687
|
const periodStart = periodTimestampValue(rawStart);
|
|
8481
8688
|
const validStart = periodStart > 0 && Number.isFinite(new Date(periodStart).getTime());
|
|
8482
|
-
const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0)));
|
|
8689
|
+
const totals = parseUsageTotals(await getJson(scopedPath("/alpha/usage/summary", validStart ? String(rawStart) : void 0), false));
|
|
8483
8690
|
if (totals !== void 0) report.usage = totals;
|
|
8484
8691
|
const limits = parseCreditLimits(credits);
|
|
8485
8692
|
if (limits !== void 0) report.credits = limits;
|
|
@@ -8634,23 +8841,15 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8634
8841
|
if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
8635
8842
|
readImage = requestImages(attachments, options.signal, timing);
|
|
8636
8843
|
}
|
|
8637
|
-
const finishCredentials = timing.phase("credentials");
|
|
8638
|
-
let apiKey;
|
|
8639
|
-
try {
|
|
8640
|
-
apiKey = await this.deps.resolveApiKey(connection, options.model);
|
|
8641
|
-
} finally {
|
|
8642
|
-
finishCredentials();
|
|
8643
|
-
}
|
|
8644
|
-
options.signal?.throwIfAborted();
|
|
8645
8844
|
let maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
|
|
8845
|
+
const builtMaxTokens = maxTokens;
|
|
8646
8846
|
const supported = KNOWN_EFFORTS[options.model];
|
|
8647
8847
|
const effort = options.purpose === "session-title" && supported?.includes("off") ? "off" : options.reasoningEffort;
|
|
8648
8848
|
const declared = effort !== void 0 && supported?.includes(effort) ? effort : void 0;
|
|
8649
8849
|
const reasoningEffort = effort === "off" && declared === void 0 ? void 0 : declared;
|
|
8650
8850
|
const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
|
|
8651
|
-
|
|
8652
|
-
let
|
|
8653
|
-
let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
|
|
8851
|
+
const projected = projectSurfaceBudget(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
|
|
8852
|
+
let requestOptions = projected.options;
|
|
8654
8853
|
const buildBody = async (target) => {
|
|
8655
8854
|
const finishBody = timing.phase("body");
|
|
8656
8855
|
try {
|
|
@@ -8668,7 +8867,30 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8668
8867
|
finishBody();
|
|
8669
8868
|
}
|
|
8670
8869
|
};
|
|
8671
|
-
|
|
8870
|
+
const provisionalProtocol = connection.protocol === "cli" ? "cli" : this.routesToMessages(options.model, modelEntry) ? "messages" : "openai";
|
|
8871
|
+
const finishCredentials = timing.phase("credentials");
|
|
8872
|
+
const credentials = this.deps.resolveApiKey(connection, options.model);
|
|
8873
|
+
credentials.catch(() => void 0);
|
|
8874
|
+
const provisionalBody = connection.offloadSeenImagesForCache === true || projected.budgetMissing > 0 ? void 0 : buildBody(provisionalProtocol);
|
|
8875
|
+
provisionalBody?.catch(() => void 0);
|
|
8876
|
+
let apiKey;
|
|
8877
|
+
try {
|
|
8878
|
+
apiKey = await credentials;
|
|
8879
|
+
} finally {
|
|
8880
|
+
finishCredentials();
|
|
8881
|
+
}
|
|
8882
|
+
options.signal?.throwIfAborted();
|
|
8883
|
+
maxTokens = Math.min(budget.max, learnedOutputLimit(connection.apiBase, options.model));
|
|
8884
|
+
let protocol = this.resolveProtocol(connection, apiKey, options.model, modelEntry);
|
|
8885
|
+
const cacheMissing = protocol === "cli" && connection.offloadSeenImagesForCache === true ? seenImageOffloadCount(requestOptions.messages, options.model) : 0;
|
|
8886
|
+
const missing = Math.max(projected.budgetMissing, cacheMissing);
|
|
8887
|
+
if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > projected.budgetMissing ? "cache" : "budget");
|
|
8888
|
+
let messagesPermissionRejection;
|
|
8889
|
+
let body = provisionalBody !== void 0 && protocol === provisionalProtocol ? await provisionalBody : await buildBody(protocol);
|
|
8890
|
+
if (maxTokens !== builtMaxTokens) {
|
|
8891
|
+
budget.max = Math.min(budget.max, maxTokens);
|
|
8892
|
+
if (!omitOutputTokens(connection.maxOutputTokens, protocol, options.maxTokens)) assignMaxTokens(body, protocol, maxTokens);
|
|
8893
|
+
}
|
|
8672
8894
|
const serializedBodies = /* @__PURE__ */ new WeakMap();
|
|
8673
8895
|
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
8674
8896
|
const tried = /* @__PURE__ */ new Set();
|
|
@@ -8790,7 +9012,7 @@ var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
|
8790
9012
|
});
|
|
8791
9013
|
trace.close();
|
|
8792
9014
|
if (options.signal?.aborted) throw error;
|
|
8793
|
-
throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
|
|
9015
|
+
throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId, serializedBodies.get(body)), responseMetadata);
|
|
8794
9016
|
}
|
|
8795
9017
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
8796
9018
|
const { response, cleanup } = connected;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars-sea/dsh-commandcode-provider",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Unofficial DeepSeek Harness LLM provider plugin for Command Code, ported from pi-commandcode-provider (MIT). Registers the 'commandcode' provider route with a Models-page card and live model catalog.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"compatibility": {
|
|
25
25
|
"dshReleases": {
|
|
26
26
|
"0.2.1-rc.2": "compatible",
|
|
27
|
-
"0.2.1-rc.3": "compatible"
|
|
27
|
+
"0.2.1-rc.3": "compatible",
|
|
28
|
+
"0.2.1-rc.4": "compatible"
|
|
28
29
|
},
|
|
29
30
|
"dsh": ">=0.2.1-rc.2 <0.2.2-0"
|
|
30
31
|
},
|
|
@@ -141,10 +142,15 @@
|
|
|
141
142
|
"@deepseek-ai/schemastery": "~3.18.5",
|
|
142
143
|
"@types/node": "^22.0.0",
|
|
143
144
|
"@types/react": "^18.3.1",
|
|
145
|
+
"@types/react-test-renderer": "^19.3.0",
|
|
144
146
|
"parse5": "8.0.1",
|
|
145
147
|
"react": "^18.2.0",
|
|
148
|
+
"react-test-renderer": "^18.3.1",
|
|
146
149
|
"tsdown": "^0.22.2",
|
|
147
150
|
"tsx": "^4.23.12",
|
|
148
151
|
"typescript": "^6.0.3"
|
|
152
|
+
},
|
|
153
|
+
"overrides": {
|
|
154
|
+
"zod": "^4.6.6"
|
|
149
155
|
}
|
|
150
156
|
}
|