dsh-plugin-subscription-oauth 0.2.2 → 0.4.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/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [English](README.md) · [简体中文](README.zh.md)
2
+
1
3
  # dsh-plugin-subscription-oauth
2
4
 
3
5
  [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/EricFL1998/dsh-subscription-oauth)
@@ -34,6 +36,7 @@ dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
34
36
  | `grok` | Grok (X Premium) | OIDC **authorization code + PKCE** on a loopback callback | OpenAI **Responses** — `https://api.x.ai/v1/responses` |
35
37
  | `antigravity` | Google Antigravity | Google **authorization code + PKCE** (`/oauth-callback`) plus Cloud AI Companion project discovery | Cloud Code Assist `v1internal` — `streamGenerateContent?alt=sse` |
36
38
  | `glm` | GLM (BigModel) | ZCode's **browser sign-in with a polled flow** (China region) | OpenAI **Responses** — `https://open.bigmodel.cn/api/coding/paas/v4/responses` |
39
+ | `ark` | Volcengine Ark | Volcengine **SSO** (authorization code + PKCE on a loopback callback) | OpenAI **Responses** — `https://ark.cn-beijing.volces.com/api/coding/v3/responses` |
37
40
  | `auto` | Auto | none — it holds no account of its own | delegates to whichever route owns the model it picks |
38
41
 
39
42
  The Kimi device flow is the one `opencodex` uses; the Grok flow is the one its
@@ -52,6 +55,15 @@ plugin generates itself. Allowances are read from BigModel's own
52
55
  `/api/monitor/usage/quota/limit`, which meters a rolling five-hour window and a
53
56
  weekly one.
54
57
 
58
+ **Ark signs in with Volcengine SSO** (authorization code + PKCE on a loopback
59
+ callback, the flow its own CLI uses) and then has to satisfy two planes that want
60
+ different credentials. The **data plane** takes only an ordinary API key as a plain
61
+ bearer — it refuses STS credentials outright — while the **control plane**, a
62
+ different host, is signed with the temporary STS credentials that the SSO token
63
+ response carries. So a sign-in mints one API key through the control plane (reusing
64
+ a key this plugin already created, if there is one) and keeps the STS set for the
65
+ quota, which is read per subscription period and per model's free allowance.
66
+
55
67
  ## What it registers
56
68
 
57
69
  | Surface | What appears |
@@ -352,6 +364,7 @@ themselves.
352
364
  | `grok` | `auth.x.ai`, `api.x.ai`, `cli-chat-proxy.grok.com` | OIDC sign-in, the Responses API, the plan's credit figure |
353
365
  | `antigravity` | `accounts.google.com`, `oauth2.googleapis.com`, `cloudcode-pa.googleapis.com` | Google sign-in, the account's project, then Cloud Code Assist (including its quota accounting) |
354
366
  | `glm` | `bigmodel.cn`, `zcode.z.ai`, `open.bigmodel.cn` | browser sign-in brokered by ZCode, then the coding-plan API and its allowance |
367
+ | `ark` | `signin.volcengine.com`, `ark.cn-beijing.volces.com`, `ark.cn-beijing.volcengineapi.com` | Volcengine SSO, then the coding-plan API (data plane) and the signed control plane that mints the API key and reports the allowance |
355
368
 
356
369
  There is no analytics, no update ping and no relay through anyone else's server:
357
370
  every request goes to the host that owns the account. No request body, prompt or
package/README.zh.md ADDED
@@ -0,0 +1,371 @@
1
+ [English](README.md) · [简体中文](README.zh.md)
2
+
3
+ # dsh-plugin-subscription-oauth
4
+
5
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/EricFL1998/dsh-subscription-oauth)
6
+ [![npm](https://img.shields.io/npm/v/dsh-plugin-subscription-oauth.svg)](https://www.npmjs.com/package/dsh-plugin-subscription-oauth)
7
+ [![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)
8
+
9
+ **把你已经付费的 AI 订阅当作 DeepSeek Harness 中的 LLM 提供方使用——无需 API
10
+ 密钥。** 每个提供方只需在 **Settings → Subscriptions**(订阅)中登录一次,该账户的
11
+ 模型就会加入会话的模型选择器,其套餐配额显示在连接旁边。
12
+
13
+ ```sh
14
+ dsh plugin --profile web add dsh-plugin-subscription-oauth
15
+ ```
16
+
17
+ 同一个包也可以直接从仓库安装,这不需要 registry:
18
+
19
+ ```sh
20
+ dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
21
+ ```
22
+
23
+ | | |
24
+ | --- | --- |
25
+ | **能力** | 模型与推理 / Model & inference — 它注册 LLM 提供方路由 |
26
+ | **许可证** | [MIT](LICENSE) |
27
+ | **适用版本** | DeepSeek Harness `0.2.0-rc.2`(`@deepseek-ai/dsh-llm`),Node ≥ 20 |
28
+ | **平台** | Windows、macOS 和 Linux;除 Node 本身外没有任何平台特定内容 |
29
+ | **账户** | 仅限你自己的订阅——凭证留在你的机器上 |
30
+ | **网络** | 提供方自己的主机以及下面列出的 OAuth 端点;没有遥测,没有第三方中转 |
31
+
32
+ | 路由 | 提供方 | 登录 | 传输 |
33
+ | --- | --- | --- | --- |
34
+ | `kimi` | Kimi Code | OAuth **设备流**(RFC 8628) | OpenAI **Responses** — `https://api.kimi.com/coding/v1/responses` |
35
+ | `grok` | Grok (X Premium) | OIDC **授权码 + PKCE**,回调走回环地址 | OpenAI **Responses** — `https://api.x.ai/v1/responses` |
36
+ | `antigravity` | Google Antigravity | Google **授权码 + PKCE**(`/oauth-callback`),外加 Cloud AI Companion 项目发现 | Cloud Code Assist `v1internal` — `streamGenerateContent?alt=sse` |
37
+ | `glm` | GLM (BigModel) | ZCode 的**浏览器登录加轮询流程**(中国区) | OpenAI **Responses** — `https://open.bigmodel.cn/api/coding/paas/v4/responses` |
38
+ | `ark` | 火山方舟 | 火山引擎 **SSO**(授权码 + PKCE,回调走回环地址) | OpenAI **Responses** — `https://ark.cn-beijing.volces.com/api/coding/v3/responses` |
39
+ | `auto` | Auto | 无——它自己不持有任何账户 | 委托给它所选模型所属的那条路由 |
40
+
41
+ Kimi 的设备流就是 `opencodex` 所用的那一个;Grok 的流程是它的 CLI 所用的那一个
42
+ (`auth.x.ai` 发现、客户端 `b1a00492-…`、端口 `56121`、scope `grok-cli:access`);
43
+ Antigravity 的流程是 Google 的桌面 OAuth 客户端(`accounts.google.com`、端口
44
+ `51121`,scope 包括 `cclog` 和 `experimentsandconfigs`),账户的项目通过
45
+ `loadCodeAssist` 发现。
46
+
47
+ **GLM 采用 ZCode 的登录方式**,而不是用 API 密钥:插件向 `zcode.z.ai` 请求一个登录
48
+ 流程,在浏览器中打开 `bigmodel.cn/login`,并轮询该流程直到订阅的令牌返回。不需要
49
+ 任何东西接收 `zcode://oauth/callback` 重定向,也不涉及应用 secret,因为回调是在
50
+ 服务端完成的,而该流程由插件自己生成的一个 secret 进行认证。额度读取自 BigModel
51
+ 自己的 `/api/monitor/usage/quota/limit`,它计量一个滚动的五小时窗口和一个每周窗口。
52
+
53
+ ## 它注册了什么
54
+
55
+ | 层面 | 出现的内容 |
56
+ | --- | --- |
57
+ | `ctx.llm` | `kimi`、`grok`、`antigravity`、`glm` 和 `auto` 提供方路由(模型、推理强度、流式输出) |
58
+ | Settings | 一个 **Subscriptions** 区域:每个提供方一张卡片——登录、退出登录、默认账户、令牌刷新、套餐配额、模型目录、管理对话框 |
59
+ | 模型选择器 | 每个提供方自己的模型,外加**作为独立路由的 `auto`**,横跨它们——Kimi 提供 `kimi-for-coding`、`k3`、`k3[1m]`、`k3-256k`;Grok 内置 `grok-4.6`、`grok-4.5`,并加入其实时目录所报告的任何模型;Antigravity 列出其端点所报告内容(`fetchAvailableModels`)中可供 agent 调用的子集,过滤掉 tabs、commit-message 和 transcription 相关层面;GLM 提供 `glm-5.3` 系列(`glm-5.3`、`glm-5.3-flash`、`glm-5.2`、`glm-5.1`、`glm-5`),并加入其编码主机所列出的一切 |
60
+
61
+ 凭证存放在 `~/.dsh/plugins/kimi-oauth/auth.json`(模式 `0600`,原子写入,每个提供方
62
+ 一个区段)。该目录保留了本插件在只服务 Kimi Code 时所用的名称:它保存着**活跃的
63
+ 登录信息**,所以保持原样而不改名,每个挂载过早期版本的 profile 都能在它当初留下的
64
+ 位置找到自己的账户。本机的 Kimi 设备 id 存放在它旁边的 `kimi-device-id` 中;它在
65
+ 重启之间保持稳定,因为服务会把用量与之关联。
66
+
67
+ ### 令牌有效期,以及卡片为什么可能显示 "expired"
68
+
69
+ Kimi Code 的访问令牌有效期为 **15 分钟**;刷新令牌有效期为 **30 天**。Grok 的访问
70
+ 令牌类似。每当访问令牌距离失效不足五分钟时,每个请求都会自动刷新,而 Settings 卡片
71
+ 会在你查看它时主动刷新——所以它显示的是实时到期时间,只有在刷新确实失败、账户需要
72
+ 重新登录时才会出现 "token expired"。访问令牌到期是正常的,并不意味着登录已经失效。
73
+
74
+ ## 设置页面
75
+
76
+ | 所有路由折叠时 | 一张订阅卡片展开时 | 一张卡片连同其目录 |
77
+ | --- | --- | --- |
78
+ | ![折叠的提供方列表](docs/images/providers-folded.png) | ![带套餐配额的 GLM 卡片](docs/images/glm-card-open.png) | ![带目录的 Kimi 卡片](docs/images/kimi-card-open.png) |
79
+
80
+ 页面以 **`auto`** 开头,它是同时触达其他所有路由的方式;它所代表的路由按配置顺序
81
+ 依次排列。每条路由是一个**折叠行**——状态圆点、名称、路由键、一行连接摘要,以及一个
82
+ **Edit** 按钮——因此该区域读起来就是一份提供方列表,并按需展开其中一个。**每次访问
83
+ 都从折叠状态开始**——展开的卡片是一次访问中的一个动作,而不是一个被保存的设置——所以
84
+ 重新打开 Settings 总是看到同样紧凑的列表。展开的卡片内部是账户行(★ 设置默认账户,
85
+ `退出登录`/`Sign out` 移除一个)、模型目录和套餐配额。控件沿用宿主自己的设置表单语汇
86
+ ——6 px 圆角(`--dsw-radius-sm`)、透明填充、细边框,以及主题的 hover/active/focus
87
+ token——它们在一个作用域样式表中声明,因此页面保持那种外观,而不会退回到浏览器的
88
+ 默认按钮样式。
89
+
90
+ **Models** 标题本身是一个独立折叠项,**每次访问都折叠**:点击它可打开目录。
91
+ **每个模型都有自己的开关**(一个 toggle,而不是 checkbox),它决定会话的模型选择器
92
+ 是否提供该模型。该选择以隐藏列表的形式存储在宿主上
93
+ (`~/.dsh/plugins/kimi-oauth/preferences.json`),因此它能跨重启保留,并且每个会话都
94
+ 能看到它;后来某次目录刷新新增的模型会自动出现,而不是以隐藏状态出现。被隐藏的模型
95
+ 以变暗的方式列出,并且仍然**可解析**——已经选中它的现有会话继续正常工作——所以隐藏
96
+ 是选择器可见性上的选择,而不是撤销。标题中的 `· 已隐藏 N 个` 统计被隐藏的数量。
97
+
98
+ **Add model** 会发布一个本构建从未听说过的 id,因此插件构建之后发布的模型可以立即
99
+ 使用。该 id 只检查形态,不比对列表——端点才是它所提供内容的唯一权威——并且它按路由
100
+ 存储,以保守的元数据发布,并在列表中带一个 `×`,以便打错的条目可以再次移除。**拖动
101
+ 一行**可通过它的 ⠿ 抓手(或在抓手获得焦点时按 ↑ / ↓)来重新排序列表;该顺序就是选择
102
+ 器对该路由所遵循的顺序。
103
+
104
+ **`auto` 是一个自成一路的提供方**(路由 `auto`),而不是每个提供方内部的某个模型:
105
+ 它在一个列表中代表 harness 所服务的*每一个*提供方的已启用模型,每一行都带有拥有它的
106
+ 提供方徽标。这既包括本插件注册的路由,**也**包括它没有注册的路由——内置的 DeepSeek
107
+ 提供方以及你自己添加的任何提供方——因为它枚举 harness 名册
108
+ (`ctx.llm.listProviders()` / `listModels()`)并通过它分发,而不是只查看自己的路由。
109
+ 它同时位于 Settings 区域和模型选择器的首位,因为它是唯一能触达每条路由的条目。它没有
110
+ 账户,所以它的卡片不提供连接或管理——只有列表。一个开关可以把某个模型移出 Auto
111
+ 而**不**把它从选择器中隐藏,拖动会重排 Auto 首先尝试的顺序,这些安排单独存储
112
+ (`auto.order` / `auto.off`)。把每个模型都关掉,Auto 就根本不会在选择器中出现。
113
+
114
+ 没有已登录账户的提供方不贡献任何行:在某条路由拥有会话之前,`listModels` 什么也不
115
+ 返回,因此退出登录会同时把该路由的模型从选择器和 Auto 中移除,其卡片会隐藏目录,而
116
+ 不是提供无法调用的模型。
117
+
118
+ 当提供方拒绝首选模型时(被重命名或已消失、被限流、额度耗尽,或传输失败),Auto 会
119
+ 落到下一个模型。具体的模型 id 从不回退——它会分派到正是那个模型。一旦某个流已经产生
120
+ 任何输出,这一轮就属于那个模型,所以之后的失败会被报告出来,而不是被第二个模型的文本
121
+ 掩盖过去。每次回退都会记录日志(`model X unavailable … falling back to Y`)。
122
+
123
+ **Plan and usage** 显示订阅层级以及提供方声明的每一项额度,并带有计量条和重置时间:
124
+ Kimi 的滚动 5 小时窗口、每周窗口和包含额度(`GET /coding/v1/usages`);Grok 的每周
125
+ 包含额度(`cli-chat-proxy.grok.com/v1/billing?format=credits`,外加来自令牌自身
126
+ `tier` 声明的套餐层级);以及 Antigravity 的**四个**桶——Gemini 和 Claude,各含滚动
127
+ 与每周——读取自 `v1internal:retrieveUserQuotaSummary`,回退到 `fetchAvailableModels`
128
+ 的按模型 `quotaInfo`(每个家族中消耗最多的模型,按 id、显示名称和层级分类,因此一行
129
+ `Opus` 会被算作 Claude)。什么都不声明的提供方会如实说明,而不是编造一个数字。
130
+
131
+ 每条路由都以距离重置还有多远来表述重置:一小时以内用**分钟**(`40 分钟后重置`),
132
+ 一天以内用**小时**(`4 小时后重置`),再远则用日期(`重置时间 2026/10/17 13:54`)
133
+ ——读者在判断是否还有余量时依据的是这个计数,而当重置在一周之后时他们需要的是日期。
134
+ 由于页面以较慢的间隔重新读取这些数字,只要页面打开着,计数就会跟着更新。
135
+
136
+ **Manage** 打开一个对话框,其中有按账户的令牌刷新、凭证文件路径,以及解析后的端点。
137
+
138
+ 当设备授权处于待处理状态时,卡片会显示代码、其到期时间,以及打开或复制它的按钮,
139
+ 并且状态圆点变为琥珀色。回环类提供方(Grok)则显示一个登录链接,并等待浏览器重定向。
140
+
141
+ ## 安装
142
+
143
+ ```sh
144
+ dsh plugin --profile web add dsh-plugin-subscription-oauth
145
+ ```
146
+
147
+ `web` 是提供 Settings 页面的 profile;如果你运行的是 `desktop`(或任何其他 profile
148
+ 名称),就改用那一个。
149
+
150
+ CLI 会把它的参数转发给 pnpm,因此固定版本、git URL 和本地路径的写法都一样:
151
+
152
+ ```sh
153
+ dsh plugin --profile web add dsh-plugin-subscription-oauth@0.2.2
154
+ dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
155
+ dsh plugin --profile web add /path/to/dsh-plugin-subscription-oauth
156
+ ```
157
+
158
+ 之后重启 `dsh web`(或桌面应用),以便加载新的 bundle。本地目录以 `link:` 方式安装,
159
+ 因此之后在检出目录中执行 `npm run build` 就足以发布一项改动;再次重启即可生效。
160
+
161
+ 宿主包(`@deepseek-ai/dsh-llm` 及其余的包)是**可选** peer:harness 在运行时提供
162
+ 它们,因此安装本插件不会获取它正运行于其中的宿主的第二份副本。
163
+
164
+ ### 发布(维护者)
165
+
166
+ `prepublishOnly` 会重新构建 `lib/`——TypeScript 输出加上浏览器 bundle——因此发布的
167
+ tarball 绝不会带有过期的客户端:
168
+
169
+ ```sh
170
+ npm version patch # or minor/major
171
+ npm publish
172
+ ```
173
+
174
+ 对于只需要模型路由(不需要 Settings 页面)的无头组合:
175
+
176
+ ```sh
177
+ dsh --profile headless --patch /path/to/dsh-plugin-subscription-oauth/overlay.yml "your task"
178
+ ```
179
+
180
+ 其中 `overlay.yml` 是一个单条目补丁,指向
181
+ `/path/to/dsh-plugin-subscription-oauth/lib/index.js`。先从 web profile 登录一次
182
+ ——令牌文件是共享的。
183
+
184
+ ## 从选择器中隐藏内置提供方
185
+
186
+ 插件无法注销另一个 bundle 的路由(`ctx.llm` 没有注销 API),因此隐藏内置提供方是一项
187
+ profile 补丁改动。要隐藏 **DeepSeek API-key** 路由及其模型,同时保留 **DeepSeek
188
+ account**(登录)路由:
189
+
190
+ ```yaml
191
+ # $DSH_HOME/profiles/<profile>/cordis.patch.yml
192
+ - id: llm-deepseek
193
+ name: "@deepseek-ai/dsh-llm-deepseek-api-key"
194
+ config:
195
+ models: []
196
+ ```
197
+
198
+ `models` 会替换适配器的默认目录,因此 `deepseek-official` 报告没有模型,选择器会
199
+ 隐藏这个空组(`Empty groups are hidden`);该路由仍保持注册,所以已经保存了其某个
200
+ 模型 id 的会话继续正常工作。不要动 `llm-deepseek-account`,以保留账户路由。重启该
201
+ profile(或让 HMR 重新加载)以使改动生效。
202
+
203
+ ## 使用
204
+
205
+ 1. 打开 **Settings → Subscriptions**(订阅),在你想要的卡片上按 **Connect**。
206
+ 2. 完成卡片显示的登录:Kimi Code 以及(在被应答时)一个你在浏览器中批准的设备码;
207
+ Grok 和 Antigravity 会打开一个登录页面并等待重定向。当一次尝试处于待处理状态时,
208
+ 该区域会自行更新。
209
+ 3. 在任意会话中打开模型选择器,并选择该提供方下的一个模型——或者让它保持在
210
+ **Auto**,让已启用的模型按顺序被尝试。
211
+
212
+ 未登录?该提供方不会出现在选择器中,请求会以 `INVALID_CREDENTIAL` 失败并指向
213
+ Settings 页面。其他一切都不受影响。
214
+
215
+ 支持多个账户:再次连接即可添加另一个。★ 默认账户服务请求;使用
216
+ **Use by default** 来切换。
217
+
218
+ ## 配置
219
+
220
+ ```yaml
221
+ - id: llm-subscription-oauth
222
+ name: dsh-plugin-subscription-oauth
223
+ config:
224
+ providers: [kimi, grok, antigravity] # routes to register; all are default
225
+ enabled: true
226
+ discovery: true # consult each endpoint's live model list
227
+ streamIdleTimeoutMs: 300000 # abandon a silent stream after this
228
+ defaultModel: kimi-for-coding # model the picker preselects on the Kimi route
229
+ extraModels: [] # Kimi ids to publish without shipped metadata
230
+ # oauthHost: https://auth.kimi.com
231
+ # apiBase: https://api.kimi.com/coding/v1
232
+ ```
233
+
234
+ 只有 Kimi 路由接受 `oauthHost`/`apiBase`/`defaultModel`/`extraModels`:Grok 和
235
+ Antigravity 的端点由它们自己的客户端固定,其模型列表来自实时目录。从 `providers`
236
+ 中省略某条路由,只是让它保持未注册。
237
+
238
+ `oauthHost` 和 `apiBase` 也可以来自环境变量(`KIMI_CODE_OAUTH_HOST` /
239
+ `KIMI_OAUTH_HOST`、`KIMI_CODE_API_BASE` / `KIMI_API_BASE`);显式的配置值优先。
240
+ `apiBase` 既接受裸 base,也接受完整的 `.../responses` URL。
241
+
242
+ ## 工作原理
243
+
244
+ 该包是一个标准的 DeepSeek Harness 插件:`lib/index.js` 导出 `apply(ctx)`,它为每条
245
+ 路由注册一个 `ctx.llm` 适配器,并通过 `ctx.slots` 挂载 Settings 区域。
246
+ `lib/client.js` 是浏览器那一半——即该区域本身,由 harness 的 bundle 加载器加载。
247
+ harness 进程之外没有任何东西在运行,卸载该 bundle 会移除它添加的每一条路由。
248
+
249
+ **登录**是针对 `auth.kimi.com` 的 RFC 8628 设备授权授予。插件请求一个设备码,显示
250
+ 它,并以 `authorization_pending` / `slow_down` / `access_denied` / `expired_token`
251
+ 处理方式轮询令牌端点,直到浏览器批准。令牌按账户存储,以 Kimi 的 JWT 内部的
252
+ `user_id`(或 `sub`)声明为键。
253
+
254
+ **Grok** 用它自己的 CLI 所用的流程登录:在 `auth.x.ai` 进行 OIDC 发现,在回环回调
255
+ (`127.0.0.1:56121/callback`)上进行带 PKCE 的授权码交换,以及一个保持客户端身份的
256
+ 刷新授予。**Antigravity** 是 Google 的桌面 OAuth 客户端,位于 `127.0.0.1:51121`,
257
+ 其 scope 包括 `cloud-platform`;只有当 `loadCodeAssist` 报告该账户的 Cloud AI
258
+ Companion 项目后,登录才算完成,因为之后每次 `v1internal` 调用都以它为作用域。
259
+
260
+ **请求**携带三层身份:
261
+
262
+ * bearer 访问令牌,在到期前刷新(提前 5 分钟),每个账户共享一个进行中的刷新;
263
+ * Kimi 的 `X-Msh-*` 客户端身份(`KimiCLI/<version>`、平台、设备 id)——服务按这个
264
+ 家族接纳请求;
265
+ * harness 的归属标识。由于 Kimi 按 CLI 产品令牌接纳请求,而 harness 要求在每个
266
+ 提供方请求上带有它自己的归属标识,两者被合并为一个 RFC 9110 `User-Agent` 值,
267
+ 而不是作为两个互相竞争的头字段发送。
268
+
269
+ **传输**采用 OpenAI Responses:`instructions` + `input` 条目,`function_call` /
270
+ `function_call_output` 保持相邻(端点要求如此),`stream: true`、`store: false`,
271
+ `prompt_cache_key` 取自会话 id 以获得缓存亲和性,`reasoning.effort` 由 harness 的
272
+ effort id 映射而来。已存储的推理绝不作为文本重放——端点会在服务端保留订阅账户的
273
+ 思考内容并加密,因此每一轮都立足于它自己可见的内容。
274
+
275
+ Kimi 在订阅编码模型上锁定温度和采样惩罚,因此调用方提供的 `temperature` 会被丢弃,
276
+ 而不是在上游被拒绝。
277
+
278
+ ## 故障排除
279
+
280
+ | 症状 | 原因 |
281
+ | --- | --- |
282
+ | `<provider> login expired; sign in again…` | 刷新令牌被拒绝;再次连接 |
283
+ | 选择器中没有模型 | 未登录,或该路由没有被重新宣告——检查 **Settings → Subscriptions** |
284
+ | Antigravity 完全没有模型 | 该账户的 `fetchAvailableModels` 什么也没返回;该路由不附带精选列表,所以登录后按 **Refresh** |
285
+ | `model not found` | 该 id 不在你的套餐所提供的范围内;使用 **Refresh** |
286
+ | 代码卡片一直不消失 | 该尝试仍在待处理或已过期;按 **Cancel** 并再次连接 |
287
+ | `model discovery failed … using the built-in catalog` | 实时列表无法访问;随附的目录仍然可用(Kimi 和 Grok 附带一个,Antigravity 没有) |
288
+ | 行无法拖动 | 该布局在 Settings 中被关闭,或拖动是从 checkbox 上开始的——抓住 ⠿ 抓手 |
289
+
290
+ ## 安全说明
291
+
292
+ ### 它存储什么,以及存在哪里
293
+
294
+ | 路径 | 内容 | 模式 |
295
+ | --- | --- | --- |
296
+ | `~/.dsh/plugins/kimi-oauth/auth.json` | 每个提供方一个区段:访问令牌、刷新令牌、到期时间、账户 id、套餐 | `0600`,原子写入 |
297
+ | `~/.dsh/plugins/kimi-oauth/preferences.json` | 模型可见性、你的排序,以及 Auto 的链——没有秘密 | `0600` |
298
+ | `~/.dsh/plugins/kimi-oauth/kimi-device-id` | Kimi 的 API 所期望的稳定设备 id | `0600` |
299
+
300
+ 该目录保留了本插件在只服务 Kimi Code 时所用的名称:它保存着**活跃的登录信息**,
301
+ 所以改名会让所有人退出登录。`~/.dsh` 之外不存储任何东西,除提供方自身之外也不向
302
+ 任何地方上传任何东西。
303
+
304
+ ### 它与什么通信
305
+
306
+ | 路由 | 联系的主机 | 原因 |
307
+ | --- | --- | --- |
308
+ | `kimi` | `auth.kimi.com`、`api.kimi.com` | 设备流登录,然后是编码 API |
309
+ | `grok` | `auth.x.ai`、`api.x.ai`、`cli-chat-proxy.grok.com` | OIDC 登录、Responses API、套餐的额度数字 |
310
+ | `antigravity` | `accounts.google.com`、`oauth2.googleapis.com`、`cloudcode-pa.googleapis.com` | Google 登录、账户的项目,然后是 Cloud Code Assist(包括其配额核算) |
311
+ | `glm` | `bigmodel.cn`、`zcode.z.ai`、`open.bigmodel.cn` | 由 ZCode 中介的浏览器登录,然后是编码套餐 API 及其额度 |
312
+ | `ark` | `signin.volcengine.com`、`ark.cn-beijing.volces.com`、`ark.cn-beijing.volcengineapi.com` | 火山引擎 SSO,然后是编码套餐 API(数据面)与签名的控制面——后者用于铸造 API Key 并读取额度 |
313
+
314
+ 没有分析、没有更新 ping,也不经由任何其他人的服务器中转:每个请求都发往拥有该账户
315
+ 的主机。任何请求体、提示词或令牌都不会被记录到日志。
316
+
317
+ ### 值得了解的边界
318
+
319
+ * 令牌文件以模式 `0600` 写入;UI 中只显示账户 id、邮箱、主机和设备 id——绝不显示
320
+ 令牌。
321
+ * Kimi 和 GLM 的流程完全不需要在本机上监听:Kimi 采用轮询,而 GLM 的浏览器回调由
322
+ 提供方在服务端完成。Grok 和 Antigravity **仅**在一次登录尝试处于待处理状态时持有
323
+ 一个回环监听器,位于 `127.0.0.1:56121` 和 `127.0.0.1:51121`,并且一旦该尝试结束
324
+ 或失效就会关闭。回环目的地绝不经过代理。
325
+ * 出站请求遵循 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` 和 `NO_PROXY`。
326
+ * 登录是授权本机使用该账户——提供方自己的同意页面就是同意步骤,它绝不会替你完成。
327
+ * 插件无法注销另一个 bundle 的提供方(`ctx.llm` 没有注销 API),因此本插件绝不隐藏
328
+ 你原本就有的任何东西;如果那正是你想要的,请参见
329
+ [从选择器中隐藏内置提供方](#从选择器中隐藏内置提供方)。
330
+ * **已知限制**:按设计不支持 OpenAI 兼容的 API 密钥(这是一个订阅类插件);GLM 的
331
+ 流程没有刷新授予,因此失效的登录会要求你重新登录,而不是静默续期;Antigravity
332
+ 不附带精选模型列表,并且依赖其端点作出应答。
333
+
334
+ ## 开发
335
+
336
+ ```sh
337
+ npm install # development baseline: published DSH 0.2.0-rc.2 packages
338
+ npm run build # tsc (lib/) + tsdown (lib/client.js browser bundle)
339
+ npm test # node --test over the compiled unit specs
340
+ npm run typecheck # tsc --noEmit
341
+ ```
342
+
343
+ `tools/probe-kimi-oauth.mjs` 执行一次实时检查,确认 Kimi 接受随附的客户端 id 和身份
344
+ 头;它会启动一次设备授权并立即放弃,因此不会触碰任何账户(先执行
345
+ `npm run build`)。
346
+
347
+ ### 在没有宿主的情况下审阅设置 UI
348
+
349
+ `tools/preview-entry.tsx` 用一个打桩的 RPC 调用方挂载真实的区域,因此一项设计改动
350
+ 可以单独截图。查询标志驱动各种状态:`?dark=1`、`?grok=1`、`?ag=1`、`?busy=1`、
351
+ `?hide=1`、`?collapse=1`、`?manage=1`。
352
+
353
+ ```sh
354
+ npm run preview # bundles probe/preview.js
355
+ # then open probe/preview.html in a browser, or screenshot it headlessly:
356
+ msedge --headless=new --user-data-dir=%TEMP%\shot --window-size=620,1900 \
357
+ --screenshot=shot.png "file:///<checkout>/probe/preview.html?dark=1&grok=1&ag=1"
358
+ ```
359
+
360
+ harness 有意不为自己的任何 `button` 设置样式:外壳通过一个插件标记不携带的 CSS 类
361
+ 为其控件提供外观,因此一个会装饰裸按钮的预览恰恰会掩盖这个页面曾有的那个回归。
362
+
363
+ 查询标志:`dark=1`(深色主题)、`compact=1`(模型列表折叠)、`busy=1`(设备授权
364
+ 待处理)、`manage=1`(管理对话框打开)。`probe/` 只保存生成的预览输出,并且被
365
+ gitignore;提交的截图位于 `docs/images/`。
366
+
367
+ 在 `npm run build` 之后,重启 `dsh web` 以应用改动。
368
+
369
+ ## License
370
+
371
+ MIT
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The Ark control plane: STS credentials, request signing, and allowance reads.
3
+ *
4
+ * Ark splits its API in two. The **data plane** (`ark.cn-beijing.volces.com`)
5
+ * takes a plan API key as a plain bearer. The **control plane**
6
+ * (`ark.cn-beijing.volcengineapi.com`) is Volcengine OpenAPI: every call is
7
+ * `POST /?Action=<name>&Version=<date>` signed with the temporary AK/SK the SSO
8
+ * identity was exchanged for, and the plan's allowance lives only there.
9
+ *
10
+ * The signing below was written from the official CLI's own algorithm and then
11
+ * checked against the live service with real credentials, which answered 200 with
12
+ * the same body the CLI reports.
13
+ *
14
+ * @module dsh-plugin-subscription-oauth/auth/ark-control
15
+ */
16
+ import type { FetchFn } from '../http.js';
17
+ /** Control-plane host: the OpenAPI endpoint, not the data-plane one. */
18
+ export declare const ARK_CONTROL_HOST = "ark.cn-beijing.volcengineapi.com";
19
+ /** Region every Ark OpenAPI action is scoped to. */
20
+ export declare const ARK_CONTROL_REGION = "cn-beijing";
21
+ /** Service name in the signing scope. */
22
+ export declare const ARK_CONTROL_SERVICE = "ark";
23
+ /** Version every Ark OpenAPI action takes. */
24
+ export declare const ARK_CONTROL_VERSION = "2024-01-01";
25
+ /** Temporary credentials the SSO identity is exchanged for. */
26
+ export interface ArkStsCredentials {
27
+ /** Access key id, `AKT…` for a session credential. */
28
+ readonly accessKeyId: string;
29
+ /** Secret access key. */
30
+ readonly secretAccessKey: string;
31
+ /** Session token that must accompany the signature. */
32
+ readonly sessionToken: string;
33
+ /** Epoch milliseconds the credentials lapse. */
34
+ readonly expiresAt: number;
35
+ }
36
+ /**
37
+ * Sign one OpenAPI request the way Volcengine expects.
38
+ *
39
+ * The canonical request lists `content-type;host;x-content-sha256;x-date`, which is
40
+ * the set the official client signs for these actions.
41
+ *
42
+ * @param input - method, query, body, and the credentials to sign with.
43
+ * @returns the headers to send.
44
+ */
45
+ export declare function signArkRequest(input: {
46
+ method: 'GET' | 'POST';
47
+ query: string;
48
+ body: string;
49
+ credentials: ArkStsCredentials;
50
+ /** Body media type; the control plane accepts form and JSON, and signs whichever is sent. */
51
+ contentType?: string;
52
+ at?: Date;
53
+ }): Record<string, string>;
54
+ /** Call one control-plane action. */
55
+ export declare function callArkControl<T>(input: {
56
+ action: string;
57
+ credentials: ArkStsCredentials;
58
+ fetchFn: FetchFn;
59
+ body?: string;
60
+ /**
61
+ * Body media type.
62
+ *
63
+ * Actions that take nested structures (creating an API key, for instance) only
64
+ * parse a JSON body; the flat actions accept the form encoding.
65
+ */
66
+ contentType?: string;
67
+ signal?: AbortSignal;
68
+ }): Promise<T>;
69
+ /**
70
+ * Mint one ordinary API key for the account.
71
+ *
72
+ * The data plane refuses STS credentials (`403 Unsupported sts authentication
73
+ * method`) and takes only an API key, so this is the step that turns a sign-in into
74
+ * a usable credential. The shape is not guesswork — it was learned from the
75
+ * service's own validation, which reports each missing parameter in turn:
76
+ * `Name`, then `ProjectName`, then `ResourceInstances`, then
77
+ * `AccessControlInfo.AllowAll`. `ResourceInstances` must be a non-empty array (an
78
+ * empty one is rejected as invalid, while one empty object is accepted), and the
79
+ * body has to be JSON, because a nested structure is not expressible in the form
80
+ * encoding the flat actions use.
81
+ *
82
+ * The reply carries only an `Id`; the key itself comes from {@link readArkApiKey}.
83
+ *
84
+ * @param input - action inputs plus the credentials to sign with.
85
+ * @returns the new key's id.
86
+ * @throws {Error} when the control plane refuses.
87
+ */
88
+ export declare function createArkApiKey(input: {
89
+ credentials: ArkStsCredentials;
90
+ fetchFn: FetchFn;
91
+ name: string;
92
+ project?: string;
93
+ signal?: AbortSignal;
94
+ }): Promise<number>;
95
+ /**
96
+ * Read one key's value.
97
+ *
98
+ * @param input - the key's id and the credentials to sign with.
99
+ * @returns the key, as the data plane expects it.
100
+ * @throws {Error} when the control plane refuses or states no key.
101
+ */
102
+ export declare function readArkApiKey(input: {
103
+ credentials: ArkStsCredentials;
104
+ fetchFn: FetchFn;
105
+ id: number;
106
+ project?: string;
107
+ signal?: AbortSignal;
108
+ }): Promise<string>;
109
+ /**
110
+ * Every key the account already has, newest first as the service lists them.
111
+ *
112
+ * @param input - the credentials to sign with.
113
+ * @returns the keys' ids and names.
114
+ */
115
+ export declare function listArkApiKeys(input: {
116
+ credentials: ArkStsCredentials;
117
+ fetchFn: FetchFn;
118
+ project?: string;
119
+ signal?: AbortSignal;
120
+ }): Promise<readonly {
121
+ id: number;
122
+ name?: string;
123
+ status?: string;
124
+ }[]>;
125
+ /** One model's free inference allowance, as `ListModelChargeItems` reports it. */
126
+ export interface ArkFreeUsage {
127
+ /** Model id the allowance belongs to. */
128
+ readonly model: string;
129
+ /** Tokens already spent. */
130
+ readonly consumed: number;
131
+ /** Tokens granted. */
132
+ readonly total: number;
133
+ }
134
+ /**
135
+ * Read every model's free inference allowance.
136
+ *
137
+ * This is the allowance that exists without a subscription — the plan quota is a
138
+ * separate action that reports `Status: Reclaimed` for an account with no plan.
139
+ * Free usage is per model, and the free-usage field is omitted for models that have
140
+ * none, so those are skipped rather than reported as zero.
141
+ *
142
+ * @param payload - the decoded `ListModelChargeItems` body.
143
+ * @returns one entry per model that states a free allowance.
144
+ */
145
+ export declare function parseArkFreeUsage(payload: unknown): ArkFreeUsage[];
146
+ /** One metered period of a paid plan, as the CLI documents it. */
147
+ export interface ArkPeriod {
148
+ /** `5h`, `weekly`, `monthly`, or `session` for the team edition. */
149
+ readonly label: string;
150
+ /** Units already spent. */
151
+ readonly used?: number;
152
+ /** Units granted. */
153
+ readonly total?: number;
154
+ /** Percent spent, when the service states it. */
155
+ readonly percent?: number;
156
+ /** Epoch milliseconds the period resets. */
157
+ readonly resetsAt?: number;
158
+ }
159
+ /** A plan's state and its metered periods. */
160
+ export interface ArkPlanUsage {
161
+ /** Whether the account holds the plan at all. */
162
+ readonly subscribed: boolean;
163
+ /** Plan state as the service names it, e.g. `Reclaimed`. */
164
+ readonly status?: string;
165
+ /** The metered periods, in the order the service listed them. */
166
+ readonly periods: readonly ArkPeriod[];
167
+ }
168
+ /**
169
+ * Read a plan's allowance out of a `GetCodingPlanUsage` / `GetAFPUsage` body.
170
+ *
171
+ * The service nests the metered periods under `coding_usage` (Coding Plan) or
172
+ * `afp_usage` (Agent Plan); each period is
173
+ * `{ label, used, total, percent, reset_at }`, which the CLI's own help documents
174
+ * with a worked example. An account with no plan answers `Status: Reclaimed` and no
175
+ * periods, which is a state to report rather than an error.
176
+ *
177
+ * @param payload - the decoded control-plane body.
178
+ * @returns the plan state and its periods.
179
+ */
180
+ export declare function parseArkPlanUsage(payload: unknown): ArkPlanUsage;