dsh-connect-agnes-token-plan 0.9.0 → 0.10.0

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  本文件只记**公开行为变化**(新增能力、破坏性改动、重要修复)。实现细节、重构与测试加固请直接看 `git log`。
4
4
 
5
+ ## [0.10.0] — 2026-10-04
6
+
7
+ ### AgnesCode 目录改读 `/v2/models`:模型名带上平台真实的积分倍率与促销标
8
+
9
+ - **目录端点从 `/v1/models` 换到 `{origin}/v2/models`**:两者是同一 host 上的**两条不同目录**,不是同一份数据——v1 只有 8 行且**不带任何按模型价格**,v2 有 9 行且其中 8 行带平台自己的 `points_cost_multiplier`。桌面端一直读 v2,这就是它的选择器能显示 `1.20x` / `1.85x` 而本插件此前什么都不显示的原因。此前的结论「AgnesCode 不存在按模型倍率」**是错的**,根因是端点选错(详见 `docs/ROADMAP.md` §6.3.1「倍率字段正记」)。
10
+ - **模型显示名带上倍率 `Name · xNN.NN`**:pi-ai 的 descriptor 没有承载计费的字段(`cost` 是每 token 美元,本网关是积分制),所以倍率走**显示名**——这是 DSH 选择器唯一会渲染的表面。拼写沿用兄弟插件 `dsh-connect-sensenova-token-plan` 与 WorkBuddy 自有选择器既有的 `· x` 形态,**不引入第二套约定**。
11
+ - **`0` 是已公布的价格(免费),不是缺失读数**:`×0.00` 与「未声明」是两件事,前者照常显示、后者才不画。为此新增 `numZeroOk()`——仓库既有的 `num()` / `numOrNull()` 都以 `> 0` 为准,直接复用会把「免费」静默变成「没读到」。
12
+ - **面板花名册新增促销标芯片**(`display_label`,如 `限时七折`):用桌面端同款蓝底配色,与中性灰底的「会员」徽章刻意区分——一个是**平台定价事实**,一个是**账号状态**。平台在**每一行**都放这个键、未促销的值为空串,因此判据是「非空才画」,而不是「键存在就画」。
13
+ - **`auto` 行排除**:v2 多出的 `auto` 是路由别名而非模型(无价格、无厂商),按 id 显式排除;判据不取「没有倍率」,以免平台给它定价后它悄悄回到花名册。
14
+ - **会员门槛字段随端点迁移**(`is_member_only` 布尔 → `allowed_subscription` 列表):映射规则为「列表非空即会员」,已对 v1 逐行核对 **8/8 一致**,属**重新编码**而非重新解释。
15
+ - **修:改价不触发重注册**(`catalogSignature` 现纳入倍率)。签名原先只编码 `id:vision`,而倍率已进显示名——`glm-5.2` 从 `1.85` 改到 `9.99` 时签名**完全相同**,选择器会一直显示旧倍率。平台在促销边界上确实会改价(`glm-5.2` 当前带 `限时七折`),所以这是会真实发生的路径。**操作者手填的伪倍率仍不触发重注册**(它在签名之后才挂上),原裁定不变。
16
+ - **修:AgnesCode tab 在文档隐藏时停止轮询**(与 `PanelPage` 对齐),不再在后台空转。
17
+ - **修:`render.test.mjs` 的午夜 flake**:`now + 1h` 在 23:00 之后跨到次日,而断言仍要求「同日」的裸时钟——每天 23:00–00:00 之间该门禁固定转红 1 项。改为按构造恒为当天的时刻。详见 `docs/PITFALLS.md` §48(与 §41「护栏的存活守卫自己漏了平台假设」同族,这次漏的是**时间**假设)。
18
+ - **文档**:`docs/AGNES-API.md` §7.3 记入「单次 chat 请求图像上限 4 张」的 400 实测(官方文档未公示该上限);`docs/PITFALLS.md` 新增 §48(午夜 flake)与 §49(`num()` 把「免费」塌成「未声明」)。
19
+ - 影响面:`src/host/{agnescode.ts, agnescode-models.ts, provider-publish.ts, util.ts, routes/agnescode.ts}`、`src/client/{agnescode-tab.ts, wire.ts, styles.ts, i18n.ts}`、`test/{agnescode, publish-core, render, live-agnescode}`、`docs/{ROADMAP, AGNES-API, PITFALLS, TESTING}`。
20
+
5
21
  ## [0.9.0] — 2026-10-03
6
22
 
7
23
  ### UI:开关统一为项目品牌色,积分额度拆成三张卡片
package/README.md CHANGED
@@ -1,77 +1,89 @@
1
1
  # dsh-connect-agnes-token-plan
2
2
 
3
- Agnes 接入的 DSH **Plugins 页**插件卡**全家桶**(三个 tab):① **积分额度** tab——登录一次 Agnes 控制台,实时查看四类额度窗口与账号累计用量,令牌失效自动重登,之后无需再管;② **接入 API** tab——把 Agnes 模型注册为 DSH provider,参与对话与出图;③ **AgnesCode** tab(可选、默认关)——读取本机 AgnesCode 桌面端登录态接入桌面端上游。两条上游凭据相互独立。此外 429 自愈在后台生效:限频被误判为"额度耗尽"时在 Host 侧纠正回退避重试,模型不会无端消失。
3
+ 把 Agnes 接进 DSH 的 **Plugins 页**插件卡。**DSH** 即 **DeepSeek Harness**;**Agnes** 是商汤科技的模型平台(本插件对接其控制台 API 与推理 API)。
4
4
 
5
- ## 功能
5
+ > **第一次读这个仓库?先看这三句。**
6
+ >
7
+ > 1. **这是给 DeepSeek Harness 用的插件**,不是独立应用;所有入口都在 DSH 的 Plugins 页那张卡上(三个 tab,见下)。
8
+ > 2. **三种「红」是正常的,不是回归**:① `store/routes/wiring.test.mjs` 等 peer 套件报 `cannot resolve the peer dependency` —— 那是 DSH runtime 自带的包不在本仓库,属环境问题,先查 `test/peer-roots.mjs`;② 没装 `dsh` CLI 时 e2e 会 SKIP,属预期;③ 若在 **23:00–00:00** 之间单独跑 `render.test.mjs` 报红,那是历史上的午夜窗口缺陷(已修,见 [docs/PITFALLS.md](docs/PITFALLS.md) §48),升级到含该修复的版本即消失。
9
+ > 3. **见到「奇怪」的写法先别定罪**——本仓多数刻意的取舍都在 [docs/PITFALLS.md](docs/PITFALLS.md) / [docs/ADR.md](docs/ADR.md) 有明文出处。**动手前先 grep 一遍 docs/**。
10
+ >
11
+ > 协作会话的完整纪律见 [AGENTS.md](AGENTS.md)。
6
12
 
7
- Agnes 的 Token Plan **按窗口限流**,不是积分余额——账号级四个维度各有上限,用超了只能等窗口滚过:
13
+ - **积分额度**——登录一次,实时查看积分余额、额度窗口与每模型消耗,令牌自动续期,之后无需再管;
14
+ - **接入 API**——把爱思模型注册为 DSH provider,参与对话、出图与出视频;
15
+ - **Agnescode**——上游限流较宽松的另一条爱思办公产品线,独立账号、独立积分。
8
16
 
9
- | 维度 | 上限字段 | 窗口 |
10
- |---|---|---|
11
- | 模型请求 | `concurrency_limit` | `concurrency_window_h`(各档均为 5 小时) |
12
- | 每周请求 | `text_weekly_limit` | 168 小时 |
13
- | 生图 | `image_daily_limit` | 24 小时 |
14
- | 视频 | `video_daily_limit` | 24 小时 |
17
+ 面板分三个 tab,按「先看数、再接入、最后可选加桌面端上游」排序:
15
18
 
16
- 这个面板把数字搬进 DSH 的 Plugins 页,写代码时不用切网页就能盯住:
19
+ ## 积分额度
17
20
 
18
- - **额度窗口**:四个窗口的上限、周期与**平台报出的已用量**(`used / limit` 与百分比)。面板直接引用控制台自己的「当前用量」,进度条是逐字转写、不做减法;`usage_pct` 也是平台算好的
21
+ 将 Agnes 的 Token Plan 接入dsh,,用超了只能等窗口重置。这个 tab 把数字搬进 DSH,写代码时不用切网页就能盯住:
22
+
23
+ - **额度窗口**:模型请求(5 小时滚动)、每周请求、生图与视频(各 24 小时),**平台报出已用量**、独立滚动,(含百分比与重置时间)
19
24
  - **账号累计用量**:控制台口径的累计请求 / 文本 Token / 生图 / 视频秒数 / 活跃天数,以及近 N 天的分桶柱图
20
- - **套餐对比**:平台**公开**的套餐目录(六档:入门版 / 专业版 / 高级版 × 月付 / 年付),无需登录即可读,用来回答"升级能买到什么"
21
- - **模型清单**:当前 Key 实际能调哪些模型,其中哪些能看图(按平台 `input_modalities` 判定,不靠名字猜)
22
- - **可选接入**:把 Agnes 模型接进 DSH 对话,思考强度可选;并可注册出图 / 视频工具
23
- - **桌面端上游(默认关)**:面板「AgnesCode」tab 读取本机 AgnesCode 桌面端的登录态,独立 provider 与独立积分池
25
+ - **套餐对比**:平台**公开**的套餐目录(六档),无需登录即可读,用来回答"升级能买到什么"
24
26
 
25
- 面板**不代你操作账务**:不改套餐、不代扣额度、不碰 Key 明文;数据来自 Agnes 控制台自己的 API,与网页控制台口径一致。真正会「动」的四部分——注册 provider、挂出图工具、挂视频工具、接入 AgnesCode 桌面端上游——全部 opt-in 且**默认关闭**,不打开时插件退化为纯信息展示。
27
+ ![「积分额度」tab](assets/panel-credit.png)
26
28
 
27
- **注册 provider 时**(面板开关打开),插件把 Agnes 模型接进 DSH 的对话模型选择器,并在 Host 侧纠正 peer 对限频 429 的误判(peer 的 `isQuotaExceededError` 命中面过宽,任何带额度措辞的 429——如 `out of rate budget`——都会被抢判成「额度耗尽」而不重试)——限频真正退避重试,模型不再无端 "消失"。
29
+ 首次使用在「连接 Agnes 控制台」填一次账号和密码(Agnes 不发 refresh token,续期就是自动重登一次)。登录失败时表单会直接显示 Agnes 返回的原因;密码错误属于**凭据型拒绝**,面板**绝不自动重试**(Agnes 有失败次数锁定)。想清除账号,用面板底部的按钮。
28
30
 
29
- ## 安装
31
+ ## 接入 API(可选)
30
32
 
31
- 1. 在 DSH「插件」页搜索 `dsh-connect-agnes-token-plan` 点击安装,或运行:
33
+ 把 Agnes 模型注册为 DSH provider,参与对话与出图。
32
34
 
33
- ```powershell
34
- dsh plugin --profile web add dsh-connect-agnes-token-plan # Web 端
35
- dsh plugin --profile desktop add dsh-connect-agnes-token-plan # 桌面端
36
- ```
35
+ - **模型清单**:在「API Key」卡粘贴 Key 保存`AGNES_TOKEN_PLAN_API_KEY`,Host 以 `agnes-token-plan` 之名注册 OpenAI 兼容 provider,模型列表随 `/v1/models` 自动刷新;
36
+ - **语言模型**:显示当前 Key 实际能调哪些模型,其中哪些能看图(按平台 `input_modalities` 判定,不靠名字猜)。
37
+ - **出图工具**:Host 给 agent 注册 `agnes_draw_image` ,出图模型按 catalog 的 `output_modalities` 结构化判定。
38
+ - **视频工具**:Host 给 agent 注册 `agnes_video_generate`,按选中模型分派 V2.0 / 2.5 两套互斥参数。
39
+ - 开关与勾选都在面板热生效,无需重启。但工具的实际挂载 / 缺席发生在**下一次 Host 启动**(agent tools 没有 unregister 语义)。细节见 [docs/SETUP.md](docs/SETUP.md) §3、[docs/PROVIDER-HOT-RELOAD.md](docs/PROVIDER-HOT-RELOAD.md) 与 [docs/AGNES-API.md](docs/AGNES-API.md) §7.5。
37
40
 
38
- 2. **完全退出 DSH(含托盘)再启动**。
41
+ ![「接入 API」tab](assets/panel-API-provider.png)
39
42
 
40
- > 装到的是哪一版由源决定:安装器会询问 profile 配的 registry,**默认含 `registry.npmmirror.com`,刚发布的版本在镜像上可能延迟几分钟**,官方源立即可用;要确认拿到了哪一版,对照 `npm view dsh-connect-agnes-token-plan version --registry=https://registry.npmjs.org`。三种 target 形态(npm 包名 / git 地址 / 本地路径)见 [docs/SETUP.md](docs/SETUP.md)。
43
+ 注意:`sk-` 走 API 按量计费,`cpk-` 走订阅配额(本 tab 只读订阅配额),两类 Key 有独立限制池,需从官网了解实际限制。
41
44
 
42
- ## 快速开始
45
+ ## AgnesCode(可选,默认关)
43
46
 
44
- 1. 打开 DSH 的 **Plugins 页**,找到 `dsh-connect-agnes-token-plan` 的插件卡(面板是页内的内联卡片,**不在侧边栏**)。
45
- 2. 在「积分额度」tab 点「连接 Agnes 控制台」,填一次账号和密码,点登录。
46
- 3. 之后令牌失效会自动重登(Agnes 不发 refresh token,续期的唯一路径就是重登一次),无需再操作。
47
+ **读取 AgnesCode 桌面端** 留在本机的加密会话文件,获取登录态,再以 provider id `agnescode` 注册**独立** provider、独立积分池——两条上游凭据互不相通:
47
48
 
48
- 登录失败时表单会直接显示 Agnes 返回的原因;密码错误属于**凭据型拒绝**,面板**绝不自动重试**(Agnes 有失败次数锁定),只有你主动点「登录」才会再发一次。想清除账号,用面板底部的按钮。
49
49
 
50
- ## 它是怎么工作的
50
+ 1. **接口地址**:写在会话文件里、**按账号跟随**,钉死在 Agnes 域名族内,地址不对就拒绝使用;
51
+ 2. **显示口径**:积分是**订阅池**(时效 + 永久);模型清单带「会员」标记而不隐藏——会员门槛是账号状态,不是模型不存在;
52
+ 3. **约 28 天续期一次**:JWT 过期后没有自动路径,开一次桌面 App、再点「检测本机登录态」重新采集即可。
51
53
 
52
- 登录原理一句话:Host 在后台向 `{consoleBase}/api/user/login` 发**一次**账号密码 POST 换取 access token(Agnes 没有 OIDC 跳转、没有 refresh token),令牌过期或被拒时用同一路径**重登一次**;面板打开时才轮询一个只读本地路由,关掉即停。协议细节(一跳登录、密码明文过 TLS 与「不落盘」纪律、防锁号节流)见 [docs/AUTH.md](docs/AUTH.md),接口契约见 [docs/AGNES-API.md](docs/AGNES-API.md)。
54
+ 协议探针记录见 [docs/ROADMAP.md](docs/ROADMAP.md) §6.3。
53
55
 
54
- **窗口「已用」是平台报出的,面板不计算**:`/api/cn/user/subscription` 直接返回每个窗口的 `used` / `limit` / `reset_at` / `usage_pct`(控制台「当前用量」那一屏的数据源),面板逐字转写并渲染进度条。但 `overview` / `series` 是**账号累计**口径,覆盖的是一段更长的时间——**不能用它去减窗口上限**,那个「剩余」跨周期、没人能担保,所以面板把「窗口用量」与「账号累计」作为两个独立事实并排显示,并明说不可相减。
56
+ ![AgnesCode tab](assets/panel-AgnesCode.png)
55
57
 
56
- ## 把 Agnes 模型接进 DSH(可选)
58
+ 注意:检测不到登录态时,面板会逐条列出探测过哪些文件、各自为什么没成(「没装 App」「解不开密」「会话里没有令牌」是三种不同的处理);
57
59
 
58
- 「接入 API」tab 的三张卡按"你为什么来这"排序,而不是按依赖排序:**语言模型**(注册 provider + 勾选推送哪些模型)、**出图工具**——两张都在最前且默认展开;**API Key** 收在最后(默认收起),它是前两张卡的前置条件,由它们指回来。
60
+ ## 边界
59
61
 
60
- 在「API Key」卡里粘贴 API Key 保存(免费版 `sk-` 或 Token Plan `cpk-` 皆可):Host 即以 `agnes-token-plan` 之名注册 OpenAI 兼容 provider,模型列表随 `/v1/models` 自动刷新,还能在「语言模型」卡勾选具体要推送哪些模型。Key 只进 DSH 凭据(引用名 `AGNES_TOKEN_PLAN_API_KEY`)、面板永不回显。开关与勾选都在面板热生效,无需重启。细节见 [docs/SETUP.md](docs/SETUP.md) §3 与 [docs/PROVIDER-HOT-RELOAD.md](docs/PROVIDER-HOT-RELOAD.md)。
62
+ - 面板**不代你操作账务**——不改套餐、不代扣额度、不碰 Key 明文;数据来自 Agnes 控制台自己的 API,与网页控制台口径一致。
63
+ - 注册 provider、挂出图工具、挂视频工具、接桌面端上游,全部 opt-in 且**默认关闭**;一个都不打开时,插件退化为纯信息展示。
61
64
 
62
- > **免费版与付费 Token Plan 共用同一个 API Key 输入框**:`sk-`(免费版)与 `cpk-`(Token Plan)两类 Key 都走同一份 `/v1/models` 目录,本插件对前缀不做限制。**计费双轨**——`sk-` 走 API 按 token / 张 / 秒计费(`agnes-2.5-flash` / `agnes-3.0-flash` / 图片模型当前优惠价 `$0`,`agnes-2.5-pro` 按刊例价),`cpk-` 走 Token Plan 订阅配额(四窗口)。面板「积分额度」tab 只读**订阅配额**,不读 API 按量余额;两类 Key 使用**独立限制池**,换 Key 即换池。两者都能在此粘贴。
65
+ ## 安装
63
66
 
64
- ## 出图工具(可选,默认关)
67
+ 1. 在 DSH「插件」页搜索 `dsh-connect-agnes-token-plan` 点击安装,或运行:
68
+
69
+ ```powershell
70
+ dsh plugin --profile web add dsh-connect-agnes-token-plan # Web 端
71
+ dsh plugin --profile desktop add dsh-connect-agnes-token-plan # 桌面端
72
+ ```
65
73
 
66
- 面板「出图工具」卡(在「语言模型」下方,默认展开)打开开关后,Host 给 agent 注册工具 `agnes_draw_image`(首选模型由 `drawModelId` 指定),鉴权走同一把 `AGNES_TOKEN_PLAN_API_KEY`。出图模型优先按 catalog 的 `output_modalities` 结构化判定,该字段在 Agnes 目录上**缺失**时退回模型名里的 `image` 段(名字兜底才是实际命中的路径)。注意:工具的实际挂载 / 缺席发生在**下一次 Host 启动**(agent tools 没有 unregister 语义),开关值本身立即生效。
74
+ 2. **完全退出 DSH(含托盘)再启动**。
67
75
 
68
- ## 视频工具(可选,默认关)
76
+ 3. 打开 DSH 的 **Plugins 页**,找到 `dsh-connect-agnes-token-plan` 的插件卡(面板是页内的内联卡片,**不在侧边栏**)。
69
77
 
70
- 面板「视频工具」卡(在「出图工具」下方,默认展开)打开开关后,Host 给 agent 注册工具 `agnes_video_generate`(首选模型由 `videoModelId` 指定),鉴权同样走 `AGNES_TOKEN_PLAN_API_KEY`。与出图**不同**的是协议:图片一次请求同步返回,视频是**异步任务制**——建任务后轮询到完成。**V2.0 与 2.5 两个参数体系都覆盖**,工具按选中模型分派请求体:V2.0(`agnes-video-v2.0`)走 `width`/`height`/`num_frames`/`frame_rate`,2.5(`agnes-video-2.5` / `agnes-video-2.5-flash`)走 `mode`/`seconds`/`size`/`aspect_ratio`。两套字段互斥、永不同时发给同一模型,但处理是**不对称**的:V2.0 帧数字段发到 2.5 模型会被换算成最接近的整秒(`121 @ 24fps → 5s`),保留「不判断家族也能出片」的退路;反过来,2.5 专有字段发到 V2.0 模型会**当场报错并给出修法**——工具不会静默丢弃,丢弃等于让你以为拿到 10 秒 2K、实际拿到 5 秒 720P。自动选择优先 V2.0,目录里没有 V2.0 时回落第一个 2.5 模型;面板仍会把 2.5 家族单独点名。协议细节与校验规则见 [docs/AGNES-API.md](docs/AGNES-API.md) §7.5。
78
+ > 装到的是哪一版由源决定:安装器会询问 profile 配的 registry,**默认含 `registry.npmmirror.com`,刚发布的版本在镜像上可能延迟几分钟**,官方源立即可用;要确认拿到了哪一版,对照 `npm view dsh-connect-agnes-token-plan version --registry=https://registry.npmjs.org`。三种 target 形态(npm 包名 / git 地址 / 本地路径)见 [docs/SETUP.md](docs/SETUP.md)。
79
+
80
+ ## 它是怎么工作的
71
81
 
72
- ## 桌面端上游:AgnesCode(可选,默认关)
82
+ 登录原理:
83
+ - Host 在后台向 `{consoleBase}/api/user/login` 发**一次**账号密码 POST 换取 access token(Agnes 没有 OIDC 跳转、没有 refresh token),令牌过期或被拒时用同一路径**重登一次**;
84
+ - `/api/cn/user/subscription` 直接返回每个窗口的 `used` / `limit` / `reset_at` / `usage_pct`(控制台「当前用量」那一屏的数据源),面板逐字转写、不做减法。下方的「账号累计用量」统计同理,
85
+ - 面板打开时才轮询一个只读本地路由,关掉即停。协议细节(一跳登录、密码明文过 TLS 与「不落盘」纪律、防锁号节流)见 [docs/AUTH.md](docs/AUTH.md),接口契约见 [docs/AGNES-API.md](docs/AGNES-API.md)。
73
86
 
74
- 面板「AgnesCode」tab 接的是 **AgnesCode 桌面端**的登录态:微信扫码发生在桌面 App 里,本插件只**读取** App 留在本机的加密会话文件(Chromium os_crypt,密钥经系统 DPAPI 解封,全程内存使用、不落盘不显示;解出的凭据随后存入 DSH 凭据服务,与账号密码同一纪律),并以 provider id `agnescode`(显示名 AgnesCode)注册**独立** provider。它的接口地址写在会话文件里、**按账号跟随**(钉死在 Agnes 域名族内,地址不对就拒绝使用);显示的积分是**订阅池**口径(时效 + 永久),模型清单带「会员」标记(会员门槛是账号状态,不是模型不存在,所以标记而不隐藏)。检测不到登录态时,面板逐条列出**探测过哪些文件、各自为什么没成**——「没装 App」「解不开密」「会话里没有令牌」是三种不同的处理方式,不会笼统叫你重新登录。JWT 有效期约 28 天,过期后开一次桌面 App 再点「检测本机登录态」即可;协议探针记录见 [docs/ROADMAP.md](docs/ROADMAP.md) §6.3。
75
87
 
76
88
  ## 运维诊断:这台机器现在挂没挂 provider?
77
89
 
@@ -90,7 +102,7 @@ npm run doctor:json # 机器读:JSON(可进你的巡检 / 工单脚本
90
102
  - [docs/API.md](docs/API.md) — 路由与控制台端点
91
103
  - [docs/AGNES-API.md](docs/AGNES-API.md) — Agnes 接口全集(控制台额度侧 + 推理侧)
92
104
  - [docs/TESTING.md](docs/TESTING.md) — 测试体系
93
- - [docs/PITFALLS.md](docs/PITFALLS.md) — 真实踩坑(47 条)
105
+ - [docs/PITFALLS.md](docs/PITFALLS.md) — 真实踩坑(49 条)
94
106
  - [CHANGELOG.md](CHANGELOG.md) — 版本变化
95
107
 
96
108
  AI 协作会话请先读 [AGENTS.md](AGENTS.md)。
@@ -98,7 +110,6 @@ AI 协作会话请先读 [AGENTS.md](AGENTS.md)。
98
110
  ## 诚实声明
99
111
 
100
112
  - 面板显示的是**控制台自己的口径**,与网页控制台一致;`GET /v1/models` 只区分权限,不计费也不占推理额度;
101
- - 窗口卡片里的「已用」是控制台报出的「当前用量」,面板逐字转写、不做减法;下方「账号累计」覆盖的是另一段时间,不能与窗口上限相减;
102
113
  - 令牌被拒时面板明确提示重新登录,而不是静默显示旧数据;
103
114
  - 凭据(账号、access token)只经 DSH 凭据服务保存,**密码不落盘**;本插件不写任何明文凭据文件或调试日志。
104
115
 
Binary file
package/client.js CHANGED
@@ -38,6 +38,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
38
38
  "panel.refresh": "刷新",
39
39
  "panel.updated": "更新于 {time}",
40
40
  "panel.loading": "加载中…",
41
+ "panel.refreshing": "刷新中…",
41
42
  "panel.error": "读取失败:{error}",
42
43
  "panel.jwtMissing": "还没有配置控制台账号。",
43
44
  "panel.jwtExpired": "控制台令牌已失效。Agnes 不发放 refresh 令牌,Host 会用已保存的账号重新登录一次;若仍失败,请手动重新登录。",
@@ -88,6 +89,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
88
89
  "quota.cycle.yearly": "按年",
89
90
  "quota.unit.requests": "次",
90
91
  "quota.unit.images": "张",
92
+ "quota.unit.seconds": "秒",
91
93
  "quota.used": "已用",
92
94
  "quota.resetAt": "重置 {time}",
93
95
  "quota.resetCountdown": "约 {minutes} 分钟后重置",
@@ -102,7 +104,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
102
104
  "quota.total.requests": "模型请求",
103
105
  "quota.total.tokens": "文本 Token",
104
106
  "quota.total.images": "生图",
105
- "quota.total.video": "视频秒数",
107
+ "quota.total.video": "视频",
106
108
  "quota.total.activeDays": "活跃天数",
107
109
  "quota.usageMissing": "{label}:暂未读到。",
108
110
  "usage.none": "该区间内没有用量记录。",
@@ -225,6 +227,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
225
227
  "agnescode.balancePermanentLabel": "永久",
226
228
  "agnescode.models": "模型({count})",
227
229
  "agnescode.memberOnly": "会员",
230
+ "agnescode.rateTitle": "平台公布的积分消耗倍率(0 为免费)",
228
231
  "agnescode.registeredPill": "已注册",
229
232
  "agnescode.rosterHint": "勾选决定哪些模型推送进 DSH 模型列表,改动即时生效。",
230
233
  "agnescode.rosterSave": "保存",
@@ -256,6 +259,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
256
259
  "panel.refresh": "Refresh",
257
260
  "panel.updated": "Updated {time}",
258
261
  "panel.loading": "Loading…",
262
+ "panel.refreshing": "Refreshing…",
259
263
  "panel.error": "Could not read: {error}",
260
264
  "panel.jwtMissing": "No Agnes console account is configured yet.",
261
265
  "panel.jwtExpired": "The console token is no longer valid. Agnes issues no refresh token, so the Host signs in again with the saved account; if that also fails, sign in manually.",
@@ -306,6 +310,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
306
310
  "quota.cycle.yearly": "yearly",
307
311
  "quota.unit.requests": " times",
308
312
  "quota.unit.images": " images",
313
+ "quota.unit.seconds": " s",
309
314
  "quota.used": "Used",
310
315
  "quota.resetAt": "resets {time}",
311
316
  "quota.resetCountdown": "resets in ~{minutes} min",
@@ -320,7 +325,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
320
325
  "quota.total.requests": "Model requests",
321
326
  "quota.total.tokens": "Text tokens",
322
327
  "quota.total.images": "Images",
323
- "quota.total.video": "Video seconds",
328
+ "quota.total.video": "Video",
324
329
  "quota.total.activeDays": "Active days",
325
330
  "quota.usageMissing": "{label}: not read yet.",
326
331
  "usage.none": "No usage recorded in this range.",
@@ -443,6 +448,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
443
448
  "agnescode.balancePermanentLabel": "Permanent",
444
449
  "agnescode.models": "Models ({count})",
445
450
  "agnescode.memberOnly": "member",
451
+ "agnescode.rateTitle": "Credit multiplier as published by the platform (0 = free)",
446
452
  "agnescode.registeredPill": "Registered",
447
453
  "agnescode.rosterHint": "Ticking decides which models are pushed into DSH's model list; the change takes effect immediately.",
448
454
  "agnescode.rosterSave": "Save",
@@ -544,11 +550,14 @@ var dsh_connect_agnes_token_plan_client = (function() {
544
550
  * decimal 128 000. A flat /1000 rounding once printed "1049k" for the 1M
545
551
  * window and it read like a placeholder bug; so figures divisible by 1000 keep
546
552
  * the decimal reading they were written with, binary-only figures (262144 →
547
- * 256K, 65536 → 64K) get the binary one, and anything ≥ 1M goes to M.
553
+ * 256K, 65536 → 64K) get the binary one, anything ≥ 1M goes to M and anything
554
+ * ≥ 1G goes to G — the account-lifetime token total (billions) must not print
555
+ * as a four-digit "5214M".
548
556
  */
549
557
  function tokenSize(value) {
550
558
  const number = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : 0;
551
559
  if (number <= 0) return "";
560
+ if (number >= 1e9) return `${Math.round(number / 1e8) / 10}G`;
552
561
  if (number >= 1e6) return `${Math.round(number / 1e5) / 10}M`;
553
562
  if (number % 1e3 === 0) return `${number / 1e3}K`;
554
563
  if (number % 1024 === 0) return `${number / 1024}K`;
@@ -983,8 +992,8 @@ var dsh_connect_agnes_token_plan_client = (function() {
983
992
  color: "var(--dsw-alias-label-secondary)"
984
993
  },
985
994
  quotaRemaining: {
986
- fontSize: 16,
987
- lineHeight: "20px",
995
+ fontSize: 17,
996
+ lineHeight: "22px",
988
997
  fontWeight: 650,
989
998
  letterSpacing: "-0.02em",
990
999
  fontVariantNumeric: "tabular-nums"
@@ -1306,6 +1315,14 @@ var dsh_connect_agnes_token_plan_client = (function() {
1306
1315
  background: "var(--dsw-alias-bg-layer-2)",
1307
1316
  color: "var(--dsw-alias-label-secondary)"
1308
1317
  },
1318
+ modelPromo: {
1319
+ flex: "none",
1320
+ fontSize: 11,
1321
+ padding: "1px 7px",
1322
+ borderRadius: 999,
1323
+ background: "#D1DDFC",
1324
+ color: "#003DD9"
1325
+ },
1309
1326
  modelMeta: {
1310
1327
  paddingLeft: 25,
1311
1328
  fontSize: 11,
@@ -1323,6 +1340,12 @@ var dsh_connect_agnes_token_plan_client = (function() {
1323
1340
  borderRadius: 12,
1324
1341
  background: "var(--dsw-alias-bg-layer-1)",
1325
1342
  padding: "12px 14px"
1343
+ },
1344
+ diagRow: {
1345
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1346
+ fontSize: 11,
1347
+ lineHeight: "16px",
1348
+ marginBottom: 2
1326
1349
  }
1327
1350
  };
1328
1351
  }));
@@ -1904,6 +1927,64 @@ var dsh_connect_agnes_token_plan_client = (function() {
1904
1927
  ...labelStyle ?? {}
1905
1928
  } }, label));
1906
1929
  }
1930
+ /**
1931
+ * The drawn radio dot, same discipline as the pill switch above: a NATIVE
1932
+ * radio input (transparent but focusable — keyboard focus ring and screen
1933
+ * reader semantics stay free) over a drawn circle whose selected state is a
1934
+ * brand-colored ring + inner dot. The bare native circle read as unstyled
1935
+ * next to the drawn switch and checkboxes; this puts the three controls in
1936
+ * one visual language without reimplementing the widget.
1937
+ *
1938
+ * The drawn span is `pointerEvents: "none"` so clicks reach the input; the
1939
+ * input is on top so the browser's own focus ring draws around the dot.
1940
+ * Hook-free like `Switch` — the caller owns `checked`/`onChange`, and the
1941
+ * surrounding `<label>` (the roster row) is what makes it clickable.
1942
+ */
1943
+ function Radio({ checked, disabled, onChange, name }) {
1944
+ const size = 16;
1945
+ return h("span", { style: {
1946
+ position: "relative",
1947
+ flex: "none",
1948
+ width: size,
1949
+ height: size,
1950
+ display: "inline-block"
1951
+ } }, h("input", {
1952
+ type: "radio",
1953
+ name,
1954
+ checked,
1955
+ disabled,
1956
+ onChange,
1957
+ style: {
1958
+ appearance: "none",
1959
+ WebkitAppearance: "none",
1960
+ position: "absolute",
1961
+ inset: 0,
1962
+ width: size,
1963
+ height: size,
1964
+ margin: 0,
1965
+ background: "transparent",
1966
+ border: "none",
1967
+ cursor: disabled ? "default" : "pointer"
1968
+ }
1969
+ }), h("span", { style: {
1970
+ position: "absolute",
1971
+ inset: 0,
1972
+ borderRadius: "50%",
1973
+ boxSizing: "border-box",
1974
+ border: `1px solid ${checked ? "var(--agnes-brand, #1E40AF)" : "var(--dsw-alias-border-l3)"}`,
1975
+ background: "var(--dsw-alias-bg-layer-2)",
1976
+ pointerEvents: "none",
1977
+ transition: "border-color 120ms ease"
1978
+ } }, checked ? h("span", { style: {
1979
+ position: "absolute",
1980
+ left: 3,
1981
+ top: 3,
1982
+ width: 8,
1983
+ height: 8,
1984
+ borderRadius: "50%",
1985
+ background: "var(--agnes-brand, #1E40AF)"
1986
+ } }) : null));
1987
+ }
1907
1988
  var LABEL_STYLE, TRACK_STYLE, THUMB_COLOR;
1908
1989
  var init_switch = __esmMin((() => {
1909
1990
  init_runtime();
@@ -2160,13 +2241,11 @@ var dsh_connect_agnes_token_plan_client = (function() {
2160
2241
  flex: "1 1 auto",
2161
2242
  minWidth: 0,
2162
2243
  cursor: busy ? "default" : "pointer"
2163
- } }, h("input", {
2164
- type: "radio",
2244
+ } }, h(Radio, {
2165
2245
  name: radioName,
2166
2246
  checked: preferred === null && enabled,
2167
2247
  disabled: busy || !enabled,
2168
- onChange: () => void saveModel(null),
2169
- style: S.modelCheck
2248
+ onChange: () => void saveModel(null)
2170
2249
  }), h("span", { style: S.modelName }, k("autoOption"))), h("span", { style: S.modelBadge }, effective !== "" ? `${k("badge")} · ${effective}` : `${k("badge")} · ${k("badgeNone")}`))), ...candidates.map((id) => h("li", {
2171
2250
  style: enabled ? S.modelRow : {
2172
2251
  ...S.modelRow,
@@ -2180,13 +2259,11 @@ var dsh_connect_agnes_token_plan_client = (function() {
2180
2259
  flex: "1 1 auto",
2181
2260
  minWidth: 0,
2182
2261
  cursor: busy ? "default" : "pointer"
2183
- } }, h("input", {
2184
- type: "radio",
2262
+ } }, h(Radio, {
2185
2263
  name: radioName,
2186
2264
  checked: preferred === id && enabled,
2187
2265
  disabled: busy || !enabled,
2188
- onChange: () => void saveModel(id),
2189
- style: S.modelCheck
2266
+ onChange: () => void saveModel(id)
2190
2267
  }), h("span", {
2191
2268
  style: S.modelName,
2192
2269
  title: id
@@ -2648,27 +2725,27 @@ var dsh_connect_agnes_token_plan_client = (function() {
2648
2725
  {
2649
2726
  key: "requests",
2650
2727
  label: tt("quota.total.requests"),
2651
- value: source.totalRequests
2728
+ value: count(source.totalRequests)
2652
2729
  },
2653
2730
  {
2654
2731
  key: "tokens",
2655
2732
  label: tt("quota.total.tokens"),
2656
- value: source.totalTokens
2733
+ value: tokenSize(source.totalTokens) || count(source.totalTokens)
2657
2734
  },
2658
2735
  {
2659
2736
  key: "images",
2660
2737
  label: tt("quota.total.images"),
2661
- value: source.totalImages
2738
+ value: count(source.totalImages)
2662
2739
  },
2663
2740
  {
2664
2741
  key: "video",
2665
2742
  label: tt("quota.total.video"),
2666
- value: source.totalVideoSeconds
2743
+ value: `${count(source.totalVideoSeconds)} ${tt("quota.unit.seconds")}`
2667
2744
  },
2668
2745
  ...typeof source.activeDays === "number" ? [{
2669
2746
  key: "days",
2670
2747
  label: tt("quota.total.activeDays"),
2671
- value: source.activeDays
2748
+ value: count(source.activeDays)
2672
2749
  }] : []
2673
2750
  ];
2674
2751
  return h("div", null, h("div", { style: {
@@ -2678,7 +2755,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
2678
2755
  } }, label), h("div", { style: S.metricGrid }, cells.map((cell) => h("div", {
2679
2756
  key: cell.key,
2680
2757
  style: S.metric
2681
- }, h("span", { style: S.metricLabel }, cell.label), h("span", { style: S.metricValue }, count(cell.value))))));
2758
+ }, h("span", { style: S.metricLabel }, cell.label), h("span", { style: S.metricValue }, cell.value)))));
2682
2759
  }
2683
2760
  /**
2684
2761
  * Per-bucket consumption as a mini bar chart.
@@ -2871,15 +2948,36 @@ var dsh_connect_agnes_token_plan_client = (function() {
2871
2948
  }, []);
2872
2949
  useEffect(() => {
2873
2950
  alive.current = true;
2874
- load();
2875
- const timer = setInterval(() => {
2951
+ let timer = null;
2952
+ const run = () => {
2876
2953
  if (alive.current) load();
2877
- }, AGNESCODE_POLL_MS);
2954
+ };
2955
+ const start = () => {
2956
+ if (timer === null) timer = setInterval(run, AGNESCODE_POLL_MS);
2957
+ };
2958
+ const stop = () => {
2959
+ if (timer !== null) {
2960
+ clearInterval(timer);
2961
+ timer = null;
2962
+ }
2963
+ };
2964
+ load();
2965
+ start();
2966
+ const onVisibility = () => {
2967
+ if (!alive.current) return;
2968
+ if (document.visibilityState === "hidden") stop();
2969
+ else {
2970
+ run();
2971
+ start();
2972
+ }
2973
+ };
2974
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") document.addEventListener("visibilitychange", onVisibility);
2878
2975
  return () => {
2879
2976
  alive.current = false;
2880
2977
  generation.current += 1;
2881
2978
  inFlight.current?.abort?.();
2882
- clearInterval(timer);
2979
+ stop();
2980
+ if (typeof document !== "undefined" && typeof document.removeEventListener === "function") document.removeEventListener("visibilitychange", onVisibility);
2883
2981
  };
2884
2982
  }, [load]);
2885
2983
  useEffect(() => {
@@ -3023,8 +3121,8 @@ var dsh_connect_agnes_token_plan_client = (function() {
3023
3121
  }, attempts.map((attempt, index) => h("div", {
3024
3122
  key: `${String(attempt?.file ?? index)}-${index}`,
3025
3123
  role: "listitem",
3026
- style: { marginBottom: 2 }
3027
- }, h("span", { style: { color: attempt?.tier === "ok" ? "inherit" : "var(--dsw-alias-label-secondary)" } }, `[${tt(`agnescode.tier.${attempt?.tier}`)}] ${String(attempt?.file ?? "")}${attempt?.detail ? ` — ${attempt.detail}` : ""}`)))) : null, note !== null ? h("div", {
3124
+ style: S.diagRow
3125
+ }, h("span", { style: { color: attempt?.tier === "ok" ? "inherit" : "var(--dsw-alias-state-warn-primary)" } }, `[${tt(`agnescode.tier.${attempt?.tier}`)}] ${String(attempt?.file ?? "")}${attempt?.detail ? ` — ${attempt.detail}` : ""}`)))) : null, note !== null ? h("div", {
3028
3126
  style: {
3029
3127
  ...S.formNote,
3030
3128
  fontSize: 12,
@@ -3092,11 +3190,13 @@ var dsh_connect_agnes_token_plan_client = (function() {
3092
3190
  * the unlinked frame — a roster inlined there is unassertable. The row is the
3093
3191
  * SAME two-line shape as the sibling rosters (head line over an indented
3094
3192
  * parameter line), sharing `S.modelRow`'s contract — and, since the panel can
3095
- * now curate this provider, the SAME checkbox. What it carries instead
3096
- * of a rate chip is the `memberOnly` badge — the platform-declared
3097
- * gating fact this provider HAS; no multiplier chip exists here because the
3098
- * upstream declares no per-model rate (billing is the credit pool), and
3099
- * inventing one would libel the roster.
3193
+ * now curate this provider, the SAME checkbox.
3194
+ *
3195
+ * The head line's tail carries three platform-declared facts, in this order:
3196
+ * a PROMOTION tag (`displayLabel`, tinted like the desktop App's own chip), the
3197
+ * credit rate (`multiplier`, as `×N`), and the gating badge (`memberOnly`).
3198
+ * All three come from the live `/v2/models` catalogue — none is inferred, and
3199
+ * an absent field draws nothing rather than a placeholder.
3100
3200
  *
3101
3201
  * The checkbox is hook-free like the sibling rows: `onToggle` is handed in, so
3102
3202
  * without it the box is display-only and the roster cannot be edited at all.
@@ -3139,6 +3239,8 @@ var dsh_connect_agnes_token_plan_client = (function() {
3139
3239
  const label = String(row?.name ?? id);
3140
3240
  const on = modelIsOn(enabledIds, id);
3141
3241
  const meta = [typeof row?.contextWindow === "number" && row.contextWindow > 0 ? format(tt("llm.contextBadge"), { ctx: tokenSize(row.contextWindow) }) : null, typeof row?.maxOutputLength === "number" && row.maxOutputLength > 0 ? format(tt("llm.metaOutput"), { out: tokenSize(row.maxOutputLength) }) : null].filter(Boolean).join(" · ");
3242
+ const rate = typeof row?.multiplier === "number" ? row.multiplier : null;
3243
+ const promo = typeof row?.displayLabel === "string" && row.displayLabel !== "" ? row.displayLabel : null;
3142
3244
  return h("li", {
3143
3245
  key: id,
3144
3246
  style: {
@@ -3162,7 +3264,10 @@ var dsh_connect_agnes_token_plan_client = (function() {
3162
3264
  }), h("span", {
3163
3265
  style: S.modelName,
3164
3266
  title: id
3165
- }, label)), h("span", { style: S.spacer }), row.memberOnly === true ? h("span", { style: S.modelBadge }, tt("agnescode.memberOnly")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
3267
+ }, label)), h("span", { style: S.spacer }), promo === null ? null : h("span", { style: S.modelPromo }, promo), rate === null ? null : h("span", {
3268
+ style: S.modelRate,
3269
+ title: tt("agnescode.rateTitle")
3270
+ }, `×${rate}`), row.memberOnly === true ? h("span", { style: S.modelBadge }, tt("agnescode.memberOnly")) : null), meta === "" ? null : h("div", { style: S.modelMeta }, meta));
3166
3271
  })));
3167
3272
  }
3168
3273
  /**
@@ -3330,6 +3435,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
3330
3435
  const [data, setData] = useState(null);
3331
3436
  const [error, setError] = useState(null);
3332
3437
  const [loadedOnce, setLoadedOnce] = useState(false);
3438
+ const [refreshing, setRefreshing] = useState(false);
3333
3439
  const [updatedAt, setUpdatedAt] = useState(0);
3334
3440
  const [, setLocaleRevision] = useState(0);
3335
3441
  const [openSections, setOpenSections] = useState({
@@ -3356,6 +3462,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
3356
3462
  generation.current += 1;
3357
3463
  const mine = generation.current;
3358
3464
  const isCurrent = () => generation.current === mine;
3465
+ setRefreshing(true);
3359
3466
  inFlight.current?.abort?.();
3360
3467
  const controller = typeof AbortController === "function" ? new AbortController() : null;
3361
3468
  inFlight.current = controller;
@@ -3396,6 +3503,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
3396
3503
  setError(reason instanceof Error ? reason.message : String(reason));
3397
3504
  } finally {
3398
3505
  if (isCurrent()) setLoadedOnce(true);
3506
+ if (isCurrent()) setRefreshing(false);
3399
3507
  if (inFlight.current === controller) inFlight.current = null;
3400
3508
  }
3401
3509
  }, []);
@@ -3636,7 +3744,7 @@ var dsh_connect_agnes_token_plan_client = (function() {
3636
3744
  } : S.button,
3637
3745
  disabled: refresh === null,
3638
3746
  onClick: () => refresh?.()
3639
- }, tt("panel.refresh")), onClose ? h("button", {
3747
+ }, refreshing && plan.refresh === "snapshot" ? tt("panel.refreshing") : tt("panel.refresh")), onClose ? h("button", {
3640
3748
  type: "button",
3641
3749
  style: S.button,
3642
3750
  onClick: () => onClose()
@@ -1,5 +1,5 @@
1
- import { t as DEFAULT_REASONING_EFFORT, w as name } from "./llm-models-CLnHQAqK.js";
2
- import { i as buildAgnescodeDescriptors, n as AGNESCODE_PROVIDER_ID, r as agnescodeRoster, t as AGNESCODE_DISPLAY_NAME } from "./agnescode-models-DBsK6xeK.js";
1
+ import { t as DEFAULT_REASONING_EFFORT, w as name } from "./llm-models-Bep2p7Tp.js";
2
+ import { i as buildAgnescodeDescriptors, n as AGNESCODE_PROVIDER_ID, r as agnescodeRoster, t as AGNESCODE_DISPLAY_NAME } from "./agnescode-models-BMNUJaIy.js";
3
3
  import { i as buildRetryPolicyConfig, n as STREAM_IDLE_TIMEOUT_MS, r as createWrappedPiAiAdapter, t as REQUEST_IMAGE_BUDGETS } from "./pi-ai-adapter-core-BKmN5s11.js";
4
4
  import { createProvider } from "@earendil-works/pi-ai";
5
5
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -1,4 +1,4 @@
1
- import { D as num, P as str, i as PROBED_VISION, k as obj, w as name } from "./llm-models-CLnHQAqK.js";
1
+ import { A as obj, D as num, F as str, i as PROBED_VISION, k as numZeroOk, w as name } from "./llm-models-Bep2p7Tp.js";
2
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -896,6 +896,17 @@ const AGNESCODE_HARVEST_TIER = Object.freeze({
896
896
  OK: "ok"
897
897
  });
898
898
  /**
899
+ * Catalogue rows that are NOT models and must never reach the roster.
900
+ *
901
+ * `/v2/models` adds an `auto` row that `/v1/models` does not have. It is a
902
+ * routing alias (no vendor, no `points_cost_multiplier`, and the only row the
903
+ * platform omits the price on), so offering it as a selectable model would put
904
+ * a non-model in the picker. Keyed by id on purpose: a rule tied to "has no
905
+ * multiplier" would silently start leaking `auto` back in the day the platform
906
+ * prices it.
907
+ */
908
+ const AGNESCODE_CATALOGUE_EXCLUDED_IDS = Object.freeze(["auto"]);
909
+ /**
899
910
  * Whether a BFF base may carry the account's Authorization header. Pinned to
900
911
  * the Agnes origin family over https — see the module header. Anything else
901
912
  * reads as `UNTRUSTED_BASE` and the harvest keeps walking.
@@ -965,9 +976,12 @@ function decodeAgnescodeJwtExpMs(token) {
965
976
  }
966
977
  }
967
978
  /**
968
- * The header set one AgnesCode BFF request carries. The catalogue headers are
969
- * REQUIRED on `/v1/models` (probed) and harmless elsewhere; the language
970
- * header mirrors the desktop client.
979
+ * The header set one AgnesCode BFF request carries. The catalogue headers
980
+ * (`X-App-Id` / `X-Platform`) were probed REQUIRED on the catalogue endpoint
981
+ * and are harmless elsewhere; the language header mirrors the desktop client.
982
+ * The catalogue moved from `/v1/models` to `{origin}/v2/models` on 2026-10-04
983
+ * and the same header set answers 200 there (re-probed live, so this map did
984
+ * not need to change with the endpoint).
971
985
  * @param {object} credential - `{ accessToken }`.
972
986
  * @returns {object} the header map.
973
987
  */
@@ -1248,13 +1262,29 @@ async function harvestAgnescodeLocalSession(options = {}) {
1248
1262
  * adapter falls back to the static roster on `null` (the `console-client`
1249
1263
  * silent-fallback discipline).
1250
1264
  *
1251
- * The BFF rows carry `is_member_only` — a fact about gating, not visibility,
1252
- * so it is KEPT (the panel badges it; offering a member-gated model is
1253
- * preferable to silently hiding one, mirroring red line 7's spirit of stating
1254
- * account-level limits instead of dropping models).
1265
+ * **Endpoint: `/v2/models`, not `/v1/models`** (switched 2026-10-04). The two
1266
+ * are different directories on the same host, not the same data: v1 carries 8
1267
+ * rows and NO credit multiplier, v2 carries 9 rows and the platform's own
1268
+ * `points_cost_multiplier` on 8 of them (measured live, same token/headers;
1269
+ * see ROADMAP §6.3.1 「倍率字段正记」). The desktop App reads v2 — that is why
1270
+ * its picker shows `1.20x` / `1.85x` while this plugin showed nothing.
1271
+ *
1272
+ * The member fact MOVES FIELDS between the two: v1 declares
1273
+ * `is_member_only: bool`, v2 declares `allowed_subscription: string[]`. The
1274
+ * mapping `memberOnly = allowed_subscription.length > 0` was verified against
1275
+ * v1 across all 8 shared rows with zero mismatches — so this is a re-encoding,
1276
+ * not a reinterpretation.
1277
+ *
1278
+ * `auto` is DROPPED: it exists only in v2, is a routing alias rather than a
1279
+ * real model, and carries no multiplier. Dropping by explicit id (not by
1280
+ * "no multiplier") keeps the filter honest if the platform later prices it.
1281
+ *
1282
+ * The rows carry a per-model credit multiplier — a fact about billing, not
1283
+ * gating, so it is KEPT and travels to the panel and the descriptor's display
1284
+ * name (see `agnescodeToDescriptor`).
1255
1285
  * @param {object} credential - `{ accessToken, bffBase }`.
1256
1286
  * @param {typeof fetch} [fetcher] - injected fetch.
1257
- * @returns {Promise<object[]|null>} `[{id, name, vision, memberOnly, contextWindow, maxOutputLength}]`, or `null`.
1287
+ * @returns {Promise<object[]|null>} `[{id, name, vision, memberOnly, multiplier, contextWindow, maxOutputLength}]`, or `null`.
1258
1288
  */
1259
1289
  async function fetchAgnescodeCatalog(credential, fetcher) {
1260
1290
  const effective = fetcher ?? globalThis.fetch;
@@ -1262,7 +1292,7 @@ async function fetchAgnescodeCatalog(credential, fetcher) {
1262
1292
  const bffBase = trustAgnescodeBffBase(source.bffBase);
1263
1293
  if (bffBase === null) return null;
1264
1294
  try {
1265
- const response = await effective(`${bffBase}/models`, {
1295
+ const response = await effective(`${agnescodeApiRoot(bffBase)}/v2/models`, {
1266
1296
  headers: agnescodeHeaders(source),
1267
1297
  signal: AbortSignal.timeout(3e4)
1268
1298
  });
@@ -1275,13 +1305,19 @@ async function fetchAgnescodeCatalog(credential, fetcher) {
1275
1305
  const model = obj(raw);
1276
1306
  const id = str(model.id, "");
1277
1307
  if (id === "" || seen.has(id)) continue;
1308
+ if (id === AGNESCODE_CATALOGUE_EXCLUDED_IDS[0]) continue;
1278
1309
  if (str(model.model_type, "text") !== "text") continue;
1279
1310
  seen.add(id);
1311
+ const allowed = Array.isArray(model.allowed_subscription) ? model.allowed_subscription : [];
1312
+ const multiplier = numZeroOk(model.points_cost_multiplier);
1313
+ const displayLabel = str(model.display_label, "");
1280
1314
  out.push({
1281
1315
  id,
1282
1316
  name: str(model.name, id),
1283
1317
  vision: false,
1284
- memberOnly: model.is_member_only === true,
1318
+ memberOnly: allowed.length > 0,
1319
+ ...multiplier === void 0 ? {} : { multiplier },
1320
+ ...displayLabel === "" ? {} : { displayLabel },
1285
1321
  contextWindow: num(model.max_input_tokens),
1286
1322
  maxOutputLength: num(model.max_output_tokens)
1287
1323
  });
@@ -1346,6 +1382,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1346
1382
  name: "Agnes 3.0 Flash",
1347
1383
  memberOnly: false,
1348
1384
  vision: false,
1385
+ multiplier: 0,
1349
1386
  contextWindow: 512e3,
1350
1387
  maxOutputLength: 65536
1351
1388
  },
@@ -1354,6 +1391,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1354
1391
  name: "Agnes 2.5 Flash",
1355
1392
  memberOnly: false,
1356
1393
  vision: false,
1394
+ multiplier: 0,
1357
1395
  contextWindow: 512e3,
1358
1396
  maxOutputLength: 65536
1359
1397
  },
@@ -1362,6 +1400,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1362
1400
  name: "Agnes 2.5 Pro",
1363
1401
  memberOnly: false,
1364
1402
  vision: false,
1403
+ multiplier: 1,
1365
1404
  contextWindow: 512e3,
1366
1405
  maxOutputLength: 65536
1367
1406
  },
@@ -1370,6 +1409,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1370
1409
  name: "DeepSeek V4 Flash",
1371
1410
  memberOnly: true,
1372
1411
  vision: false,
1412
+ multiplier: 1.2,
1373
1413
  contextWindow: 1e6,
1374
1414
  maxOutputLength: 393216
1375
1415
  },
@@ -1378,6 +1418,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1378
1418
  name: "Agnes 2.0 Flash",
1379
1419
  memberOnly: false,
1380
1420
  vision: false,
1421
+ multiplier: 0,
1381
1422
  contextWindow: 512e3,
1382
1423
  maxOutputLength: 65536
1383
1424
  },
@@ -1386,6 +1427,8 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1386
1427
  name: "GLM-5.2",
1387
1428
  memberOnly: true,
1388
1429
  vision: false,
1430
+ multiplier: 1.85,
1431
+ displayLabel: "限时七折",
1389
1432
  contextWindow: 1e6,
1390
1433
  maxOutputLength: 131072
1391
1434
  },
@@ -1394,6 +1437,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1394
1437
  name: "Kimi K3",
1395
1438
  memberOnly: true,
1396
1439
  vision: false,
1440
+ multiplier: 5.3,
1397
1441
  contextWindow: 1048576,
1398
1442
  maxOutputLength: 131072
1399
1443
  },
@@ -1402,6 +1446,7 @@ const AGNESCODE_FALLBACK_MODELS = Object.freeze([
1402
1446
  name: "DeepSeek V4 Pro",
1403
1447
  memberOnly: true,
1404
1448
  vision: false,
1449
+ multiplier: 1.5,
1405
1450
  contextWindow: 1e6,
1406
1451
  maxOutputLength: 393216
1407
1452
  }
@@ -1513,12 +1558,13 @@ function agnescodeRoster(catalog) {
1513
1558
  for (const row of rows) {
1514
1559
  const id = str(row?.id, "");
1515
1560
  if (id === "") continue;
1561
+ const multiplier = numZeroOk(row?.multiplier);
1516
1562
  out.push({
1517
1563
  id,
1518
1564
  name: str(row?.name, id),
1519
1565
  vision: agnescodeVisionOf(row),
1520
1566
  memberOnly: row?.memberOnly === true,
1521
- multiplier: typeof row?.multiplier === "number" ? row.multiplier : void 0,
1567
+ ...multiplier === void 0 ? {} : { multiplier },
1522
1568
  contextWindow: num(row?.contextWindow),
1523
1569
  maxOutputLength: num(row?.maxOutputLength)
1524
1570
  });
@@ -1582,9 +1628,11 @@ function agnescodeToDescriptor(row, options = {}) {
1582
1628
  const bffBase = str(options.bffBase, "");
1583
1629
  if (bffBase === "") throw new Error("agnescodeToDescriptor: a pinned bffBase is required");
1584
1630
  const vision = agnescodeVisionOf(row);
1631
+ const multiplier = numZeroOk(row?.multiplier);
1632
+ const name = str(row?.name, id);
1585
1633
  return {
1586
1634
  id,
1587
- name: str(row?.name, id),
1635
+ name: multiplier === void 0 ? name : `${name} · x${multiplier.toFixed(2)}`,
1588
1636
  api: "openai-completions",
1589
1637
  provider: AGNESCODE_PROVIDER_ID,
1590
1638
  baseUrl: bffBase,
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as pluginError, C as isAdmitted, D as num, E as resolveSettings, F as verbatim, M as redactSecrets, N as retryBounded, O as numOrNull, P as str, S as inject, T as resolveAuthOverrides, _ as parseUsageSeries, b as CONFIG_DEFAULTS, c as summarizeCatalog, d as isVideoGenModel, f as checkShape, g as parseUsageOverview, h as parseSubscriptionUsage, j as redactError, k as obj, l as visionOf, m as parsePlans, n as LLM_DISPLAY_NAME, o as filterByEnabled, p as matchCurrentPlan, r as LLM_PROVIDER_ID, s as rosterWithAvailability, t as DEFAULT_REASONING_EFFORT, u as isImageGenModel, v as quotaWindows, w as name, x as hostName, y as readSubscriptionExpiry } from "./llm-models-CLnHQAqK.js";
2
- import { S as writeStateFile, _ as profileSegment, a as filterAgnescodeRows, b as stateDir, c as fetchAgnescodeBalance, d as trustAgnescodeBffBase, f as createFileCatalogStore, g as ensureStateDir, h as createStateReadCache, l as fetchAgnescodeCatalog, m as STATE_READ_TTL_MS, n as AGNESCODE_PROVIDER_ID, o as AGNESCODE_FALLBACK_MODELS, p as normalizeEnabledIds, s as decodeAgnescodeJwtExpMs, t as AGNESCODE_DISPLAY_NAME, u as harvestAgnescodeLocalSession, v as profileStateDir, x as temporaryOf, y as readStateJson } from "./agnescode-models-DBsK6xeK.js";
1
+ import { A as obj, C as isAdmitted, D as num, E as resolveSettings, F as str, I as verbatim, M as redactError, N as redactSecrets, O as numOrNull, P as retryBounded, S as inject, T as resolveAuthOverrides, _ as parseUsageSeries, b as CONFIG_DEFAULTS, c as summarizeCatalog, d as isVideoGenModel, f as checkShape, g as parseUsageOverview, h as parseSubscriptionUsage, j as pluginError, k as numZeroOk, l as visionOf, m as parsePlans, n as LLM_DISPLAY_NAME, o as filterByEnabled, p as matchCurrentPlan, r as LLM_PROVIDER_ID, s as rosterWithAvailability, t as DEFAULT_REASONING_EFFORT, u as isImageGenModel, v as quotaWindows, w as name, x as hostName, y as readSubscriptionExpiry } from "./llm-models-Bep2p7Tp.js";
2
+ import { S as writeStateFile, _ as profileSegment, a as filterAgnescodeRows, b as stateDir, c as fetchAgnescodeBalance, d as trustAgnescodeBffBase, f as createFileCatalogStore, g as ensureStateDir, h as createStateReadCache, l as fetchAgnescodeCatalog, m as STATE_READ_TTL_MS, n as AGNESCODE_PROVIDER_ID, o as AGNESCODE_FALLBACK_MODELS, p as normalizeEnabledIds, s as decodeAgnescodeJwtExpMs, t as AGNESCODE_DISPLAY_NAME, u as harvestAgnescodeLocalSession, v as profileStateDir, x as temporaryOf, y as readStateJson } from "./agnescode-models-BMNUJaIy.js";
3
3
  import { rm } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { homedir } from "node:os";
@@ -2848,7 +2848,7 @@ function createAgnescodePublisher(deps = {}) {
2848
2848
  const { panelSwitch, resolveToken, getLlm, loadAdapterModule, emit, logger } = deps;
2849
2849
  const effectivePanelSwitch = panelSwitch ?? (async () => null);
2850
2850
  const effectiveResolveToken = resolveToken ?? (async () => "");
2851
- const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./agnescode-llm-adapter-BrButArh.js"));
2851
+ const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./agnescode-llm-adapter-j7S1OMDU.js"));
2852
2852
  const effectiveGetLlm = getLlm ?? (() => null);
2853
2853
  const effectiveEmit = emit ?? (() => {});
2854
2854
  const effectiveLogger = logger ?? { warn: () => {} };
@@ -3092,7 +3092,7 @@ function wireAgnescodePublisher({ store, panelSwitch, enabledIds, getLlm, loadAd
3092
3092
  panelSwitch: panelSwitch ?? (async () => null),
3093
3093
  resolveToken,
3094
3094
  getLlm,
3095
- loadAdapterModule: loadAdapterModule ?? (() => import("./agnescode-llm-adapter-BrButArh.js")),
3095
+ loadAdapterModule: loadAdapterModule ?? (() => import("./agnescode-llm-adapter-j7S1OMDU.js")),
3096
3096
  emit: emit ?? (() => {}),
3097
3097
  logger
3098
3098
  });
@@ -3208,7 +3208,7 @@ function createProviderPublisher(deps = {}) {
3208
3208
  const { settings, panelSwitch, loadAdapterModule, getLlm, resolveApiKey, emit, logger } = deps;
3209
3209
  const effectiveSettings = settings ?? {};
3210
3210
  const effectivePanelSwitch = panelSwitch ?? (async () => null);
3211
- const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./llm-adapter-D2Igtn0s.js"));
3211
+ const effectiveLoadAdapterModule = loadAdapterModule ?? (() => import("./llm-adapter-iKICEhY6.js"));
3212
3212
  const effectiveGetLlm = getLlm ?? (() => null);
3213
3213
  const effectiveResolveApiKey = resolveApiKey ?? (async () => "");
3214
3214
  const effectiveEmit = emit ?? (() => {});
@@ -3394,12 +3394,32 @@ function seedPublisherFromCatalog(publisher, listCatalog, listEnabled, signature
3394
3394
  * use (an id whose modality flipped must rebuild even though the id list did
3395
3395
  * not change), plus the curated allow-list. Anything else changing in a
3396
3396
  * catalog entry does not affect the registered offer.
3397
+ *
3398
+ * **The multiplier IS part of the offer** (since 2026-10-04): it rides in the
3399
+ * descriptor's display name (`Name · xNN.NN` — see `agnescodeToDescriptor`),
3400
+ * because pi-ai's descriptor has no billing channel for it. So a repriced model
3401
+ * must rebuild the registration, or the picker keeps showing a stale factor:
3402
+ * measured on this signature, `glm-5.2` moving 1.85 → 9.99 produced an
3403
+ * IDENTICAL `id:vision` string, so the old shape would have kept advertising
3404
+ * 1.85 forever. This matters because the platform reprices on promotion
3405
+ * boundaries (`glm-5.2` carries `display_label: "限时七折"`).
3406
+ *
3407
+ * Reading the multiplier from `entries` (the raw catalogue) deliberately does
3408
+ * NOT catch the Token Plan side's operator-configured pseudo multiplier: that
3409
+ * one is attached later, by `matchMultiplier`, to the rows the snapshot hands
3410
+ * the panel — never to `catalog`. Turning that knob still does not rebuild the
3411
+ * registration, which is the documented intent (IMPROVEMENTS.md §7).
3397
3412
  * @param {object[]} entries - the normalized catalog entries.
3398
3413
  * @param {string[]} enabledIds - the allow-list (empty = all).
3399
3414
  * @returns {string}
3400
3415
  */
3401
3416
  function catalogSignature(entries, enabledIds) {
3402
- return `${(Array.isArray(entries) ? entries : []).map((entry) => `${str(entry?.id, "")}:${visionOf(entry).vision === true ? 1 : 0}`).join(",")}|${(Array.isArray(enabledIds) ? enabledIds : []).join(",")}`;
3417
+ return `${(Array.isArray(entries) ? entries : []).map((entry) => {
3418
+ const id = str(entry?.id, "");
3419
+ const vision = visionOf(entry).vision === true ? 1 : 0;
3420
+ const multiplier = numZeroOk(entry?.multiplier);
3421
+ return `${id}:${vision}:${multiplier === void 0 ? "-" : multiplier}`;
3422
+ }).join(",")}|${(Array.isArray(enabledIds) ? enabledIds : []).join(",")}`;
3403
3423
  }
3404
3424
 
3405
3425
  //#endregion
@@ -7090,7 +7110,7 @@ function apply(ctx, config = {}, deps = {}) {
7090
7110
  }
7091
7111
  };
7092
7112
  const emitAdaptersUpdate = () => emitAdaptersUpdated((event) => ctx.emit?.(event));
7093
- const loadAdapterModule = deps.loadAdapterModule ?? (() => import("./llm-adapter-D2Igtn0s.js"));
7113
+ const loadAdapterModule = deps.loadAdapterModule ?? (() => import("./llm-adapter-iKICEhY6.js"));
7094
7114
  const publisher = createProviderPublisher({
7095
7115
  settings,
7096
7116
  panelSwitch: () => providerStore.enabled().catch(() => null),
@@ -1,4 +1,4 @@
1
- import { a as buildDescriptors, n as LLM_DISPLAY_NAME, r as LLM_PROVIDER_ID, t as DEFAULT_REASONING_EFFORT, w as name } from "./llm-models-CLnHQAqK.js";
1
+ import { a as buildDescriptors, n as LLM_DISPLAY_NAME, r as LLM_PROVIDER_ID, t as DEFAULT_REASONING_EFFORT, w as name } from "./llm-models-Bep2p7Tp.js";
2
2
  import { i as buildRetryPolicyConfig, n as STREAM_IDLE_TIMEOUT_MS, r as createWrappedPiAiAdapter, t as REQUEST_IMAGE_BUDGETS } from "./pi-ai-adapter-core-BKmN5s11.js";
3
3
  import { createProvider } from "@earendil-works/pi-ai";
4
4
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -89,6 +89,27 @@ function numOrNull$1(value) {
89
89
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
90
90
  }
91
91
  /**
92
+ * Read a finite number that may legitimately be ZERO, else `undefined`.
93
+ *
94
+ * `num()` and `numOrNull()` both reject `0` because for their callers a zero is
95
+ * a missing reading (a limit, a window, a count — none of which is honestly
96
+ * zero when the platform simply did not say). This reader exists for the
97
+ * opposite case, and the distinction is load-bearing at the call site: the
98
+ * platform's `points_cost_multiplier` uses `0` to mean "this model is free",
99
+ * which is a published PRICE, not a missing value. Routing it through `num()`
100
+ * would collapse "free" into "not stated" and the panel would then render
101
+ * nothing where it should render the free badge (the sibling SenseNova plugin
102
+ * renders this exact figure as `· x0.00`).
103
+ *
104
+ * So: absent / non-numeric / non-finite / negative → `undefined` (no claim);
105
+ * `0` → `0` (a claim of free).
106
+ * @param {unknown} value - the raw field.
107
+ * @returns {number|undefined} the finite non-negative number, or `undefined`.
108
+ */
109
+ function numZeroOk(value) {
110
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
111
+ }
112
+ /**
92
113
  * Run one attempt loop inside a bounded window.
93
114
  *
94
115
  * The shared backoff shape for every "a service or a seed may be late" case
@@ -1781,4 +1802,4 @@ function summarizeCatalog(entries) {
1781
1802
  }
1782
1803
 
1783
1804
  //#endregion
1784
- export { pluginError as A, isAdmitted as C, num as D, resolveSettings as E, verbatim as F, redactSecrets as M, retryBounded as N, numOrNull$1 as O, str as P, inject as S, resolveAuthOverrides as T, parseUsageSeries as _, buildDescriptors as a, CONFIG_DEFAULTS as b, summarizeCatalog as c, isVideoGenModel as d, checkShape as f, parseUsageOverview as g, parseSubscriptionUsage as h, PROBED_VISION as i, redactError as j, obj as k, visionOf as l, parsePlans as m, LLM_DISPLAY_NAME as n, filterByEnabled as o, matchCurrentPlan as p, LLM_PROVIDER_ID as r, rosterWithAvailability as s, DEFAULT_REASONING_EFFORT as t, isImageGenModel as u, quotaWindows as v, name as w, hostName as x, readSubscriptionExpiry as y };
1805
+ export { obj as A, isAdmitted as C, num as D, resolveSettings as E, str as F, verbatim as I, redactError as M, redactSecrets as N, numOrNull$1 as O, retryBounded as P, inject as S, resolveAuthOverrides as T, parseUsageSeries as _, buildDescriptors as a, CONFIG_DEFAULTS as b, summarizeCatalog as c, isVideoGenModel as d, checkShape as f, parseUsageOverview as g, parseSubscriptionUsage as h, PROBED_VISION as i, pluginError as j, numZeroOk as k, visionOf as l, parsePlans as m, LLM_DISPLAY_NAME as n, filterByEnabled as o, matchCurrentPlan as p, LLM_PROVIDER_ID as r, rosterWithAvailability as s, DEFAULT_REASONING_EFFORT as t, isImageGenModel as u, quotaWindows as v, name as w, hostName as x, readSubscriptionExpiry as y };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-connect-agnes-token-plan",
3
3
  "displayName": "DSH Connect Agnes Token Plan",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "description": "Agnes connect bundle for the Harness Web UI: Token Plan quota panel (account-wide quota windows, cumulative usage, console sign-in) plus optional provider registration that brings Agnes models into DSH conversation, with host-side rate-limit self-healing.",
6
6
  "keywords": [
7
7
  "dsh-plugin",
package/screenshots.json CHANGED
@@ -1,4 +1,5 @@
1
1
  [
2
2
  "assets/panel-credit.png",
3
- "assets/panel-API-provider.png"
3
+ "assets/panel-API-provider.png",
4
+ "assets/panel-AgnesCode.png"
4
5
  ]