@hystudio/dsh-quota-pill 0.1.1 → 0.2.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/AGENTS.md +8 -1
- package/README.md +15 -0
- package/client.js +35 -4
- package/package.json +1 -1
- package/src/adapters-deepseek.js +230 -0
- package/src/credentials.js +35 -0
- package/src/server.js +6 -1
package/AGENTS.md
CHANGED
|
@@ -30,9 +30,15 @@ There are **two credential families**, and this matters for every step:
|
|
|
30
30
|
|
|
31
31
|
| Family | Providers | What the user needs |
|
|
32
32
|
|---|---|---|
|
|
33
|
+
| DSH Platform sign-in | `deepseek` | **nothing** — works if they are signed in to DeepSeek Platform in DSH |
|
|
33
34
|
| API key | `kimi`, `opencode`, `commandcode` | a key pasted into `~/.dsh/.credentials.yaml` |
|
|
34
35
|
| OAuth via CLIProxyAPI | `xai`, `codex`, `agy` | **CLIProxyAPI installed and logged in once** |
|
|
35
36
|
|
|
37
|
+
**Check DeepSeek first.** It is the official account and needs no setup beyond a
|
|
38
|
+
DSH Platform sign-in, so it is usually the one the user actually wants. If it
|
|
39
|
+
reports `none` with "not signed in to DeepSeek Platform", tell them to use the
|
|
40
|
+
DSH account menu, then re-run step 5.
|
|
41
|
+
|
|
36
42
|
---
|
|
37
43
|
|
|
38
44
|
## 1. Verify the sidecar runs
|
|
@@ -81,6 +87,7 @@ Do **not** guess. Ask, in one message, listing the options with what each needs:
|
|
|
81
87
|
|
|
82
88
|
> I can read quota for six providers. Which do you have?
|
|
83
89
|
>
|
|
90
|
+
> - **DeepSeek** — works already if you are signed in to DeepSeek Platform in DSH
|
|
84
91
|
> - **Kimi Coding** — needs a `KIMI_CODING_API_KEY`
|
|
85
92
|
> - **OpenCode Go** — needs an `OPENCODE_GO_API_KEY`
|
|
86
93
|
> - **Command Code** — needs a `COMMANDCODE_API_KEY`
|
|
@@ -170,7 +177,7 @@ For each provider look at `kind`:
|
|
|
170
177
|
| `subscription-quota` | working — `remainingPct` and `windows` are populated |
|
|
171
178
|
| `none` | not configured — the `error` field says exactly what to add |
|
|
172
179
|
| `probe-failed` | configured but the upstream call failed — see `error` |
|
|
173
|
-
| `payg` | metered
|
|
180
|
+
| `payg` | prepaid/metered — no ceiling, so no percentage. If `credits.remain` is set, that is the balance to report |
|
|
174
181
|
|
|
175
182
|
Cross-check one provider against its official dashboard if the human is unsure.
|
|
176
183
|
**The plugin reports remaining quota, not used.**
|
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ into the live number.
|
|
|
15
15
|
|
|
16
16
|
| Provider | Credential |
|
|
17
17
|
|---|---|
|
|
18
|
+
| **DeepSeek** (official) | the Platform sign-in DSH already holds — nothing to add |
|
|
18
19
|
| **Kimi Coding** | `refs.KIMI_CODING_API_KEY` in `~/.dsh/.credentials.yaml` |
|
|
19
20
|
| **OpenCode Go** | `refs.OPENCODE_GO_API_KEY` |
|
|
20
21
|
| **Command Code** | `refs.COMMANDCODE_API_KEY` |
|
|
@@ -22,6 +23,9 @@ into the live number.
|
|
|
22
23
|
| **Codex (ChatGPT Plus)** | same — and CLIProxyAPI must be **running** |
|
|
23
24
|
| **Antigravity** | same |
|
|
24
25
|
|
|
26
|
+
**DeepSeek works with no setup at all** if you are signed in to DeepSeek Platform
|
|
27
|
+
in DSH — that is the official account, and it is the one most users care about.
|
|
28
|
+
|
|
25
29
|
The three API-key providers need nothing but the key. The three OAuth providers
|
|
26
30
|
need CLIProxyAPI installed and logged in, because that is what holds the tokens;
|
|
27
31
|
this plugin only reads them and never runs an OAuth flow itself.
|
|
@@ -39,6 +43,9 @@ dsh plugin add @hystudio/dsh-quota-pill
|
|
|
39
43
|
npx @hystudio/dsh-quota-pill
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
> The npm package is `@hystudio/...` while the repo lives at `598934321/...` —
|
|
47
|
+
> they are the same author under two account names.
|
|
48
|
+
|
|
42
49
|
The pill shows **"quota service is not running"** with that exact command until
|
|
43
50
|
the sidecar is up. Keep it running in the background — see
|
|
44
51
|
[Running it as a service](#running-it-as-a-service).
|
|
@@ -183,6 +190,14 @@ would flicker between models you do not even have configured.
|
|
|
183
190
|
`monthlyCap = weekly.cap × 2`, an identity verified against two different plans
|
|
184
191
|
(31.6 + 38.4 = 70 with `weekly.cap` 35; 0.225 + 9.775 = 10 with `weekly.cap` 6).
|
|
185
192
|
|
|
193
|
+
**DeepSeek** is a prepaid CNY wallet, so there is no percentage to show — the pill
|
|
194
|
+
shows the balance and, separately, any bonus credit. Two traps worth recording:
|
|
195
|
+
the stored credential is a *Platform login grant*, not an `api.deepseek.com` API
|
|
196
|
+
key (sending it there returns 401), and calling `platform.deepseek.com` directly
|
|
197
|
+
returns 429 from Cloudflare. The balance is therefore read through DSH's own
|
|
198
|
+
`account/getBalance` RPC, which already holds the grant and projects the response
|
|
199
|
+
down to wallet currency and balance. We never handle the Platform token ourselves.
|
|
200
|
+
|
|
186
201
|
## Privacy
|
|
187
202
|
|
|
188
203
|
- Credentials are read from disk, used for the upstream call, and never logged.
|
package/client.js
CHANGED
|
@@ -221,6 +221,8 @@ window.__ModuleLoader__.load({
|
|
|
221
221
|
spendUnit: (req, tok) => [`${req} 次 · ${tok}`, `${req} reqs · ${tok}`],
|
|
222
222
|
poolStatus: ['状态未知', 'Status unknown'],
|
|
223
223
|
noSource: ['无数据源', 'No source'],
|
|
224
|
+
balanceLeft: ['余额剩余', 'Balance left'],
|
|
225
|
+
bonusBalance: ['赠金余额', 'Bonus balance'],
|
|
224
226
|
offlineTitle: ['额度服务未启动', 'Quota service is not running'],
|
|
225
227
|
offlineHint: ['在本机运行下面的命令即可(保持后台)', 'Start it locally in the background:'],
|
|
226
228
|
creditsAll: ['全部积分', 'All credits'],
|
|
@@ -408,7 +410,15 @@ window.__ModuleLoader__.load({
|
|
|
408
410
|
}
|
|
409
411
|
// v.label 来自 adapter,本身就是双语无关的短词,优先用它。
|
|
410
412
|
if (kind === 'free-usable') return { text: v.label || pick(lang, 'free'), tone: 'ok', pct: null };
|
|
411
|
-
if (kind === 'payg')
|
|
413
|
+
if (kind === 'payg') {
|
|
414
|
+
// Prepaid wallets (e.g. the official DeepSeek account) have a real
|
|
415
|
+
// balance to show. Only fall back to the label when there is no amount.
|
|
416
|
+
const c = v.credits;
|
|
417
|
+
if (c && typeof c.remain === 'number') {
|
|
418
|
+
return { text: money(c.remain, c.unit), tone: 'ok', pct: null };
|
|
419
|
+
}
|
|
420
|
+
return { text: v.label || pick(lang, 'payg'), tone: 'muted', pct: null };
|
|
421
|
+
}
|
|
412
422
|
// `none` = 这个 provider 当前取不到任何读数(渠道被删、插件被卸、没凭据)。
|
|
413
423
|
// 之前没有这个分支,会掉到最后的 String(kind),界面上直接显示字面量 "none"。
|
|
414
424
|
if (kind === 'none') {
|
|
@@ -979,13 +989,23 @@ window.__ModuleLoader__.load({
|
|
|
979
989
|
}
|
|
980
990
|
|
|
981
991
|
function paygBody(data, lang) {
|
|
992
|
+
const c = data && data.credits;
|
|
993
|
+
const hasBalance = c && typeof c.remain === 'number';
|
|
982
994
|
return h(React.Fragment, null,
|
|
983
995
|
h('div', { className: 'quota-pill-header' },
|
|
984
|
-
h('span', { className: 'quota-pill-headline' },
|
|
985
|
-
|
|
996
|
+
h('span', { className: 'quota-pill-headline' },
|
|
997
|
+
hasBalance ? pick(lang, 'balanceLeft') : ((data && data.label) ? String(data.label) : pick(lang, 'payg'))),
|
|
998
|
+
hasBalance
|
|
999
|
+
? h('span', { className: 'quota-pill-figures' }, money(c.remain, c.unit))
|
|
1000
|
+
: h('span', { className: 'quota-pill-figures' }, (data && data.note) ? String(data.note) : pick(lang, 'noCeiling')),
|
|
986
1001
|
),
|
|
987
1002
|
h('dl', { className: 'quota-pill-rows' },
|
|
988
|
-
|
|
1003
|
+
// A prepaid wallet's bonus is worth showing separately: it is spendable
|
|
1004
|
+
// but not the same money, and mixing it into the balance would overstate.
|
|
1005
|
+
data.bonus && typeof data.bonus.amount === 'number'
|
|
1006
|
+
? statRow(pick(lang, 'bonusBalance'), money(data.bonus.amount, data.bonus.unit))
|
|
1007
|
+
: null,
|
|
1008
|
+
!hasBalance && data.account && statRow(pick(lang, 'account'), String(data.account)),
|
|
989
1009
|
data.usedAmount != null && statRow(pick(lang, 'usedThisPeriod'), String(data.usedAmount) + pick(lang, 'usedRaw')),
|
|
990
1010
|
data.monthlyLimit != null && statRow(pick(lang, 'monthlyCap'), String(data.monthlyLimit)),
|
|
991
1011
|
data.resetAt && statRow(pick(lang, 'nextReset'), String(data.resetAt).slice(0, 10)),
|
|
@@ -1092,6 +1112,17 @@ window.__ModuleLoader__.load({
|
|
|
1092
1112
|
}
|
|
1093
1113
|
|
|
1094
1114
|
/** 到期时间:积分包给的是「哪天到期」,用绝对日期比「X 天后重置」贴切。 */
|
|
1115
|
+
/**
|
|
1116
|
+
* Currency amount. Uses a symbol for the few currencies we may see and a
|
|
1117
|
+
* bare code otherwise, so we never imply a currency we did not get.
|
|
1118
|
+
*/
|
|
1119
|
+
function money(amount, unit) {
|
|
1120
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount)) return '';
|
|
1121
|
+
const sym = { CNY: '\u00a5', USD: '$', EUR: '\u20ac' }[String(unit || '').toUpperCase()];
|
|
1122
|
+
const n = Math.abs(amount) >= 100 ? amount.toFixed(1) : amount.toFixed(2);
|
|
1123
|
+
return sym ? sym + n : n + (unit ? ' ' + unit : '');
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1095
1126
|
function fmtExpiry(iso, lang) {
|
|
1096
1127
|
if (!iso) return '';
|
|
1097
1128
|
const d = new Date(iso);
|
package/package.json
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Platform (the official account).
|
|
3
|
+
*
|
|
4
|
+
* The credential in ~/.dsh/.credentials.yaml is a **Platform login grant**, not
|
|
5
|
+
* an api.deepseek.com API key. Two consequences, both verified:
|
|
6
|
+
*
|
|
7
|
+
* 1. Sending it to api.deepseek.com/user/balance returns 401 — those are
|
|
8
|
+
* different systems.
|
|
9
|
+
* 2. Calling platform.deepseek.com/api/v0/users/get_user_summary directly
|
|
10
|
+
* returns 429 from Cloudflare (non-browser TLS).
|
|
11
|
+
*
|
|
12
|
+
* So the balance is read through DSH's own `account/getBalance` RPC, which
|
|
13
|
+
* already holds the grant, derives the right headers, and projects the answer
|
|
14
|
+
* down to just wallet currency + balance. That is also the most conservative
|
|
15
|
+
* route: we never handle a Platform token ourselves.
|
|
16
|
+
*
|
|
17
|
+
* The RPC needs a signed session cookie:
|
|
18
|
+
* name = "dsh-auth-" + base64url(sha256(authority))
|
|
19
|
+
* value = "v1." + base64url(JSON payload) + "." + base64url(HMAC-SHA256(secret, body))
|
|
20
|
+
* payload = { version: 1, authority, issuedAt, expiresAt } ← milliseconds
|
|
21
|
+
*/
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { URL } from 'node:url';
|
|
25
|
+
import { deepseekGrant, browserSessionSecret } from './credentials.js';
|
|
26
|
+
|
|
27
|
+
const RPC_PORT_GUESSES = [8319, 8080, 3000];
|
|
28
|
+
const DSH_BASE_ENV = process.env.DSH_RPC_BASE || process.env.QUOTA_PILL_DSH_BASE || null;
|
|
29
|
+
|
|
30
|
+
const b64u = (buf) => Buffer.from(buf).toString('base64url');
|
|
31
|
+
|
|
32
|
+
/** Find the DSH web server port. macOS/Linux: ask lsof; otherwise try env then guesses. */
|
|
33
|
+
function findDshBase() {
|
|
34
|
+
if (DSH_BASE_ENV) return DSH_BASE_ENV.replace(/\/$/, '');
|
|
35
|
+
if (process.env.QUOTA_PILL_DSH_PORT) {
|
|
36
|
+
return `http://127.0.0.1:${process.env.QUOTA_PILL_DSH_PORT}`;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const out = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN'], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: 4000,
|
|
42
|
+
});
|
|
43
|
+
for (const line of out.split('\n')) {
|
|
44
|
+
// The DSH process shows up under its app name; match on the command column.
|
|
45
|
+
if (!/DeepSeek|dsh/i.test(line)) continue;
|
|
46
|
+
const m = /127\.0\.0\.1:(\d+)/.exec(line);
|
|
47
|
+
if (m) return `http://127.0.0.1:${m[1]}`;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
/* lsof missing or blocked — fall through */
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build the signed session cookie DSH expects. */
|
|
56
|
+
function signCookie(base, secretB64) {
|
|
57
|
+
const authority = new URL(base).host;
|
|
58
|
+
let raw;
|
|
59
|
+
try {
|
|
60
|
+
raw = Buffer.from(secretB64, 'base64url');
|
|
61
|
+
} catch {
|
|
62
|
+
raw = Buffer.from(secretB64, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
const name = 'dsh-auth-' + b64u(crypto.createHash('sha256').update(authority).digest());
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const payload = {
|
|
67
|
+
version: 1,
|
|
68
|
+
authority,
|
|
69
|
+
issuedAt: now,
|
|
70
|
+
expiresAt: now + 3600 * 1000,
|
|
71
|
+
};
|
|
72
|
+
const body = b64u(JSON.stringify(payload));
|
|
73
|
+
const sig = b64u(crypto.createHmac('sha256', raw).update(body).digest());
|
|
74
|
+
return { name, value: `v1.${body}.${sig}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** POST one RPC to DSH and return its result payload. */
|
|
78
|
+
async function dshRpc(method, args, base) {
|
|
79
|
+
const secret = browserSessionSecret();
|
|
80
|
+
if (!secret) return { ok: false, error: 'no browser-session secret in credentials' };
|
|
81
|
+
|
|
82
|
+
const { name, value } = signCookie(base, secret);
|
|
83
|
+
const body = JSON.stringify({
|
|
84
|
+
type: 'client-request',
|
|
85
|
+
rpcId: 'quota-pill-' + method,
|
|
86
|
+
method,
|
|
87
|
+
payload: { args },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(base + '/api/' + method.replace(/\./g, '/'), {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
Accept: 'application/json',
|
|
98
|
+
Cookie: `${name}=${value}`,
|
|
99
|
+
},
|
|
100
|
+
body,
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
const text = await res.text();
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
return { ok: false, status: res.status, error: `dsh rpc http ${res.status}: ${text.slice(0, 120)}` };
|
|
106
|
+
}
|
|
107
|
+
let env;
|
|
108
|
+
try {
|
|
109
|
+
env = JSON.parse(text);
|
|
110
|
+
} catch {
|
|
111
|
+
return { ok: false, error: 'dsh rpc returned unparsable json' };
|
|
112
|
+
}
|
|
113
|
+
const result = (env && env.result) || {};
|
|
114
|
+
if (result.ok === false) {
|
|
115
|
+
const e = result.error || {};
|
|
116
|
+
return { ok: false, error: `${e.code || 'rpc-failed'}: ${e.message || ''}`.trim() };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, value: result.value };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Official DeepSeek balance.
|
|
128
|
+
*
|
|
129
|
+
* Reported as a `payg` reading: it is a prepaid CNY wallet, not a percentage
|
|
130
|
+
* against a ceiling. `remainingPct` is deliberately null — inventing a
|
|
131
|
+
* percentage from a balance we cannot divide by anything would be a fake number.
|
|
132
|
+
*/
|
|
133
|
+
export async function deepseek() {
|
|
134
|
+
const grant = deepseekGrant();
|
|
135
|
+
if (!grant) {
|
|
136
|
+
return {
|
|
137
|
+
kind: 'none',
|
|
138
|
+
provider: 'deepseek',
|
|
139
|
+
error: 'not signed in to DeepSeek Platform '
|
|
140
|
+
+ '(no deepseek-account-platform/default grant in ~/.dsh/.credentials.yaml)',
|
|
141
|
+
setup: { kind: 'dsh-account', hint: 'Sign in to DeepSeek Platform from the DSH account menu.' },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const base = findDshBase();
|
|
146
|
+
if (!base) {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'none',
|
|
149
|
+
provider: 'deepseek',
|
|
150
|
+
error: 'cannot locate the DSH web server (needed to read Platform balance)',
|
|
151
|
+
setup: { kind: 'dsh-account', hint: 'Set QUOTA_PILL_DSH_PORT or DSH_RPC_BASE.' },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The client metadata identifies the calling UI. `version` is a string here,
|
|
156
|
+
// not a number — the strict wire codec rejects a numeric version.
|
|
157
|
+
const res = await dshRpc('account/getBalance', {
|
|
158
|
+
client: { version: '1', locale: 'zh-CN', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 },
|
|
159
|
+
}, base);
|
|
160
|
+
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
return {
|
|
163
|
+
kind: 'probe-failed',
|
|
164
|
+
provider: 'deepseek',
|
|
165
|
+
error: res.error,
|
|
166
|
+
setup: { kind: 'dsh-account' },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const value = res.value;
|
|
171
|
+
// DSH returns {status:'ready', value:[{currency,balance}], bonusWallets:[...]}
|
|
172
|
+
if (!value || value.status !== 'ready') {
|
|
173
|
+
return {
|
|
174
|
+
kind: 'none',
|
|
175
|
+
provider: 'deepseek',
|
|
176
|
+
error: 'Platform balance unavailable (status: '
|
|
177
|
+
+ ((value && value.status) || 'unknown') + ')',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const wallets = Array.isArray(value.value) ? value.value : [];
|
|
182
|
+
const bonus = Array.isArray(value.bonusWallets) ? value.bonusWallets : [];
|
|
183
|
+
if (!wallets.length && !bonus.length) {
|
|
184
|
+
return { kind: 'none', provider: 'deepseek', error: 'Platform returned no wallets' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const primary = wallets[0] || bonus[0] || {};
|
|
188
|
+
const toNum = (v) => {
|
|
189
|
+
const n = Number(v);
|
|
190
|
+
return Number.isFinite(n) ? n : null;
|
|
191
|
+
};
|
|
192
|
+
const currency = primary.currency || 'CNY';
|
|
193
|
+
const balance = toNum(primary.balance);
|
|
194
|
+
const bonusTotal = bonus.reduce((sum, w) => {
|
|
195
|
+
const n = toNum(w && w.balance);
|
|
196
|
+
return n == null ? sum : sum + n;
|
|
197
|
+
}, 0);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
kind: 'payg',
|
|
201
|
+
provider: 'deepseek',
|
|
202
|
+
account: currency,
|
|
203
|
+
label: 'prepaid',
|
|
204
|
+
note: `prepaid balance ${balance == null ? '?' : balance.toFixed(2)} ${currency}`
|
|
205
|
+
+ (bonusTotal > 0 ? ` (+${bonusTotal.toFixed(2)} bonus)` : ''),
|
|
206
|
+
remainingPct: null,
|
|
207
|
+
usedPct: null,
|
|
208
|
+
// Amounts are kept separately so the UI can show a currency figure.
|
|
209
|
+
credits: {
|
|
210
|
+
remain: balance,
|
|
211
|
+
total: null,
|
|
212
|
+
unit: currency,
|
|
213
|
+
},
|
|
214
|
+
windows: balance == null ? [] : [{
|
|
215
|
+
key: 'balance',
|
|
216
|
+
remainPct: null,
|
|
217
|
+
amount: balance,
|
|
218
|
+
unit: currency,
|
|
219
|
+
resetAt: null,
|
|
220
|
+
}],
|
|
221
|
+
bonus: bonusTotal > 0
|
|
222
|
+
? { amount: bonusTotal, unit: currency }
|
|
223
|
+
: null,
|
|
224
|
+
raw: {
|
|
225
|
+
source: 'DSH account/getBalance (Platform grant)',
|
|
226
|
+
wallets,
|
|
227
|
+
bonusWallets: bonus,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
package/src/credentials.js
CHANGED
|
@@ -161,3 +161,38 @@ export function credentialSource(provider) {
|
|
|
161
161
|
};
|
|
162
162
|
return { kind: 'api-key', dir: null, ref: refByProvider[provider] || null };
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Read a `records` entry (grants and session material live here, not in refs).
|
|
167
|
+
* Returns the record's payload object, or null.
|
|
168
|
+
*/
|
|
169
|
+
export function readRecord(name) {
|
|
170
|
+
try {
|
|
171
|
+
const doc = parseCredentialsYaml(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
|
|
172
|
+
const rec = (doc.records || {})[name];
|
|
173
|
+
return rec && rec.payload ? rec.payload : null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The DeepSeek Platform grant.
|
|
181
|
+
*
|
|
182
|
+
* Note this is NOT an api.deepseek.com API key — it is a Platform login grant,
|
|
183
|
+
* used as an `x-dsh-auth-token` header against platform.deepseek.com. Sending it
|
|
184
|
+
* to api.deepseek.com fails with 401 (verified), so the balance must be read
|
|
185
|
+
* through DSH's own account/getBalance RPC.
|
|
186
|
+
*/
|
|
187
|
+
export function deepseekGrant() {
|
|
188
|
+
const p = readRecord('deepseek-account-platform/default');
|
|
189
|
+
if (!p || typeof p.token !== 'string' || !p.token) return null;
|
|
190
|
+
return { token: p.token, issuer: p.issuer || 'https://platform.deepseek.com' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The browser-session secret, used to sign DSH RPC cookies. */
|
|
194
|
+
export function browserSessionSecret() {
|
|
195
|
+
const p = readRecord('client-connection/browser-session');
|
|
196
|
+
if (!p || typeof p.secret !== 'string' || !p.secret) return null;
|
|
197
|
+
return p.secret;
|
|
198
|
+
}
|
package/src/server.js
CHANGED
|
@@ -22,16 +22,20 @@
|
|
|
22
22
|
import http from 'node:http';
|
|
23
23
|
import { kimi, opencode, commandcode } from './adapters-apikey.js';
|
|
24
24
|
import { xai, codex, agy } from './adapters-cliproxy.js';
|
|
25
|
+
import { deepseek } from './adapters-deepseek.js';
|
|
25
26
|
import { credentialSource, hasCliProxy, CLI_PROXY_DIR, CREDENTIALS_FILE } from './credentials.js';
|
|
26
27
|
|
|
27
28
|
const PORT = Number(process.env.QUOTA_PILL_PORT || 8319);
|
|
28
29
|
const HOST = process.env.QUOTA_PILL_HOST || '127.0.0.1';
|
|
29
30
|
const CACHE_TTL_MS = Number(process.env.QUOTA_PILL_CACHE_TTL || 60) * 1000;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// Order matters: the pill lists providers in this order, and the official
|
|
33
|
+
// DeepSeek account is the one most people look for first.
|
|
34
|
+
const ADAPTERS = { deepseek, kimi, opencode, commandcode, xai, codex, agy };
|
|
32
35
|
|
|
33
36
|
/** Display names. Deliberately small: the table stays in sync with ADAPTERS. */
|
|
34
37
|
const DISPLAY_NAMES = {
|
|
38
|
+
deepseek: 'DeepSeek',
|
|
35
39
|
kimi: 'Kimi Coding',
|
|
36
40
|
opencode: 'OpenCode Go',
|
|
37
41
|
commandcode: 'Command Code',
|
|
@@ -52,6 +56,7 @@ const PREFIX_ALIASES = {
|
|
|
52
56
|
codex: 'codex',
|
|
53
57
|
antigravity: 'agy',
|
|
54
58
|
agy: 'agy',
|
|
59
|
+
deepseek: 'deepseek',
|
|
55
60
|
};
|
|
56
61
|
|
|
57
62
|
/** provider|model → { at, payload }. Keyed by model because readings differ per model. */
|