@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.md +157 -213
- package/README.ru.md +174 -0
- package/README.zh.md +109 -0
- package/lib/accounts.js +5 -3
- package/lib/adapter.js +3 -1
- package/lib/client.js +150 -0
- package/lib/index.js +294 -9
- package/lib/loopback.js +66 -0
- package/lib/mask.js +21 -0
- package/lib/proxy.js +79 -0
- package/lib/subscriptions.js +4 -1
- package/lib/vendors/codex.js +45 -0
- package/package.json +6 -2
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,
|
|
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,
|
|
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.
|
|
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) })(),
|