@hystudio/dsh-quota-pill 0.2.7 → 0.2.12

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/client.js CHANGED
@@ -87,9 +87,17 @@ window.__ModuleLoader__.load({
87
87
  return ALL_PROVIDERS.map(([k]) => k).find((k) => m.startsWith(k + '/')) || '';
88
88
  }
89
89
  if (prefix === 'command-code') return 'commandcode';
90
+ if (prefix === 'commandcode-goat') return 'commandcode';
90
91
  if (prefix === 'opencode-go') return 'opencode';
91
92
  if (prefix === 'kimi-coding') return 'kimi';
92
93
  if (prefix === 'antigravity') return 'agy';
94
+ // DSH ids that differ from the adapter keys. Without these the Current
95
+ // page cannot resolve the session's model even though the All page lists
96
+ // the provider (a real catalog has deepseek-account / grok /
97
+ // commandcode-goat / opendesign-go, not the short adapter names).
98
+ if (prefix === 'deepseek-account') return 'deepseek';
99
+ if (prefix === 'opendesign-go') return 'opendesign';
100
+ if (prefix === 'grok') return 'xai';
93
101
  if (prefix === 'kimi' || m.startsWith('kimi') || m.startsWith('k3')) return 'kimi';
94
102
  if (prefix === 'xai' || m.includes('grok')) return 'xai';
95
103
  if (prefix === 'agy' || m.includes('gemini')) return 'agy';
@@ -158,6 +166,75 @@ window.__ModuleLoader__.load({
158
166
  return hit ? hit[1] : (EXTRA_PROVIDER_NAMES[key] || String(key || ''));
159
167
  }
160
168
 
169
+ /**
170
+ * 当前 provider 未被识别时,给新会话的 agent 的完整适配提示词。
171
+ * 用户点「复制提示词」后只需在新会话粘贴,不用自己描述上下文。
172
+ */
173
+ function adaptationPrompt(model, provider, lang) {
174
+ const m = String(model || '(unknown)');
175
+ const p = String(provider && provider !== 'unknown' ? provider : 'unknown');
176
+ if (lang === 'zh') {
177
+ return [
178
+ '你是 DSH quota-pill 的 provider 适配 agent。当前模型 ' + m + ' 的 provider(' + p + ')未被插件识别,读取不到余额。',
179
+ '',
180
+ '请按 AGENTS.md 的 "Adding a new provider" 流程完成完整适配(不要修改 npm 包内文件,用户适配器会随插件更新保留):',
181
+ '1. 运行 curl -s "http://127.0.0.1:8319/setup?fresh=1",读取 catalog、unadaptedCatalog、userAdapters,确认未知的 DSH provider id。',
182
+ '2. 阅读 https://github.com/598934321/dsh-quota-pill/blob/main/AGENTS.md 和 src/user-adapters.js 的契约。',
183
+ '3. 找到该 provider 真实的额度 API 和凭据(官方 console/CLI/API;推理 key 不等于额度 key;不要猜数字)。',
184
+ '4. 在 ~/.dsh/quota-pill/adapters/<id>.mjs 新建用户适配器,导出 provider、catalogIds、displayName、credential、read(ctx);read 返回 subscription-quota / payg / usage-only / none / probe-failed 标准结构,可复用 ctx.readRef、ctx.readFirstRef、ctx.getJson、ctx.getJsonViaCurl。',
185
+ '5. 用 curl "http://127.0.0.1:8319/quota?model=' + encodeURIComponent(m) + '&fresh=1" 验证;失败要说明缺什么,绝不伪造读数。',
186
+ '6. 若需要凭据,只向用户要该 provider 的凭据;完成后运行 /quota/all?fresh=1 与 /setup?fresh=1,报告已适配/未适配的 provider。',
187
+ ].join('\n');
188
+ }
189
+ return [
190
+ 'You are the provider-adaptation agent for DSH quota-pill. The current model ' + m + ' uses a provider (' + p + ') the plugin does not recognize, so no quota can be read.',
191
+ '',
192
+ 'Follow the "Adding a new provider" flow in AGENTS.md (do not edit files inside the npm package; user adapters survive plugin updates):',
193
+ '1. Run curl -s "http://127.0.0.1:8319/setup?fresh=1"; inspect catalog, unadaptedCatalog and userAdapters to identify the unknown DSH provider id.',
194
+ '2. Read https://github.com/598934321/dsh-quota-pill/blob/main/AGENTS.md and the contract in src/user-adapters.js.',
195
+ '3. Find that provider\'s real quota API and credential (official console/CLI/API; an inference key is not a quota key; never invent numbers).',
196
+ '4. Create ~/.dsh/quota-pill/adapters/<id>.mjs exporting provider, catalogIds, displayName, credential, read(ctx). read() returns the standard subscription-quota / payg / usage-only / none / probe-failed shape and may use ctx.readRef, ctx.readFirstRef, ctx.getJson, ctx.getJsonViaCurl.',
197
+ '5. Verify with curl "http://127.0.0.1:8319/quota?model=' + encodeURIComponent(m) + '&fresh=1"; if it fails, say exactly what is missing and do not fake a reading.',
198
+ '6. If a credential is needed, ask the user only for that provider\'s credential; then run /quota/all?fresh=1 and /setup?fresh=1 and report adapted/unadapted providers.',
199
+ ].join('\n');
200
+ }
201
+
202
+ function copyToClipboard(text) {
203
+ try {
204
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
205
+ return navigator.clipboard.writeText(text);
206
+ }
207
+ } catch {}
208
+ try {
209
+ const el = document.createElement('textarea');
210
+ el.value = text;
211
+ el.setAttribute('readonly', '');
212
+ el.style.position = 'fixed';
213
+ el.style.opacity = '0';
214
+ document.body.appendChild(el);
215
+ el.select();
216
+ if (document.execCommand) document.execCommand('copy');
217
+ document.body.removeChild(el);
218
+ } catch {}
219
+ return Promise.resolve();
220
+ }
221
+
222
+ function isUnrecognizedProvider(data) {
223
+ return !!(data && (data.provider === 'unknown' || /no adapter matches/i.test(String(data.error || ''))));
224
+ }
225
+
226
+ function adaptHintBlock(data, model, lang) {
227
+ const prompt = adaptationPrompt(model, data && data.provider, lang);
228
+ return h('div', { className: 'quota-pill-adapt' },
229
+ h('div', { className: 'quota-pill-adapt-text' }, pick(lang, 'adaptHint')),
230
+ h('button', {
231
+ className: 'quota-pill-adapt-copy',
232
+ type: 'button',
233
+ onClick: () => copyToClipboard(prompt),
234
+ }, pick(lang, 'copyPrompt')),
235
+ );
236
+ }
237
+
161
238
  // ------------------------------------------------- 语言(跟随 DSH 的语言设置)
162
239
  // DSH 的 @deepseek-ai/dsh-client-locale 会把活动语言写到 <html lang>。
163
240
  // 读它即可跟随系统设置,并用 MutationObserver 订阅——切语言时无需刷新。
@@ -204,6 +281,8 @@ window.__ModuleLoader__.load({
204
281
  resetDayHour: (d, h) => [`${d} 天 ${h} 小时后重置`, `Resets in ${d}d ${h}h`],
205
282
  bodyProbeFailed: ['该模型当前不可用(可用性探测失败)。', 'This model is unavailable (availability probe failed).'],
206
283
  bodyNoApi: ['该模型暂无可查额度接口。', 'No queryable quota API for this model.'],
284
+ adaptHint: ['无法识别该 provider 的余额。请复制提示词到新会话,让 agent 完成适配。', 'This provider is not recognized. Copy the prompt into a new session and ask an agent to adapt it.'],
285
+ copyPrompt: ['复制提示词', 'Copy prompt'],
207
286
  querying: ['查询中…', 'Querying…'],
208
287
  underThreshold: ['不足 0.1%', '<0.1%'],
209
288
  turnsTotal: (n) => [`(${n} 轮合计)`, ` (${n} turns)`],
@@ -513,29 +592,28 @@ window.__ModuleLoader__.load({
513
592
  credits: null,
514
593
  }];
515
594
  }
516
- // Prepaid wallets (official DeepSeek): a balance, plus optional bonus
517
- // credit. Both are money, not percentages, so each needs its own row —
518
- // collapsing them into one label silently loses the bonus.
595
+ // Prepaid wallets (official DeepSeek): the balance is the reading, and
596
+ // the optional bonus credit is its secondary line. Keeping them as two
597
+ // window rows inherited the window-row shape — a reserved (empty) reset
598
+ // line under each amount — which wasted vertical space and made DeepSeek
599
+ // look unlike every other provider group. One row with a real secondary
600
+ // line matches the rest of the list.
519
601
  if (v.kind === 'payg' && v.credits && typeof v.credits.remain === 'number') {
520
- const rows = [{
602
+ const bonus = (v.bonus && typeof v.bonus.amount === 'number')
603
+ ? `${pick(lang, 'bonusBalance')} ${money(v.bonus.amount, v.bonus.unit)}`
604
+ : '';
605
+ return [{
521
606
  key: 'balance',
522
607
  label: pick(lang, 'balanceLeft'),
523
608
  pct: null,
524
609
  reset: '',
610
+ detail: bonus,
525
611
  credits: { remain: v.credits.remain, unit: v.credits.unit },
526
612
  money: true,
613
+ // A wallet with no bonus has no secondary line at all; do not reserve
614
+ // an empty one just to keep the row as tall as the others.
615
+ compact: !bonus,
527
616
  }];
528
- if (v.bonus && typeof v.bonus.amount === 'number') {
529
- rows.push({
530
- key: 'bonus',
531
- label: pick(lang, 'bonusBalance'),
532
- pct: null,
533
- reset: '',
534
- credits: { remain: v.bonus.amount, unit: v.bonus.unit },
535
- money: true,
536
- });
537
- }
538
- return rows;
539
617
  }
540
618
  if (v.kind !== 'subscription-quota' || v.remainingPct === null || v.remainingPct === undefined) {
541
619
  return [{ key: 'kind', label: describeQuota(v, lang).text, pct: null, reset: '', credits: null }];
@@ -586,7 +664,11 @@ window.__ModuleLoader__.load({
586
664
  const model = current ? `${current.provider}/${current.model}` : null;
587
665
  const provider = providerFor(model);
588
666
  // 读数的缓存键按 provider|model——换模型时额度来源确实不同,不能混用。
589
- const readKey = provider ? `${provider}|${current ? current.model : ''}` : '';
667
+ // 未识别的 provider 也要有一个稳定的 key,否则 query() 会直接放弃,
668
+ // Current 页就永远看不到「无法识别」的提示。
669
+ const readKey = provider
670
+ ? `${provider}|${current ? current.model : ''}`
671
+ : (current ? `unknown|${current.model}` : '');
590
672
  const readKeyRef = React.useRef(readKey);
591
673
  readKeyRef.current = readKey;
592
674
 
@@ -734,14 +816,29 @@ window.__ModuleLoader__.load({
734
816
  if (!open || tab !== 'all') allRefreshRef.current = null;
735
817
  }, [open, tab]);
736
818
 
819
+ // 切回 DSH(或从系统设置回来)时,DSH 里可能刚加/删了 provider;
820
+ // 「全部」页开着就重读一次,不必让用户先想起点刷新。
821
+ React.useEffect(() => {
822
+ if (!open || tab !== 'all') return undefined;
823
+ const onVisible = () => {
824
+ if (document.visibilityState === 'visible') loadAll(true);
825
+ };
826
+ window.addEventListener('focus', onVisible);
827
+ document.addEventListener('visibilitychange', onVisible);
828
+ return () => {
829
+ window.removeEventListener('focus', onVisible);
830
+ document.removeEventListener('visibilitychange', onVisible);
831
+ };
832
+ }, [open, tab, loadAll]);
833
+
737
834
  /**
738
835
  * 读一次额度。
739
836
  * @param mode 'soft' 上下文变化时用——允许 sidecar 的 60s 缓存命中,没发生消耗就不必打上游
740
837
  * 'fresh' 真有活动(任务起止 / 一轮结束)或手动刷新时用——强制读上游
741
838
  */
742
839
  const query = React.useCallback((tag, mode) => {
743
- if (!provider) { setData(null); return; }
744
840
  const key = readKeyRef.current;
841
+ if (!key) { setData(null); return; }
745
842
  const fresh = mode === 'fresh';
746
843
  fetch(`${SIDECAR}/quota?model=${encodeURIComponent(model)}${fresh ? '&fresh=1' : ''}`)
747
844
  .then((r) => r.json())
@@ -1030,7 +1127,19 @@ window.__ModuleLoader__.load({
1030
1127
  // ① 有百分比的订阅额度 ② 积分类(pct 为 null,但有 credits)
1031
1128
  // ③ 免费池(usage-only,既无百分比也无积分)
1032
1129
  : canRenderBody(dataPayload)
1033
- ? quotaBody(dataPayload, lastDelta, lastSpan, avgDelta, remainTasks, rec.turns.length, lang, providerName(provider), stale)
1130
+ ? quotaBody(
1131
+ dataPayload,
1132
+ lastDelta,
1133
+ lastSpan,
1134
+ avgDelta,
1135
+ remainTasks,
1136
+ rec.turns.length,
1137
+ lang,
1138
+ dataPayload && dataPayload.displayName
1139
+ ? String(dataPayload.displayName)
1140
+ : providerName(provider),
1141
+ stale,
1142
+ )
1034
1143
  : h('div', {
1035
1144
  className: 'quota-pill-empty',
1036
1145
  // 这个 provider 从没刷过:显示「—」并让刷新动效说明正在取。
@@ -1040,6 +1149,8 @@ window.__ModuleLoader__.load({
1040
1149
  },
1041
1150
  dataPayload && dataPayload.kind === 'probe-failed' ? pick(lang, 'bodyProbeFailed')
1042
1151
  : dataPayload && dataPayload.kind === 'token-meter' ? pick(lang, 'bodyNoApi')
1152
+ : isUnrecognizedProvider(dataPayload)
1153
+ ? adaptHintBlock(dataPayload, model, lang)
1043
1154
  : dataPayload && dataPayload.error ? String(dataPayload.error)
1044
1155
  : stale ? '—' : pick(lang, 'querying')),
1045
1156
  ),
@@ -1274,7 +1385,13 @@ window.__ModuleLoader__.load({
1274
1385
  }
1275
1386
 
1276
1387
  function WindowRow(props) {
1277
- const { label, pct, reset, credits, lang, stale, money: isMoney } = props;
1388
+ const { label, pct, reset, detail, credits, lang, stale, money: isMoney, compact } = props;
1389
+ // `detail` is a generic secondary line (a payg bonus, an aggregate hint);
1390
+ // `reset` is the window-reset countdown. Rows in a normal group reserve
1391
+ // the line even when empty so their numbers stay aligned inside the group;
1392
+ // a compact row drops it instead of leaving a blank line.
1393
+ const secondary = detail || reset || '';
1394
+ const reserveSecondary = !compact || !!secondary;
1278
1395
  const isNum = typeof pct === 'number' && Number.isFinite(pct);
1279
1396
  const tweened = useTween(isNum ? pct : null, 450);
1280
1397
  // 首帧 tween 还没拿到起点时 shown 是 null;这时按目标值渲染静态读数,
@@ -1295,14 +1412,11 @@ window.__ModuleLoader__.load({
1295
1412
  h('div', { className: 'quota-pill-win-main' },
1296
1413
  h('div', { className: 'quota-pill-win-text' },
1297
1414
  h('span', { className: 'quota-pill-win-label', title: label }, label),
1298
- // Reserve the reset line even when there is nothing to show.
1299
- // Conditionally omitting it made rows with a reset taller than those
1300
- // without one, so the column of numbers did not line up.
1301
- h('span', {
1415
+ reserveSecondary ? h('span', {
1302
1416
  className: 'quota-pill-win-reset',
1303
- title: reset || undefined,
1304
- style: reset ? undefined : { visibility: 'hidden' },
1305
- }, reset || '\u00a0'),
1417
+ title: secondary || undefined,
1418
+ style: secondary ? undefined : { visibility: 'hidden' },
1419
+ }, secondary || '\u00a0') : null,
1306
1420
  ),
1307
1421
  h('div', { className: 'quota-pill-win-gauge' },
1308
1422
  showCredits
@@ -1344,7 +1458,7 @@ window.__ModuleLoader__.load({
1344
1458
  h('div', { className: 'quota-pill-group-head' }, name),
1345
1459
  // 组内行 key 带组名,避免不同 provider 的同名窗口(都是 5h/weekly)
1346
1460
  // 被 React 当成同一个组件实例,导致 tween 跨 provider 插值。
1347
- entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
1461
+ entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, detail: e.detail, compact: e.compact, credits: e.credits, lang, stale, money: e.money })),
1348
1462
  );
1349
1463
  }
1350
1464
 
@@ -1499,6 +1613,10 @@ window.__ModuleLoader__.load({
1499
1613
  '.quota-pill-row dt{color:var(--dsw-alias-label-secondary)}',
1500
1614
  '.quota-pill-row dd{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);margin:0}',
1501
1615
  '.quota-pill-empty{color:var(--dsw-alias-label-secondary)}',
1616
+ '.quota-pill-adapt{display:flex;flex-direction:column;gap:8px;padding:4px 0}',
1617
+ '.quota-pill-adapt-text{color:var(--dsw-alias-label-secondary)}',
1618
+ '.quota-pill-adapt-copy{align-self:flex-start;cursor:pointer;border:1px solid var(--dsw-alias-border-l3,rgba(127,127,127,.28));background:transparent;color:var(--dsw-alias-label-primary);font:inherit;padding:4px 8px;border-radius:var(--dsw-radius-sm,6px)}',
1619
+ '.quota-pill-adapt-copy:hover{background:var(--dsw-alias-interactive-bg-hover)}',
1502
1620
  // 页签:当前 / 全部
1503
1621
  '.quota-pill-tabs{display:flex;gap:2px;margin:-4px 0 10px;padding:2px;border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-interactive-bg-hover)}',
1504
1622
  '.quota-pill-tab{flex:1;cursor:pointer;border:0;background:transparent;font:inherit;padding:3px 0;border-radius:var(--dsw-radius-sm,6px);color:var(--dsw-alias-label-secondary)}',
Binary file
package/index.js CHANGED
@@ -2,18 +2,26 @@
2
2
  * Host half of the quota-pill bundle.
3
3
  *
4
4
  * Rendering lives in client.js, which the web GUI loads separately. This half
5
- * registers nothing: the plugin only paints a pill, exposes no host service and
6
- * no RPC.
5
+ * starts the local sidecar on first launch, so a fresh install shows quota
6
+ * without a separate service unit. The sidecar is idempotent: if one is already
7
+ * listening (launchd/systemd/previous launch), ensureSidecar() leaves it alone.
7
8
  *
8
9
  * It still has to be a well-formed Cordis plugin, which means exporting `name`.
9
10
  * Omitting it makes the loader reject the bundle at import time with
10
11
  * "@hystudio/dsh-quota-pill: import failed" — and because the failure happens
11
12
  * before activation, nothing in the UI says why. That is how 0.2.2 shipped.
12
13
  */
14
+ import { ensureSidecar } from './src/autostart.js';
15
+
13
16
  export const name = 'quota-pill';
14
17
 
15
18
  /** No host-side services are required. */
16
19
  export const inject = [];
17
20
 
18
- /** Nothing to set up on the host; the client half does all the work. */
19
- export function apply() {}
21
+ /**
22
+ * Best-effort sidecar start. Never reject: a plugin host failure would be worse
23
+ * than "quota service is not running", which the pill already explains.
24
+ */
25
+ export function apply() {
26
+ return ensureSidecar().catch(() => null);
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hystudio/dsh-quota-pill",
3
- "version": "0.2.7",
3
+ "version": "0.2.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Quota pill for DeepSeek Harness: remaining quota for every LLM subscription you have, in one collapsible pill",
@@ -29,7 +29,8 @@
29
29
  "./server": "./src/server.js"
30
30
  },
31
31
  "bin": {
32
- "dsh-quota-pill": "bin/quota-pill-server.js"
32
+ "dsh-quota-pill": "bin/quota-pill-server.js",
33
+ "dsh-quota-pill-opendesign-login": "bin/opendesign-login.js"
33
34
  },
34
35
  "files": [
35
36
  "index.js",
@@ -38,6 +39,7 @@
38
39
  "src/**/*.js",
39
40
  "bin/**/*.js",
40
41
  "docs/**/*.md",
42
+ "docs/**/*.png",
41
43
  "README.md",
42
44
  "AGENTS.md",
43
45
  "LICENSE"
@@ -61,6 +63,6 @@
61
63
  "scripts": {
62
64
  "start": "node bin/quota-pill-server.js",
63
65
  "test": "node --test test/*.test.js",
64
- "check": "node --check src/server.js && node --check client.js"
66
+ "check": "node --check src/server.js && node --check src/providers.js && node --check src/autostart.js && node --check src/user-adapters.js && node --check client.js"
65
67
  }
66
68
  }
@@ -5,7 +5,7 @@
5
5
  * ~/.dsh/.credentials.yaml → refs.<NAME>. No OAuth, no external tool: if the
6
6
  * key is present the adapter works, and it is the only requirement.
7
7
  */
8
- import { readRef, credentialSource } from './credentials.js';
8
+ import { readRef, readFirstRef, credentialSource } from './credentials.js';
9
9
  import { getJson } from './http.js';
10
10
 
11
11
  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
@@ -213,8 +213,13 @@ export async function opencode() {
213
213
  * with weekly.cap 6). So the ceiling is inferred rather than hardcoded to 70.
214
214
  */
215
215
  export async function commandcode() {
216
- const token = readRef('COMMANDCODE_API_KEY');
217
- if (!token) return notConfigured('commandcode', 'COMMANDCODE_API_KEY');
216
+ // DSH's own provider config names this ref after its apiKeyEnv
217
+ // (COMMANDCODE_GOAT_API_KEY); older docs use the short name. Accept both so
218
+ // a key the user already has is not reported as missing.
219
+ const token = readFirstRef(['COMMANDCODE_API_KEY', 'COMMANDCODE_GOAT_API_KEY']);
220
+ if (!token) {
221
+ return notConfigured('commandcode', 'COMMANDCODE_API_KEY or COMMANDCODE_GOAT_API_KEY');
222
+ }
218
223
 
219
224
  const res = await getJson('https://api.commandcode.ai/alpha/billing/credits', {
220
225
  token,