@goodandready/dsh-subscriptions 0.4.14 → 0.4.16

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.zh.md ADDED
@@ -0,0 +1,109 @@
1
+ # 📦 @goodandready/dsh-subscriptions
2
+
3
+ <div align="center">
4
+
5
+ <h3>DeepSeek Harness 个人 AI 订阅桥接、多账号池轮换与零泄漏 OAuth 插件</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-subscriptions"><img src="https://img.shields.io/npm/v/@goodandready/dsh-subscriptions.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
+ </p>
23
+
24
+ </div>
25
+
26
+ ---
27
+
28
+ ## ⚡ 插件概览
29
+
30
+ **`dsh-subscriptions`** 将您现有的付费个人 AI 订阅无缝接入 **DeepSeek Harness**,作为第一类模型服务商。
31
+
32
+ 无需为日常智能体任务支付昂贵的 API 按量计费。本插件支持标准 OAuth PKCE 鉴权、**单服务商多账号池动态轮换**(遭遇 429 限流时自动秒切可用账号)、**前置配额平滑切换**,并通过**进程内 Cordis 服务 (`ctx.subscriptions`)** 赋能 [`dsh-image-gen`](https://github.com/GooDAnDReaDY/dsh-image-gen) 与 [`dsh-grok-xsearch`](https://github.com/GooDAnDReaDY/dsh-grok-xsearch) 等插件,实现 Token 零网络泄漏。
33
+
34
+ ```mermaid
35
+ graph LR
36
+ subgraph DSHCore [DeepSeek Harness 对话流]
37
+ Agent[🤖 智能体任务执行] --> Router{服务商路由器}
38
+ end
39
+
40
+ subgraph SubscriptionsCore [dsh-subscriptions 调度核心]
41
+ Router --> Pool{多账号负载池}
42
+ Pool -->|账号 1| Acc1[👤 主账号: 活跃中]
43
+ Pool -->|账号 2| Acc2[👤 副账号: 待命中]
44
+ Pool -->|账号 3| Acc3[👤 备用账号: 冷却中]
45
+ Acc1 -->|HTTP 429 / 配额耗尽| Rotate[智能配额与冷却轮换器]
46
+ Rotate -->|流量平滑切换| Acc2
47
+ end
48
+
49
+ subgraph VendorBridges [4 大服务商桥接适配]
50
+ Acc1 --> B1[ChatGPT / Codex 后端]
51
+ Acc1 --> B2[Claude Pro / Max 协议]
52
+ Acc1 --> B3[xAI / Grok 订阅]
53
+ Acc1 --> B4[Google Cloud Code Assist / Antigravity]
54
+ end
55
+
56
+ subgraph EcosystemBridge [Cordis 进程内共享: ctx.subscriptions]
57
+ Pool --> ImgGen[dsh-image-gen: 订阅零成本生图]
58
+ Pool --> XSearch[dsh-grok-xsearch: X 社交搜索]
59
+ end
60
+
61
+ style DSHCore fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
62
+ style SubscriptionsCore fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
63
+ style VendorBridges fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
64
+ style EcosystemBridge fill:#181825,stroke:#f38ba8,stroke-width:2px,color:#cdd6f4
65
+ ```
66
+
67
+ ---
68
+
69
+ ## ✨ 核心功能
70
+
71
+ ### 支持的内置订阅供应商
72
+ | 供应商 | 订阅级别 | 协议 |
73
+ |---|---|---|
74
+ | `codex` | ChatGPT Plus / Pro | 流式响应、工具调用、图像生成 |
75
+ | `claude` | Claude Pro / Max | 原生 Messages 协议、用量跟踪 |
76
+ | `grok` | xAI / X Premium | 实时推理、计费检查 |
77
+ | `antigravity` | Google Cloud Code Assist | 流式生成 |
78
+
79
+ ### 多账号轮换与限流保护
80
+ * 每个供应商可挂多个账号(如 `CODEX_OAUTH_1`、`CODEX_OAUTH_2`)。
81
+ * 遇到 429/配额耗尽自动切换到下一个健康账号;支持按剩余量提前切换和动态冷却恢复。
82
+
83
+ ### 安全与无头登录
84
+ * OAuth 令牌绝不通过 HTTP API 返回,也不在 Web 界面渲染;仅存放于宿主加密凭据存储。
85
+ * **回环自动回调 (`autoLoopback`,`v0.4.9`)**:供应商重定向为回环地址(如 Codex `:1455`)时,插件自动启动临时本地服务器接收回调,无需手动粘贴链接。
86
+ * **设备码登录(Codex,`v0.4.9`)**:完全无浏览器的主机上,点击 **Device login**,在任意设备打开 `https://auth.openai.com/codex/device` 输入短码即可完成 PKCE 授权。
87
+
88
+ ### 账号级代理(`v0.4.9`)
89
+ * 每个账号可配置独立代理(`http://`、`https://`、`socks5://`);令牌刷新、供应商检查、模型请求均走该代理。
90
+ * 账号卡片内的 **Check proxy** 按钮一键检测代理延迟。
91
+
92
+ ### 隐私模式与诊断报告(`v0.4.9`)
93
+ * **`privacyMask`**:一个开关即可在整个界面隐藏个人数据(邮箱显示为 `j***n@example.com`),服务端遮蔽,适合屏幕共享。
94
+ * **匿名诊断报告**:设置卡片内一键生成(插件/运行时版本、系统、各供应商健康状态、HTTP 状态聚合、最近错误与耗时、非敏感配置)并自动复制到剪贴板;令牌、邮箱、凭据名与代理地址严格排除。
95
+ * 同区块提供问题追踪器链接,便于提交 issue。
96
+
97
+ ---
98
+
99
+ ## 📦 安装指南
100
+
101
+ ```bash
102
+ dsh plugin --profile web add @goodandready/dsh-subscriptions
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 📄 开源协议
108
+
109
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/lib/accounts.js CHANGED
@@ -48,7 +48,7 @@ export function vendorConfig(provider, cfg) {
48
48
  }
49
49
  }
50
50
 
51
- export function createAccountStore({ credentials, getConfig, fetchImpl, onLimitNotice }) {
51
+ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchForRef, onLimitNotice }) {
52
52
  const cooldowns = new Map()
53
53
  const quotas = new Map()
54
54
  const refreshLocks = new Map()
@@ -60,6 +60,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, onLimitN
60
60
  const windows = new Map()
61
61
  const usageFetched = new Map()
62
62
  const doFetch = fetchImpl || fetch
63
+ // #88: per-account proxy fetch for token refresh / usage (falls back to doFetch).
64
+ const fetchFor = (ref) => (typeof fetchForRef === 'function' && fetchForRef(ref)) || doFetch
63
65
 
64
66
  async function resolveRaw(ref) {
65
67
  try {
@@ -168,7 +170,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, onLimitN
168
170
  const promise = (async () => {
169
171
  try {
170
172
  const cfg = vendorConfig(provider, getConfig())
171
- const next = await getVendor(provider).refresh(cfg, blob, doFetch)
173
+ const next = await getVendor(provider).refresh(cfg, blob, fetchFor(ref))
172
174
  const merged = {
173
175
  ...blob,
174
176
  ...next,
@@ -201,7 +203,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, onLimitN
201
203
  if (!raw) continue
202
204
  try {
203
205
  const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
204
- const snap = await vendor.usage(blob, cfg, doFetch)
206
+ const snap = await vendor.usage(blob, cfg, fetchFor(slot.ref))
205
207
  usageFetched.set(slot.ref, Date.now())
206
208
  if (!snap) continue
207
209
  if (Number.isFinite(Number(snap.usedPercent))) {
package/lib/adapter.js CHANGED
@@ -3,6 +3,7 @@ import { displayName } from './refs.js'
3
3
  import { getVendor } from './vendors/index.js'
4
4
  import { modelCatalog } from './messages.js'
5
5
  import { streamWithRotation } from './stream-rotate.js'
6
+ import { pickFetch } from './proxy.js'
6
7
 
7
8
  function asLlmError(err) {
8
9
  if (err instanceof LlmError) return err
@@ -86,7 +87,7 @@ export class SubscriptionAdapter extends LlmAdapter {
86
87
  const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
87
88
  const vendor = getVendor(provider)
88
89
  // ponytail: capture x-ratelimit headers without touching vendors
89
- const baseFetch = deps.fetchImpl || fetch
90
+ const baseFetch = pickFetch(deps, account.ref)
90
91
  const quotaFetch = async (url, init) => {
91
92
  const res = await baseFetch(url, init)
92
93
  try {
@@ -131,6 +132,7 @@ export class SubscriptionAdapter extends LlmAdapter {
131
132
  path: '/responses',
132
133
  method: 'POST',
133
134
  status: 200,
135
+ ms: Date.now() - t0,
134
136
  kind: 'stream',
135
137
  })
136
138
  } catch {}
package/lib/client.js CHANGED
@@ -88,12 +88,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
88
88
  'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
89
89
  'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
90
90
  'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
91
+ 'privacyMask': 'Mask emails and account identifiers',
92
+ 'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
91
93
  'reconnect': 'Reconnect',
92
94
  'connect': 'Connect',
93
95
  'disconnect': 'Disconnect',
94
96
  'removeSlot': 'Remove slot',
95
97
  'pastePlaceholder': 'Paste redirected URL or code',
96
98
  'submitCode': 'Submit code',
99
+ 'proxyPlaceholder': 'Per-account proxy (http://, https://, socks5://) - optional',
100
+ 'proxyCheck': 'Check proxy',
101
+ 'proxyOk': 'proxy ok',
102
+ 'proxyFail': 'proxy fail',
103
+ 'deviceLogin': 'Device login',
104
+ 'deviceHint': 'Headless: open the link, enter the code, keep this tab open.',
105
+ 'deviceCopy': 'Copy code',
106
+ 'devicePending': 'Waiting for confirmation',
107
+ 'deviceAuthorized': 'Signed in',
108
+ 'deviceExpired': 'Expired - start again',
97
109
  'verifyPrefix': 'Google requires one-time account verification. ',
98
110
  'verifyLink': 'Open verification link',
99
111
  'verifySuffix': ' then reconnect.',
@@ -143,12 +155,24 @@ const cssId = 'dsh-subscriptions/settings.module.css'
143
155
  'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
144
156
  'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
145
157
  'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
158
+ 'privacyMask': 'Маскировать email и учётные записи',
159
+ 'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
146
160
  'reconnect': 'Переподключить',
147
161
  'connect': 'Подключить',
148
162
  'disconnect': 'Отключить',
149
163
  'removeSlot': 'Убрать место',
150
164
  'pastePlaceholder': 'Адрес перехода или код',
151
165
  'submitCode': 'Отправить код',
166
+ 'proxyPlaceholder': 'Прокси аккаунта (http://, https://, socks5://) - необязательно',
167
+ 'proxyCheck': 'Проверить прокси',
168
+ 'proxyOk': 'прокси ок',
169
+ 'proxyFail': 'прокси недоступен',
170
+ 'deviceLogin': 'Вход по коду',
171
+ 'deviceHint': 'Headless: откройте ссылку, введите код, держите вкладку открытой.',
172
+ 'deviceCopy': 'Скопировать код',
173
+ 'devicePending': 'Ожидание подтверждения',
174
+ 'deviceAuthorized': 'Вход выполнен',
175
+ 'deviceExpired': 'Истёк - начните заново',
152
176
  'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
153
177
  'verifyLink': 'Открыть страницу проверки',
154
178
  'verifySuffix': ' затем подключитесь заново.',
@@ -237,6 +261,8 @@ function PluginCard(props) {
237
261
  const [accounts, setAccounts] = React.useState([])
238
262
  const [providers, setProviders] = React.useState([])
239
263
  const [paste, setPaste] = React.useState({})
264
+ const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
265
+ const [proxyRes, setProxyRes] = React.useState({}) // #88 key -> {ok,latencyMs,error}
240
266
  const [checkRes, setCheckRes] = React.useState({})
241
267
  const [checking, setChecking] = React.useState({})
242
268
  const [saved, setSaved] = React.useState(false)
@@ -259,6 +285,16 @@ function PluginCard(props) {
259
285
  return () => { alive = false }
260
286
  }, [])
261
287
 
288
+ // #90: poll device login while any slot is in 'pending' state.
289
+ React.useEffect(() => {
290
+ const pendingKeys = Object.entries(device).filter(([, d]) => d && d.status === 'pending')
291
+ if (!pendingKeys.length) return
292
+ const timers = pendingKeys.map(([key, d]) => setInterval(() => {
293
+ devicePollOnce(key.split(':')[0], Number(key.split(':')[1]))
294
+ }, d.intervalMs || 5000))
295
+ return () => timers.forEach(clearInterval)
296
+ })
297
+
262
298
  if (!draft) return React.createElement('div', { className: 'dsub-wrap' }, t('loading'))
263
299
 
264
300
  const slots = Array.isArray(draft.slots) ? draft.slots : []
@@ -286,6 +322,45 @@ function PluginCard(props) {
286
322
  if (data.url) window.open(data.url, '_blank', 'noopener')
287
323
  }
288
324
 
325
+ // #90: device-code login (headless). Server mints user_code; we show it,
326
+ // open the verification page, and poll until authorized.
327
+ const deviceStartLogin = async (provider, index) => {
328
+ const key = provider + ':' + index
329
+ setErr('')
330
+ const res = await fetch('/dsh-subscriptions/oauth/device/start', {
331
+ method: 'POST',
332
+ headers: { 'Content-Type': 'application/json' },
333
+ body: JSON.stringify({ provider, index }),
334
+ })
335
+ const data = await res.json().catch(() => ({}))
336
+ if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
337
+ setDevice((m) => Object.assign({}, m, { [key]: { state: data.state, userCode: data.userCode, authUrl: data.authUrl, intervalMs: data.intervalMs || 5000, status: 'pending' } }))
338
+ }
339
+
340
+ const devicePollOnce = async (provider, index) => {
341
+ const key = provider + ':' + index
342
+ const d = device[key]
343
+ if (!d || !d.state) return
344
+ const res = await fetch('/dsh-subscriptions/oauth/device/poll', {
345
+ method: 'POST',
346
+ headers: { 'Content-Type': 'application/json' },
347
+ body: JSON.stringify({ state: d.state }),
348
+ })
349
+ const data = await res.json().catch(() => ({}))
350
+ if (!res.ok) {
351
+ setDevice((m) => Object.assign({}, m, { [key]: Object.assign({}, d, { status: 'expired' }) }))
352
+ return
353
+ }
354
+ if (data.status === 'authorized') {
355
+ setDevice((m) => Object.assign({}, m, { [key]: Object.assign({}, d, { status: 'authorized' }) }))
356
+ reload().catch(() => {})
357
+ return
358
+ }
359
+ if (data.status === 'expired') {
360
+ setDevice((m) => Object.assign({}, m, { [key]: Object.assign({}, d, { status: 'expired' }) }))
361
+ }
362
+ }
363
+
289
364
  const complete = async (provider, index) => {
290
365
  setErr('')
291
366
  const key = provider + ':' + index
@@ -301,6 +376,24 @@ function PluginCard(props) {
301
376
  setPaste((p) => Object.assign({}, p, { [key]: '' }))
302
377
  }
303
378
 
379
+ // #88: проверить прокси аккаунта реальным запросом с замером задержки.
380
+ const doProxyCheck = async (provider, index) => {
381
+ const key = provider + ':' + index
382
+ setChecking((c) => Object.assign({}, c, { ['proxy:' + key]: true }))
383
+ setErr('')
384
+ try {
385
+ const res = await fetch('/dsh-subscriptions/proxy-check', {
386
+ method: 'POST',
387
+ headers: { 'Content-Type': 'application/json' },
388
+ body: JSON.stringify({ provider, index }),
389
+ })
390
+ const data = await res.json().catch(() => ({}))
391
+ setProxyRes((m) => Object.assign({}, m, { [key]: data }))
392
+ } catch (e) { setErr(String(e.message || e)) }
393
+ setChecking((c) => Object.assign({}, c, { ['proxy:' + key]: false }))
394
+ }
395
+
396
+
304
397
  const doCheck = async (provider, index) => {
305
398
  const key = provider + ':' + index
306
399
  setChecking((c) => Object.assign({}, c, { [key]: true }))
@@ -376,6 +469,16 @@ function PluginCard(props) {
376
469
  ),
377
470
  React.createElement('div', { className: 'dsub-sub' },
378
471
  t('useOriginHint')),
472
+ React.createElement('label', { className: 'dsub-row' },
473
+ React.createElement('input', {
474
+ type: 'checkbox',
475
+ checked: !!draft.privacyMask,
476
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { privacyMask: e.target.checked })),
477
+ }),
478
+ React.createElement('span', null, t('privacyMask')),
479
+ ),
480
+ React.createElement('div', { className: 'dsub-sub' },
481
+ t('privacyMaskHint')),
379
482
  ),
380
483
  names.map((prov) => {
381
484
  const rows = slots
@@ -404,6 +507,10 @@ function PluginCard(props) {
404
507
  type: 'button', className: 'dsub-mini',
405
508
  onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
406
509
  }, on ? t('reconnect') : t('connect')),
510
+ (row.slot.provider === 'codex' ? React.createElement('button', {
511
+ type: 'button', className: 'dsub-mini',
512
+ onClick: () => deviceStartLogin(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
513
+ }, t('deviceLogin')) : null),
407
514
  React.createElement('button', {
408
515
  type: 'button', className: 'dsub-mini',
409
516
  disabled: !on,
@@ -450,7 +557,50 @@ function PluginCard(props) {
450
557
  onClick: () => importToken(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
451
558
  }, t('importToken')),
452
559
  ),
560
+ React.createElement('div', { className: 'dsub-row' },
561
+ React.createElement('input', {
562
+ className: 'dsub-grow',
563
+ value: row.slot.proxyUrl || '',
564
+ placeholder: t('proxyPlaceholder'),
565
+ onChange: (e) => {
566
+ const next = slots.slice()
567
+ next[row.i] = Object.assign({}, next[row.i], { proxyUrl: e.target.value })
568
+ setSlots(next)
569
+ },
570
+ }),
571
+ React.createElement('button', {
572
+ type: 'button', className: 'dsub-mini',
573
+ disabled: checking['proxy:' + key],
574
+ onClick: () => doProxyCheck(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
575
+ }, checking['proxy:' + key] ? '\u2026' : t('proxyCheck')),
576
+ ),
577
+ (function(){
578
+ var r = proxyRes[key]
579
+ if (!r) return null
580
+ var txt = r.ok ? (t('proxyOk') + ' ' + r.latencyMs + 'ms' + (r.viaProxy ? '' : ' (direct)')) : (t('proxyFail') + ': ' + ((r.error && r.error.message) || ''))
581
+ return React.createElement('div', { className: r.ok ? 'dsub-ok' : 'dsub-bad' }, txt)
582
+ })(),
453
583
  ),
584
+ (function devicePanel(){
585
+ var d = device[key]
586
+ if (!d) return null
587
+ var txt = d.status === 'authorized' ? t('deviceAuthorized') : (d.status === 'expired' ? t('deviceExpired') : t('devicePending'))
588
+ var cls = d.status === 'authorized' ? 'dsub-ok' : (d.status === 'expired' ? 'dsub-bad' : 'dsub-verify')
589
+ return React.createElement('div', { className: 'dsub-verify' },
590
+ React.createElement('div', { className: cls }, txt),
591
+ d.status === 'pending' ? React.createElement(React.Fragment, null,
592
+ React.createElement('div', { style: { fontSize: '2em', fontWeight: 700, letterSpacing: '0.15em', margin: '4px 0' } }, d.userCode),
593
+ React.createElement('div', { className: 'dsub-row' },
594
+ React.createElement('button', {
595
+ type: 'button', className: 'dsub-mini',
596
+ onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch {} },
597
+ }, t('deviceCopy')),
598
+ React.createElement('a', { href: d.authUrl, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
599
+ ),
600
+ React.createElement('div', { className: 'dsub-sub' }, t('deviceHint')),
601
+ ) : null,
602
+ )
603
+ })(),
454
604
  account.accountNotice ? React.createElement('div', { className: 'dsub-verify' }, account.accountNotice) : null,
455
605
  account.refreshError ? React.createElement('div', { className: 'dsub-bad' }, t('reconnectRequired') + ': ' + account.refreshError) : null,
456
606
  (function(){ var r=checkRes[key]; if(!r) return null; var txt=r.ok ? ('ok ' + (r.email||'')) : ('fail ' + (r.error && r.error.message || '')); var cls=r.ok ? 'dsub-ok' : 'dsub-bad'; var q=r.quota; if(q && q.remaining!=null) txt += ' quota:'+q.remaining+(q.limit!=null?'/'+q.limit:''); return React.createElement('div', {className: cls}, txt) })(),