@eddyskywalker/dsh-chatgpt-subscription 0.16.0 → 0.17.1

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +3 -2
  3. package/lib/client.js +121 -1
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +3122 -2472
  6. package/lib/types/client/antigravity/AntigravitySection.d.ts.map +1 -1
  7. package/lib/types/client/antigravity/locales.d.ts +47 -2
  8. package/lib/types/client/antigravity/locales.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/locales.d.ts +2 -2
  10. package/lib/types/host/antigravity/account-pool.d.ts +24 -0
  11. package/lib/types/host/antigravity/account-pool.d.ts.map +1 -1
  12. package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
  13. package/lib/types/host/antigravity/cache-stats.d.ts +166 -0
  14. package/lib/types/host/antigravity/cache-stats.d.ts.map +1 -0
  15. package/lib/types/host/antigravity/mapper.d.ts +75 -3
  16. package/lib/types/host/antigravity/mapper.d.ts.map +1 -1
  17. package/lib/types/host/antigravity/routes.d.ts +1 -1
  18. package/lib/types/host/antigravity/routes.d.ts.map +1 -1
  19. package/lib/types/host/claude/mapper.d.ts.map +1 -1
  20. package/lib/types/host/command-code/mapper.d.ts.map +1 -1
  21. package/lib/types/host/common/request-images.d.ts +22 -2
  22. package/lib/types/host/common/request-images.d.ts.map +1 -1
  23. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  24. package/lib/types/host/kimi-code/mapper.d.ts +16 -1
  25. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  26. package/lib/types/host/kimi-code/types.d.ts +32 -0
  27. package/lib/types/host/kimi-code/types.d.ts.map +1 -1
  28. package/lib/types/host/workbuddy/mapper.d.ts.map +1 -1
  29. package/lib/types/shared/antigravity-contracts.d.ts +76 -0
  30. package/lib/types/shared/antigravity-contracts.d.ts.map +1 -1
  31. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.17.1 - 2026-10-10
6
+
7
+ - **[Kimi Code] 请求体超限时,「还有图可丢吗」的判定与计量脱节,兜底一次都没跑([#51](https://github.com/Aa728848/dsh-chatgpt-subscription/issues/51))**
8
+ - **`requestHasSendableImage()` 只扫消息顶层,不递归 `tool-result`。** 同文件的另外三个兄弟函数全部递归——`collectImageRefs`、`collectRequestImagePayloads`(其注释明写 *RECURSION IS NOT OPTIONAL*)、`replaceOldestRequestImages`。于是对「图片全部来自工具结果」的请求,计量数得出 N 张图,判定恒返回 `false`。
9
+ - **后果不是「丢错图」,而是「一张都没丢」。** `kimi-code/adapter.ts` 的 body-fit 循环从 `attempt = 0` 起算,第一轮就短路,直接 `assertRequestBodyFits` 抛 `PROVIDER_ERROR`,唯一的丢图入口从未执行;`MAX_BODY_FIT_ATTEMPTS = 64` 形同虚设。报告方的现场只超限 **1494 字节**,丢一张约 150 KB 的图即可解决。混合会话同样中招:顶层图被丢光后,tool-result 里剩下的图一张不动,然后报错说「会先丢最旧的图」。
10
+ - **报错文案同时说了反话。** `oversizedBodyMessage` 的 remedy 按字节构成判定(`imageBytes >= otherBytes`),于是文案写着 “Older images are omitted first”,而实际一张都没丢。讽刺的是同文件的 `requestBodyBreakdown` 走的是全递归的 `walkJson`——**报错里的图片字节数看得见那些图,决定要不要继续丢图的门禁看不见。**
11
+ - **修复:把判定改成计量的定义式复用。** `requestHasSendableImage(options, images)` 现在就是 `requestImagePayloadLengths(options, images).length > 0`。手写第五遍递归也能修好这一例,但那条路保留了「同一条递归规则被抄多遍、下次再漏一遍」的结构性风险;复用定义让两者**按构造**不可能再分叉。
12
+ - **顺带统一了同一路径上的第二处口径错配(独立缺陷)。** `dropOldestImagesUntilFits` 里 `requestImagePayloadLengths(baseline, resolved)` 带 images 映射,`countOmitted` 里两次调用**不带**,`replaceOldestRequestImages` 的跳过判断也**不带**。三处只在 `unavailable`(读不出来的图)上分叉:不带映射时它回落 `base64Length(attachment.bytes)` 被当作「有载荷」,于是 `alreadyDropped` 虚高、`slice` 提前掏空、`additional` 算成 0,循环空转到 64 轮才抛错。现在三处统一传入同一个 `images`,`replaceOldestRequestImages` 新增可选形参。**只改计数端是不够的**:替换端若不同步改,`lengths[N]` 与「第 N 个可替换块」仍然错位。
13
+ - **附带效果**:`unavailable` 图现在会被判定为「不是可发送的图」,这类请求因此**立即 fail fast**,而不是白白重建 65 次几 MB 的请求体。
14
+ - 测试:`test/kimi-code-body-fit.test.ts` 新增 5 例——不变量(嵌套图、**丢图之前**判定必须与计量一致)、超限请求丢一张即可恢复、顶层图仍被看见(防修复被推到反方向)、`unavailable` 图不算作可丢(防过度递归)、替换名额必须花在真正带字节的图上。**承重已验证**:`git stash push -- src/host/common/request-images.ts src/host/kimi-code/adapter.ts` 后实测 **2 failed**(`expected false to be true`、``expected ['shot'] to deeply equal ['broken']``),还原后全过。
15
+ - 既有两条断言(`:143` 顶层图 `true`、`:153` 丢图后 `false`)**逐字未改仍然成立**——它们此前就与修复后的实现同值,只是从未覆盖真正出问题的那个状态。
16
+ - 验证:`tsc -b --pretty false` 与 `tsc -p test/tsconfig.json` 均 0 错误;全量 `npm test` **2961 passed / 7 skipped, 0 failed**(较基线增 5 例)。
17
+ - **未一并处理的**(与 0.13.2 记下的同一批):claude / command-code / workbuddy / antigravity 四条线路的计量与替换仍只扫顶层,工具结果里的图对它们的图片预算系统性失明;它们没有布尔门,不会本地抛错,表现为静默超预算后被上游 413/400 拒绝。codex 干净(在构造后的 input 上测量),minimax-code 干净(复用共享递归实现)。
18
+
19
+ - **[Kimi Code] 四条线路的图片预算让工具结果里的图「隐形」,以及字节上限缺出口 —— 补完 [#51](https://github.com/Aa728848/dsh-chatgpt-subscription/issues/51) 的遗留项**
20
+ - **状态 A 换了一条路存活。** 310590b 关掉的是「`tool-result` 里的图对门禁不可见」那一个实例;但 `dropOldestImagesUntilFits` 在「剩余图片总字节 < 超限缺口」时算出 `additional = 0` 并**原样返回**,调用方无法区分「成功了」与「没进展」,于是下一轮重建出**完全相同**的 body,空转到 `attempt > 64` 才抛出——期间连续构建 65 次几 MB 的请求体。实测可达:图片 1.6 MB + 文本 1.5 MB + schema 0.7 MB = 3.8 MB,缺口 1.7 MB > 图片 1.6 MB。
21
+ - **无进展检测。** 该函数改为返回 `{ options, dropped }`,`dropped === 0` 时立即停止并带着上下文抛错,不再消耗 attempt 预算重建一个不可能改变的请求。
22
+ - **报错文案从两态改为三态。** 判据从「字节构成猜测」换成「循环实际做了什么」:①**有图可丢但丢了也补不上缺口** → 明确说明剩下的图比缺口小,省略全部也装不下(不再声称「Older images are omitted first」);②**图已全部省略仍超限** → 报出已省略的张数;③**本来就没有图** → 直接排除删图这个无效动作。`BodyFitContext` 是可选第三参,**不传时逐字沿用原文案**,现有 14 条断言一条未改仍通过。
23
+ - **kimi-code 的字节上限补上环境变量出口**,`DSH_KIMI_CODE_MAX_BODY_BYTES` / `DSH_KIMI_CODE_MAX_IMAGE_BYTES`,与 MiniMax / Codex 两条线对齐(resolver 逐字同构:trim → 空串回落 → 非正有限数回落 → floor)。**视频上限不加出口**:三条线对称(minimax 同样是裸常量、codex 无视频),且一个变量管两档会让按 2 MB 场景设的值顺手压垮 64 MiB 视频档。共享常量 `common/request-images.ts` 的值与默认参数**未动**——那 8 处无参调用仍依赖它。
24
+ - **claude / command-code / workbuddy / antigravity 四条线路的工具结果图片重新进入预算。** 四条各自持有私有副本(预算数字不同、缩放策略不同、attachment seam 不同),实测**不能**合并到共享实现:合并会同时替换四条线路的度量口径、缩放策略与取消语义,且有两处行为变化**不会让任何现有用例变红**(用例都不传 images 映射,走 `images === undefined` 分支)——那是最危险的一类。因此逐条把度量和替换改为递归,**零测试期望值变化**。
25
+ - 顺带记录一处**刻意保留**的不一致:claude 的 `countRequestImages`(claude/mapper.ts:885-894)早已递归,而同文件的 `collectRequestImageBytes` 此前不递归——这正是本 bug 的**第五次**复制。统一它会让 `requestImageEdgeLimit` 不再把 `unavailable` 的图计入,而现有测试钉住了「刻意过度计数」的语义(低上限永远不会导致拒绝),故不动。
26
+ - 测试:新增 `test/tool-result-image-budget.test.ts`(13 例,`describe.each` 覆盖四条线路的「计量」「替换名额顺序」「装得下时不动」)、`test/kimi-code-body-limits.test.ts`(6 例,出口生效 / 非法值回落矩阵 / 默认值即 2,097,152 与 1,500,000)、`test/kimi-code-body-fit.test.ts` +4 例(三态文案各一条 + 不传参数时逐字不变);`test/kimi-code-adapter.test.ts` +1 例(适配器级「无进展即停止」,此前 `KimiCodeAdapter` 的 `options.attachments` 在测试中**从未被注入过**,那段 body-fit 循环零端到端覆盖)。`buildAdapter` 因此新增 `fetchFn` / `attachments` 两个可选参数。
27
+ - **承重已验证**:`git stash push -- ` 七个源文件后实测 **18 failed**(三态文案 3、无进展 1、四线路递归 9、环境出口 5),还原后全过。
28
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2985 passed / 7 skipped, 0 failed**(较 310590b 的 2961 增 24 例);`npm run build` 干净。
29
+ - **未一并处理的**:`/compact` 的 “could not produce a useful summary” 与真实原因不符。已复核——真实原因**已经落盘**在 `compaction/end.error`,宿主 `ManualCompactionError` 自己也带着 `message` 与 `cause`,是 `command-compact` **既不读 session 也不渲染自己的 error.message**。本插件的 `inject` 里没有 `commands` / `compaction` / `sessions`,也无法重名注册 `compact`(宿主 registry 硬拒)。这是 deepseek-harness 的修复点,不在本仓库能力范围内。
30
+
31
+ ## 0.17.0 - 2026-10-09
32
+
33
+ - **[Antigravity] 缓存卡片改为只报「整段未命中」并给出它之前的空闲时长**
34
+ - **原来的口径把最有用的信息盖掉了。** 卡片报的是「相比上一次请求,三段前缀里哪一段变了」,而每次发新消息都必然改变 `contents`,于是它**每一次都显示「对话内容变化」**——既报了唯一注定要变的那一项,又把真正花钱的那件事藏了起来。这个口径是我上一版写的,属于设计失误。
35
+ - **现在只在整段未命中时发布归因**(`cachedContentTokenCount === 0`,即整个前缀被重算)。部分命中是对话增长的正常状态,每次新回合都必然带入服务端没见过的 token,`freshTokens > 0` 本身不代表任何东西,因此不再上报。
36
+ - **归因改为「先暂存、后发布」**:请求发出前算出的变化只暂存(`pendingBySession`),等响应报回用量后才决定要不要发布。改动的代价是否为零,只有响应的用量能回答。
37
+ - **新增空闲时长**:记录每个会话上一次**发出请求**的时刻,未命中时一并报出间隔。服务端的缓存按空闲时间过期(实测在 76 秒仍命中、183 秒整段失效),而客户端无法读取那个阈值——这是唯一能在本地观测到它的读数。首次请求没有可比较的前值,因此不填该字段(填 0 会被读成「立刻返回」)。
38
+ - **契约与 UI 同步**:`AntigravityCacheStatsDto.lastDriftCause` → `lastMiss: { cause, idleMs?, at }`;卡片显示「最近一次整段未命中」+「未命中前空闲」,时长按 3m 3s / 45s 这样的人类单位渲染;中英文案与段落说明一并重写。
39
+ - 测试:缓存统计 19 → 22 例(新增「命中时不报」「未命中时发布归因」「记录空闲时长」「首次请求不填空闲」),卡片渲染新增 1 例(全部命中时**不出现**任何未命中行,防止退回「每次都有话说」)。既有断言中 3 处依赖旧口径的已按新契约更新。
40
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2956 passed / 7 skipped, 0 failed**(较基线增 4 例)。
41
+
42
+ - **[Antigravity] 让 Gemini 路线的请求前缀保持逐字节稳定,并给这条线路补上缓存观测**
43
+ - **Antigravity 用的是 Google 服务端隐式缓存,客户端没有任何开关可拨。** 二进制里 `cachedContents` / `ContextCacheConfig` / `ttl` 这些字段确实存在,但都属于它链进来的官方 Go SDK,不是它的调用面:它实际只 POST `/v1internal:streamGenerateContent`,那层消息里不含任何缓存控制字段。所以唯一能提高命中率的手段,就是**不修改**请求前缀——前缀一旦在某个位置被改写,从该位置往后全部失效。
44
+ - **Gemini 路线一直在把每个 SSE text delta 回放成独立 part。** 这条修复此前只做给了 Claude(见 `docs/antigravity-claude-cache-audit.md`:385 个 delta 回放成 385 个 part,命中率因此掉到 68.84%),Gemini 走的是同一份 delta 却漏了。现在抽出纯函数 `mergeAdjacentTextParts`,两条路线各自调用;Claude 专属的 thinking/signature 配对逻辑原地保留,只对 Claude 生效。合并只针对「仅含 `text` 一个键」的相邻 part,带 signature、`thought: true`、`functionCall` 的边界一律不跨越。
45
+ - **图片预算原本按存储字节度量,而缩放发生在预算之后**,导致一组图在缩放有机会生效之前就已经超限、最旧的图照样被替换成占位文本——图片数量仍然变化,前缀照旧被改写,缩放等于没接上。现在 `requestImageBytes` 度量的是**实际要发送的尺寸**(有缩放目标时取 `min(存储字节, 目标上限)`),这是 `host/common/request-images.ts` 已记录过的同一个陷阱。预算仍保持 Antigravity 自己的 12MB,**没有**借用 Kimi 的 1.5MB。
46
+ - **这条线路此前完全不可观测**:`request-diagnostics` 的指纹是进程内临时 HMAC、不落盘,而且只看顶层 `tools/system/messages`,看不到 Antigravity 嵌套的 `request.contents/systemInstruction/tools`。新增的 `cache-stats.ts` 按会话记录命中率,并对三段前缀各取一个稳定指纹,把冷却归因到 `contents` / `systemInstruction` / `tools` / `session-id` / `none`。哈希走键排序后的稳定序列化,否则两个结构相同、键序不同的请求会被报成假漂移。统计纯内存、按 `sessionId + accountId` 分作用域、256 作用域 LRU。
47
+ - **已知局限写进了文件头,而不是留在报告里**:前缀快照按 session 分区、token 总计按 session+account 分区,两者作用域不同,所以**会话中途 429 换号会被报成 `'none'`**——读者不能据此读成「客户端什么都没变」。账号是否真的分区服务端缓存始终是 **INFERRED, not measured**,也正因如此没有把 `account` 提升成漂移原因:在假设未验证前把它写进契约,等于把假设当事实陈述。`'session-id'` 那个枚举同理,文案用「会话标识变化」而不承诺缓存语义。
48
+ - **账号调度缺省改为 sticky**,且只改缺省:`parseAntigravityPoolData` 会把缺失值补成 `'sequential'` 并写回池文件,所以那里的解析器也必须一起改,否则 `routes.ts` 的回退分支是死代码。**已存盘的显式选择继续生效**,不为存量用户做迁移——这条改动的前提(账号=缓存命名空间)尚未验证,不适合在未证实收益的情况下改变既有行为。共享 core `host/common/account-pool.ts` 的策略语义未动,其他线路不受影响。
49
+ - 设置页新增缓存卡片(命中率、累计 cached/fresh、最近一次漂移原因),中英文案齐备;未测得用量时显示为空而不是画一个 0% 的假仪表。
50
+ - 验证:`npm run typecheck` 0 错误;全量 `npm test` **2952 passed / 7 skipped, 0 failed**。新用例在改动前的实现上实测 **12 failed**(用 `git stash` 单独暂存 `mapper.ts` 复现),改后全过。
51
+ - **本次没有证明命中率提升**:Google 的隐式缓存是服务端的,本机无法做 A/B,`readImageRequest` 的真实缩放产物在测试里全是 mock。改动依据是「前缀逐字节稳定」这一机制,与 68.84% 那次的成因同构,但没有可报的线上数字。
52
+
5
53
  ## 0.16.0 - 2026-10-09
6
54
 
7
55
  - **[WorkBuddy] 模型行加上消耗倍率提示**
@@ -135,7 +183,6 @@
135
183
  - 验证:`tsc -b --force` 与 `tsc -p test/tsconfig.json` 通过;`vitest run` 2555 passed / 7 skipped;
136
184
  `npm run build` 通过。
137
185
 
138
-
139
186
  - **[MiniMax Code] 支持视频输入;把视频子系统提取为两条线路共享**
140
187
  - **视频能力(N6)**:M3 与 M3.1 现在声明并支持视频输入,此前只有文档记载、路由没有实现。
141
188
  块形状与 base64 取向**均为实测**,未沿用 Kimi 线的猜测:
package/README.md CHANGED
@@ -117,7 +117,7 @@
117
117
  - 工具调用 id 截断到 64 字符;
118
118
  - `stop` 按上限裁剪为最多 5 条、每条 ≤32 字节,超长整条丢弃(截断的停止串会在错误位置终止生成);
119
119
  - **K3 的长思考不会被截断**:输出上限跟随上下文窗口(保留 4096 余量),因为 `reasoning_content` 计入输出,固定 32K 会把 `max` 档的长推理中途截断并返回 `length`;调用方已知 prompt 规模时上限会被下调到放得下,未知时不做猜测;
120
- - **请求体超 2 MB 本地即拒绝**:该端点最常见的 400 是 `total message size N exceeds limit 2097152`,官方文案不给建议,这里直接按真实序列化体积拦截并提示压缩会话或检查大工具结果;
120
+ - **请求体超 2 MB 本地即拒绝**:该端点最常见的 400 是 `total message size N exceeds limit 2097152`,官方文案不给建议,这里直接按真实序列化体积拦截,并按「循环实际做了什么」给出处方(有图可丢但丢了也补不上缺口 / 图已丢完仍超限 / 本来无图,三种情况文案不同,不再声称发生了没发生的省略);确有实测上限的部署(前置代理/网关)可设 `DSH_KIMI_CODE_MAX_BODY_BYTES` 指定该档上限,取值非正整数时**回落到默认值而不是关闭守卫**;带视频的请求走 64 MiB 档,不受该变量影响;
121
121
  - **缓存是自动的,且无法手动干预**:
122
122
  - Kimi 按请求内容哈希命中前缀缓存。实测 `prompt_cache_key` 与 Anthropic `cache_control` 标记**均被忽略**(设与不设、同 key 与异 key 命中的是同一缓存),设备 id 与协议切换也不影响;
123
123
  - TTL 实测在 300–1800 秒之间,按 256 token 对齐;`/messages` 与 `/chat/completions` **共享同一缓存**;
@@ -127,7 +127,8 @@
127
127
  - 档位在**首次写入时锁定**,之后无法改写、命中时按原档免费续期;1 小时写入约为 5 分钟的两倍价,只有同一前缀会在一小时内被反复读取才划算。不选择时不发任何缓存字段;
128
128
  - **视频输入可用**(`k3`、`kimi-for-coding`):
129
129
  - DSH 的模态词表只有 `text`/`image`,但它是可合并扩展的接口,本插件用 TypeScript 模块增强把它扩到 `video`(**未改动 DSH 任何代码**),因此视频走 DSH 真实的能力通道,而不是只能显示在提示里;
130
- - 适配器把视频映射为服务文档的 `{type:'video_url',video_url:{url:'data:…'}}`。视频与图片**各有独立预算**(图片 2 MB、视频 48 MiB base64,最旧优先省略),请求体校验只在确实带视频时才放宽到 64 MiB;
130
+ - 适配器把视频映射为服务文档的 `{type:'video_url',video_url:{url:'data:…'}}`。视频与图片**各有独立预算**(图片 1.5 MB、视频 48 MiB base64,最旧优先省略),请求体校验只在确实带视频时才放宽到 64 MiB;
131
+ - 图片预算可用 `DSH_KIMI_CODE_MAX_IMAGE_BYTES` 覆盖(取值非正整数时回落到默认值)。视频预算**没有**环境变量出口,与 MiniMax 线路一致;
131
132
  - 以下情况会把视频降级为明确的文字说明而不是猜字段发出去:`k3-256k` 只接受图片、文档白名单外的容器、以及未文档化视频内容块的 **Anthropic 线路**;
132
133
  - **`dynamically_loaded_tools`(仅 K3)已实现**:K3 接受**消息级工具声明**(`messages[].tools`),可在会话中途用「无 `content` 字段的 system 消息」注入完整工具定义。官方把「保持顶层 `tools` 字节稳定」列为该特性的目的之一——中途修改/删除已发出的声明会使缓存从该点起失效,而在末尾追加不影响已缓存前缀,所以这是提升缓存命中的正道。声明按请求重发(服务端不保留),且仅在模型声明该能力时发送;
133
134
  - 额度卡片区分 **5 小时 / 7 天 / 月度(会员共享池)/ 月度(Kimi Code 池)** 四个窗口并显示重置时间,另可显示加油包余额;设置页为「设置 → 订阅服务 → Kimi Code」标签页,对话输入框右侧有该线路的额度胶囊。
package/lib/client.js CHANGED
@@ -2510,6 +2510,21 @@ window.__ModuleLoader__.load({
2510
2510
  save: "保存",
2511
2511
  saved: "已保存",
2512
2512
  saving: "保存中...",
2513
+ cacheSection: "缓存",
2514
+ cacheDesc: "Antigravity 的缓存由服务端隐式命中,客户端不发送任何缓存控制字段,因此这里用实际读缓存的比例反映效果。下面只报「整段未命中」的那一次,并给出它之前的空闲时长——缓存是按空闲时间过期的,而每次新对话都必然改变内容,把每次变化都报出来等于什么都没说。统计只保存在内存中,并按会话隔离。",
2515
+ cacheEmpty: "本进程还没有带用量回报的请求。",
2516
+ cacheHitRatio: "缓存命中率",
2517
+ cacheCached: "命中 tokens",
2518
+ cacheFresh: "新增 tokens",
2519
+ cacheRequests: "请求数",
2520
+ cacheMiss: "最近一次整段未命中",
2521
+ cacheMissIdle: "未命中前空闲",
2522
+ cacheDriftNone: "前缀未变化",
2523
+ cacheDriftContents: "对话内容变化",
2524
+ cacheDriftSystemInstruction: "系统指令变化",
2525
+ cacheDriftTools: "工具列表变化",
2526
+ cacheDriftSessionId: "会话标识变化",
2527
+ cacheDriftNewSession: "本次会话的首次请求",
2513
2528
  quotaSection: "用量与配额",
2514
2529
  quotaDesc: "数据来自 Google Cloud Code Assist 配额服务,页面可见时最多每 60 秒刷新一次。",
2515
2530
  refreshQuota: "刷新用量",
@@ -2573,6 +2588,21 @@ window.__ModuleLoader__.load({
2573
2588
  save: "Save",
2574
2589
  saved: "Saved",
2575
2590
  saving: "Saving...",
2591
+ cacheSection: "Cache",
2592
+ cacheDesc: "Antigravity's cache is matched implicitly by the service — the client sends no cache-control field — so the served-from-cache share is the only evidence it is working. Only a total miss is reported below, beside how long the client had been idle: the cache expires on idle time, and every new turn changes the conversation, so reporting each change would report the same thing every time. The totals are in-memory and scoped per session.",
2593
+ cacheEmpty: "No request in this process has reported usage yet.",
2594
+ cacheHitRatio: "Cache hit ratio",
2595
+ cacheCached: "Cached tokens",
2596
+ cacheFresh: "Fresh tokens",
2597
+ cacheRequests: "Requests",
2598
+ cacheMiss: "Last total miss",
2599
+ cacheMissIdle: "Idle before that miss",
2600
+ cacheDriftNone: "Prefix unchanged",
2601
+ cacheDriftContents: "Conversation contents changed",
2602
+ cacheDriftSystemInstruction: "System instruction changed",
2603
+ cacheDriftTools: "Tool list changed",
2604
+ cacheDriftSessionId: "Session identifier changed",
2605
+ cacheDriftNewSession: "This session's first request",
2576
2606
  quotaSection: "Usage & Quota",
2577
2607
  quotaDesc: "Data from Google Cloud Code Assist quota service; refreshes up to once every 60 seconds when visible.",
2578
2608
  refreshQuota: "Refresh quota",
@@ -2599,6 +2629,26 @@ window.__ModuleLoader__.load({
2599
2629
  //#endregion
2600
2630
  //#region src/client/antigravity/AntigravitySection.tsx
2601
2631
  const api$11 = createLineApi("/antigravity/api", "Antigravity");
2632
+ /** A gap in human units, so the idle reading is legible without arithmetic. */
2633
+ function idleLabel(ms) {
2634
+ const seconds = Math.round(ms / 1e3);
2635
+ if (seconds < 60) return `${seconds}s`;
2636
+ const minutes = Math.floor(seconds / 60);
2637
+ const rest = seconds % 60;
2638
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
2639
+ }
2640
+ /** Human label for the cause of the last request that lost the whole cache. */
2641
+ function missLabel(cause, t) {
2642
+ switch (cause) {
2643
+ case "none": return t.cacheDriftNone;
2644
+ case "contents": return t.cacheDriftContents;
2645
+ case "systemInstruction": return t.cacheDriftSystemInstruction;
2646
+ case "tools": return t.cacheDriftTools;
2647
+ case "session-id": return t.cacheDriftSessionId;
2648
+ case "new-session": return t.cacheDriftNewSession;
2649
+ default: return String(cause);
2650
+ }
2651
+ }
2602
2652
  function AntigravitySection({ onModelChange, loadModelDirectory }) {
2603
2653
  const [status, setStatus] = (0, react.useState)(null);
2604
2654
  const [loading, setLoading] = (0, react.useState)(true);
@@ -2903,7 +2953,7 @@ window.__ModuleLoader__.load({
2903
2953
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPoolSection, {
2904
2954
  accounts: status?.accounts ?? [],
2905
2955
  activeAccountId: status?.activeAccountId,
2906
- rotationStrategy: status?.rotationStrategy ?? "sequential",
2956
+ rotationStrategy: status?.rotationStrategy ?? "sticky",
2907
2957
  busy,
2908
2958
  labels: t,
2909
2959
  loginBusyLabel: busy === "login" ? loginProgress || t.signingIn : void 0,
@@ -3027,6 +3077,76 @@ window.__ModuleLoader__.load({
3027
3077
  })
3028
3078
  ]
3029
3079
  }),
3080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3081
+ className: "dsha-group",
3082
+ children: [
3083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3084
+ className: "dsha-grouphead",
3085
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t.cacheSection })
3086
+ }),
3087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3088
+ className: "dsha-muted",
3089
+ children: t.cacheDesc
3090
+ }),
3091
+ status?.cache == null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3092
+ className: "dsha-empty",
3093
+ children: t.cacheEmpty
3094
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3095
+ className: "dsha-quota-card",
3096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3097
+ className: "dsha-meter-wrap",
3098
+ children: [
3099
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3100
+ className: "dsha-meter-label",
3101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t.cacheHitRatio }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: status.cache.hitRatio === null ? "—" : `${(status.cache.hitRatio * 100).toFixed(1)}%` })]
3102
+ }),
3103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3104
+ className: `dsha-meter ${(status.cache.hitRatio ?? 0) >= .9 ? "dsha-meter-green" : "dsha-meter-cyan"}`,
3105
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: `${Math.round((status.cache.hitRatio ?? 0) * 100)}%` } })
3106
+ }),
3107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3108
+ className: "dsha-meter-meta",
3109
+ children: [
3110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3111
+ t.cacheCached,
3112
+ ": ",
3113
+ status.cache.cachedTokens.toLocaleString()
3114
+ ] }),
3115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3116
+ t.cacheFresh,
3117
+ ": ",
3118
+ status.cache.freshTokens.toLocaleString()
3119
+ ] }),
3120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3121
+ t.cacheRequests,
3122
+ ": ",
3123
+ status.cache.requests
3124
+ ] })
3125
+ ]
3126
+ })
3127
+ ]
3128
+ }), status.cache.lastMiss !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3129
+ className: "dsha-row",
3130
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3131
+ className: "dsha-label",
3132
+ children: t.cacheMiss
3133
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3134
+ className: "dsha-value",
3135
+ children: missLabel(status.cache.lastMiss.cause, t)
3136
+ })]
3137
+ }), status.cache.lastMiss.idleMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3138
+ className: "dsha-row",
3139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3140
+ className: "dsha-label",
3141
+ children: t.cacheMissIdle
3142
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3143
+ className: "dsha-value",
3144
+ children: idleLabel(status.cache.lastMiss.idleMs)
3145
+ })]
3146
+ })] })]
3147
+ })
3148
+ ]
3149
+ }),
3030
3150
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3031
3151
  className: "dsha-group",
3032
3152
  children: [