dsh-plugin-subscriptions 0.5.0 → 0.5.2
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 +21 -6
- package/README.zh.md +20 -6
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +18 -2
- package/lib/auth/rpc.js +98 -3
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/SubscriptionsSection.d.ts +18 -1
- package/lib/client/SubscriptionsSection.js +216 -6
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +72 -0
- package/lib/client/locales.js +72 -0
- package/lib/client.js +725 -144
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2256 -226
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/catalog-store.js +15 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +51 -33
- package/lib/providers/codex.js +58 -13
- package/lib/providers/common.d.ts +32 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +787 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +53 -24
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.js +2 -1
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +363 -0
- package/lib/translate/responses.d.ts +49 -5
- package/lib/translate/responses.js +40 -7
- package/package.json +11 -7
package/lib/auth/store.js
CHANGED
|
@@ -10,7 +10,7 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
|
10
10
|
import { dirname } from 'node:path';
|
|
11
11
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
12
12
|
/** Every provider route, in display order. */
|
|
13
|
-
export const PROVIDER_IDS = ['codex', 'claude', 'grok'];
|
|
13
|
+
export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot'];
|
|
14
14
|
/**
|
|
15
15
|
* Absolute path of the auth store file.
|
|
16
16
|
* @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
|
|
@@ -103,6 +103,38 @@ async function writeStore(store, path) {
|
|
|
103
103
|
throw error;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* One write chain per store path. Every mutation is a read-modify-write of a
|
|
108
|
+
* single JSON file, and the plugin has several independent writers — a login,
|
|
109
|
+
* a logout, and one token refresh per provider adapter, each on its own
|
|
110
|
+
* schedule. Overlapping them unserialized costs whichever provider read the
|
|
111
|
+
* store first its entry.
|
|
112
|
+
*
|
|
113
|
+
* A chain is dropped once nothing is queued behind it, so the map holds an
|
|
114
|
+
* entry only while writes are in flight.
|
|
115
|
+
*/
|
|
116
|
+
const writeChains = new Map();
|
|
117
|
+
/**
|
|
118
|
+
* Run one read-modify-write of a store path after every write already queued
|
|
119
|
+
* for it. Callers join the chain synchronously, so call order is write order.
|
|
120
|
+
* @param path - the store file being mutated.
|
|
121
|
+
* @param action - the read-modify-write to run.
|
|
122
|
+
* @returns whatever `action` returns.
|
|
123
|
+
*/
|
|
124
|
+
async function serialize(path, action) {
|
|
125
|
+
const previous = writeChains.get(path) ?? Promise.resolve();
|
|
126
|
+
// Both handlers: a failed write must not strand everything queued behind it.
|
|
127
|
+
const next = previous.then(action, action);
|
|
128
|
+
const tail = next.then(() => undefined, () => undefined);
|
|
129
|
+
writeChains.set(path, tail);
|
|
130
|
+
try {
|
|
131
|
+
return await next;
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (writeChains.get(path) === tail)
|
|
135
|
+
writeChains.delete(path);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
106
138
|
/**
|
|
107
139
|
* Read one provider's session.
|
|
108
140
|
* @param provider - the provider route.
|
|
@@ -119,9 +151,11 @@ export async function getSession(provider, path = authFilePath()) {
|
|
|
119
151
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
120
152
|
*/
|
|
121
153
|
export async function saveSession(provider, session, path = authFilePath()) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
return serialize(path, async () => {
|
|
155
|
+
const store = await loadStore(path);
|
|
156
|
+
store[provider] = session;
|
|
157
|
+
await writeStore(store, path);
|
|
158
|
+
});
|
|
125
159
|
}
|
|
126
160
|
/**
|
|
127
161
|
* Delete one provider's session (logout).
|
|
@@ -129,9 +163,11 @@ export async function saveSession(provider, session, path = authFilePath()) {
|
|
|
129
163
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
130
164
|
*/
|
|
131
165
|
export async function deleteSession(provider, path = authFilePath()) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
return serialize(path, async () => {
|
|
167
|
+
const store = await loadStore(path);
|
|
168
|
+
if (store[provider] === undefined)
|
|
169
|
+
return;
|
|
170
|
+
delete store[provider];
|
|
171
|
+
await writeStore(store, path);
|
|
172
|
+
});
|
|
137
173
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
|
|
2
2
|
import type { SubscriptionsKey } from './locales.js';
|
|
3
3
|
/** Subscription provider ids, fixed by the node half's OAuth adapters. */
|
|
4
|
-
export type SubscriptionProvider = 'codex' | 'claude' | 'grok';
|
|
4
|
+
export type SubscriptionProvider = 'codex' | 'claude' | 'grok' | 'copilot';
|
|
5
5
|
/** One provider's login state as answered by the `status` endpoint. */
|
|
6
6
|
export interface ProviderStatus {
|
|
7
7
|
loggedIn: boolean;
|
|
@@ -23,6 +23,23 @@ export interface ProviderUsage {
|
|
|
23
23
|
windows?: UsageWindow[];
|
|
24
24
|
plan?: string;
|
|
25
25
|
}
|
|
26
|
+
/** `proxyGet` endpoint value: the node half owns this shape (no secrets). */
|
|
27
|
+
export interface ProxyConfigView {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
url: string;
|
|
30
|
+
username?: string;
|
|
31
|
+
passwordSet: boolean;
|
|
32
|
+
bypass: string[];
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/** `proxyTest` endpoint value. */
|
|
36
|
+
export interface ProxyTestResult {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
viaProxy: boolean;
|
|
39
|
+
status?: number;
|
|
40
|
+
latencyMs?: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
26
43
|
/** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
|
|
27
44
|
export interface SubscriptionsSectionInjected {
|
|
28
45
|
/** Generic logical-RPC caller over the Connection transport. */
|
|
@@ -24,6 +24,7 @@ const PROVIDERS = [
|
|
|
24
24
|
{ id: 'codex', name: 'Codex (ChatGPT)' },
|
|
25
25
|
{ id: 'claude', name: 'Claude' },
|
|
26
26
|
{ id: 'grok', name: 'Grok (X Premium)' },
|
|
27
|
+
{ id: 'copilot', name: 'GitHub Copilot' },
|
|
27
28
|
];
|
|
28
29
|
/** Business error returned by the `/subscriptions-auth` channel (error branch message). */
|
|
29
30
|
class SubscriptionsAuthError extends Error {
|
|
@@ -77,6 +78,10 @@ const styles = {
|
|
|
77
78
|
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12,
|
|
78
79
|
padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
|
|
79
80
|
},
|
|
81
|
+
proxyCard: {
|
|
82
|
+
padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
|
|
83
|
+
},
|
|
84
|
+
separator: { borderTop: '1px solid var(--dsw-alias-border-l2)' },
|
|
80
85
|
cardHeader: { display: 'flex', alignItems: 'center', gap: 8 },
|
|
81
86
|
dot: { width: 8, height: 8, borderRadius: '50%', flexShrink: 0 },
|
|
82
87
|
name: { fontWeight: 500, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
|
|
@@ -122,6 +127,45 @@ const styles = {
|
|
|
122
127
|
padding: '0 10px', font: 'inherit', fontSize: 14, lineHeight: '22px',
|
|
123
128
|
background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
|
|
124
129
|
},
|
|
130
|
+
deviceCode: {
|
|
131
|
+
marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
|
|
132
|
+
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
133
|
+
padding: '10px 12px', background: 'var(--dsw-alias-bg-layer-1)',
|
|
134
|
+
},
|
|
135
|
+
deviceCodeText: {
|
|
136
|
+
fontFamily: 'monospace', fontSize: 18, lineHeight: '24px', letterSpacing: 2,
|
|
137
|
+
color: 'var(--dsw-alias-label-primary)', userSelect: 'all',
|
|
138
|
+
},
|
|
139
|
+
proxyField: { display: 'flex', flexDirection: 'column', gap: 4 },
|
|
140
|
+
proxyLabel: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
|
|
141
|
+
proxyInput: {
|
|
142
|
+
height: 32, width: '100%', boxSizing: 'border-box',
|
|
143
|
+
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
144
|
+
padding: '0 10px', font: 'inherit', fontSize: 14, lineHeight: '22px',
|
|
145
|
+
background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
|
|
146
|
+
},
|
|
147
|
+
proxyHint: {
|
|
148
|
+
margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
|
|
149
|
+
},
|
|
150
|
+
proxyCheck: {
|
|
151
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
152
|
+
fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer',
|
|
153
|
+
},
|
|
154
|
+
proxyMessage: { margin: 0, fontSize: 12, lineHeight: '18px' },
|
|
155
|
+
proxyActions: { display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'flex-end', marginTop: 2 },
|
|
156
|
+
modalOverlay: {
|
|
157
|
+
position: 'fixed', inset: 0, zIndex: 1000,
|
|
158
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
159
|
+
background: 'rgba(0, 0, 0, 0.45)',
|
|
160
|
+
},
|
|
161
|
+
modal: {
|
|
162
|
+
width: 460, maxWidth: '100%', maxHeight: '90vh', overflowY: 'auto',
|
|
163
|
+
boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12,
|
|
164
|
+
padding: '16px 18px', borderRadius: 12,
|
|
165
|
+
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
166
|
+
},
|
|
167
|
+
modalHeader: { display: 'flex', alignItems: 'center', gap: 8 },
|
|
168
|
+
modalTitle: { fontWeight: 600, fontSize: 15, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
|
|
125
169
|
};
|
|
126
170
|
/** Status dot color for one provider state. */
|
|
127
171
|
function dotColor(status) {
|
|
@@ -178,6 +222,24 @@ function usageBarColor(usedPercent) {
|
|
|
178
222
|
return 'var(--dsw-alias-state-warn-label)';
|
|
179
223
|
return 'var(--dsw-alias-state-success-primary)';
|
|
180
224
|
}
|
|
225
|
+
/** One-line status text of the proxy config card. */
|
|
226
|
+
function proxyStatusText(t, proxy, loadError) {
|
|
227
|
+
if (loadError !== undefined)
|
|
228
|
+
return t('proxyLoadFailed', { message: loadError });
|
|
229
|
+
if (proxy === undefined)
|
|
230
|
+
return t('proxyLoading');
|
|
231
|
+
if (proxy.error !== undefined)
|
|
232
|
+
return t('proxyStatusError', { message: proxy.error });
|
|
233
|
+
if (proxy.enabled)
|
|
234
|
+
return t('proxyStatusEnabled', { url: proxy.url });
|
|
235
|
+
return t('proxyStatusNone');
|
|
236
|
+
}
|
|
237
|
+
/** Feedback-line color of the proxy dialog. */
|
|
238
|
+
function messageColor(tone) {
|
|
239
|
+
return tone === 'error'
|
|
240
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
241
|
+
: 'var(--dsw-alias-state-success-primary)';
|
|
242
|
+
}
|
|
181
243
|
/**
|
|
182
244
|
* The Subscriptions settings page component.
|
|
183
245
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|
|
@@ -189,8 +251,11 @@ export function SubscriptionsSection(props) {
|
|
|
189
251
|
const [statuses, setStatuses] = useState({});
|
|
190
252
|
const [errors, setErrors] = useState({});
|
|
191
253
|
const [manualDrafts, setManualDrafts] = useState({
|
|
192
|
-
codex: '', claude: '', grok: '',
|
|
254
|
+
codex: '', claude: '', grok: '', copilot: '',
|
|
193
255
|
});
|
|
256
|
+
/** Pending device-flow codes (copilot), shown while the attempt polls. */
|
|
257
|
+
const [deviceCodes, setDeviceCodes] = useState({});
|
|
258
|
+
const [copiedCode, setCopiedCode] = useState(undefined);
|
|
194
259
|
const [usages, setUsages] = useState({});
|
|
195
260
|
const [usageErrors, setUsageErrors] = useState({});
|
|
196
261
|
const [usageLoading, setUsageLoading] = useState({});
|
|
@@ -198,6 +263,21 @@ export function SubscriptionsSection(props) {
|
|
|
198
263
|
const pollersRef = useRef(new Map());
|
|
199
264
|
/** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
|
|
200
265
|
const usageInflightRef = useRef(new Set());
|
|
266
|
+
/** Proxy config as last answered by `proxyGet`/`proxySet`. */
|
|
267
|
+
const [proxy, setProxy] = useState(undefined);
|
|
268
|
+
const [proxyLoadError, setProxyLoadError] = useState(undefined);
|
|
269
|
+
/** Proxy dialog state (draft fields; the password never pre-fills). */
|
|
270
|
+
const [proxyOpen, setProxyOpen] = useState(false);
|
|
271
|
+
const [proxyEnabled, setProxyEnabled] = useState(false);
|
|
272
|
+
const [proxyUrl, setProxyUrl] = useState('');
|
|
273
|
+
const [proxyUsername, setProxyUsername] = useState('');
|
|
274
|
+
const [proxyPassword, setProxyPassword] = useState('');
|
|
275
|
+
const [proxyClearPassword, setProxyClearPassword] = useState(false);
|
|
276
|
+
const [proxyBypass, setProxyBypass] = useState('');
|
|
277
|
+
const [proxySaving, setProxySaving] = useState(false);
|
|
278
|
+
const [proxyTesting, setProxyTesting] = useState(false);
|
|
279
|
+
const [proxyMessage, setProxyMessage] = useState(undefined);
|
|
280
|
+
const [proxyTestResult, setProxyTestResult] = useState(undefined);
|
|
201
281
|
const setProviderError = useCallback((provider, message) => {
|
|
202
282
|
if (!mountedRef.current)
|
|
203
283
|
return;
|
|
@@ -235,8 +315,17 @@ export function SubscriptionsSection(props) {
|
|
|
235
315
|
setStatuses(response.providers);
|
|
236
316
|
for (const { id } of PROVIDERS) {
|
|
237
317
|
const status = response.providers[id];
|
|
238
|
-
if (status.loggedIn || !status.busy)
|
|
318
|
+
if (status.loggedIn || !status.busy) {
|
|
239
319
|
stopPolling(id);
|
|
320
|
+
// The attempt settled (success, timeout, or cancel): drop the code card.
|
|
321
|
+
setDeviceCodes((prev) => {
|
|
322
|
+
if (prev[id] === undefined)
|
|
323
|
+
return prev;
|
|
324
|
+
const next = { ...prev };
|
|
325
|
+
delete next[id];
|
|
326
|
+
return next;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
240
329
|
}
|
|
241
330
|
}, [rpc, stopPolling]);
|
|
242
331
|
const startPolling = useCallback((provider) => {
|
|
@@ -332,11 +421,18 @@ export function SubscriptionsSection(props) {
|
|
|
332
421
|
if (typeof response.authorizeUrl !== 'string') {
|
|
333
422
|
throw new SubscriptionsAuthError(t('loginMissingUrl'));
|
|
334
423
|
}
|
|
335
|
-
window.open(response.authorizeUrl, '_blank', 'noopener');
|
|
336
424
|
if (!mountedRef.current)
|
|
337
425
|
return;
|
|
338
426
|
// Optimistic busy so Cancel and the manual fallback appear before the first poll tick.
|
|
339
427
|
setStatuses(prev => ({ ...prev, [provider]: { ...prev[provider], busy: true, loggedIn: false } }));
|
|
428
|
+
if (typeof response.userCode === 'string' && response.userCode.length > 0) {
|
|
429
|
+
// Device flow: show the code card instead of opening the page blind —
|
|
430
|
+
// the user copies the code first, then opens the verification page.
|
|
431
|
+
setDeviceCodes(prev => ({ ...prev, [provider]: { userCode: response.userCode, verificationUrl: response.authorizeUrl } }));
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
window.open(response.authorizeUrl, '_blank', 'noopener');
|
|
435
|
+
}
|
|
340
436
|
startPolling(provider);
|
|
341
437
|
}
|
|
342
438
|
catch (error) {
|
|
@@ -386,12 +482,117 @@ export function SubscriptionsSection(props) {
|
|
|
386
482
|
}
|
|
387
483
|
await refresh();
|
|
388
484
|
}, [rpc, t, setProviderError, refresh]);
|
|
485
|
+
const copyDeviceCode = useCallback((provider, userCode) => {
|
|
486
|
+
void navigator.clipboard?.writeText(userCode).then(() => {
|
|
487
|
+
if (!mountedRef.current)
|
|
488
|
+
return;
|
|
489
|
+
setCopiedCode(provider);
|
|
490
|
+
setTimeout(() => {
|
|
491
|
+
if (mountedRef.current) {
|
|
492
|
+
setCopiedCode(current => current === provider ? undefined : current);
|
|
493
|
+
}
|
|
494
|
+
}, 1500);
|
|
495
|
+
}).catch(() => undefined);
|
|
496
|
+
}, []);
|
|
497
|
+
// Proxy configuration: load once on mount; the dialog drives proxySet/proxyTest.
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
if (rpc === undefined)
|
|
500
|
+
return;
|
|
501
|
+
let alive = true;
|
|
502
|
+
void callSubscriptionsAuth(rpc, 'proxyGet', {}).then((view) => {
|
|
503
|
+
if (!alive)
|
|
504
|
+
return;
|
|
505
|
+
setProxy(view);
|
|
506
|
+
setProxyLoadError(undefined);
|
|
507
|
+
}).catch((error) => {
|
|
508
|
+
if (alive)
|
|
509
|
+
setProxyLoadError(messageOf(error));
|
|
510
|
+
});
|
|
511
|
+
return () => { alive = false; };
|
|
512
|
+
}, [rpc]);
|
|
513
|
+
useEffect(() => {
|
|
514
|
+
if (!proxyOpen)
|
|
515
|
+
return;
|
|
516
|
+
const onKey = (event) => {
|
|
517
|
+
if (event.key === 'Escape')
|
|
518
|
+
setProxyOpen(false);
|
|
519
|
+
};
|
|
520
|
+
window.addEventListener('keydown', onKey);
|
|
521
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
522
|
+
}, [proxyOpen]);
|
|
523
|
+
const openProxyDialog = useCallback(() => {
|
|
524
|
+
if (proxy === undefined)
|
|
525
|
+
return;
|
|
526
|
+
setProxyEnabled(proxy.enabled);
|
|
527
|
+
setProxyUrl(proxy.url);
|
|
528
|
+
setProxyUsername(proxy.username ?? '');
|
|
529
|
+
setProxyPassword('');
|
|
530
|
+
setProxyClearPassword(false);
|
|
531
|
+
setProxyBypass(proxy.bypass.join(', '));
|
|
532
|
+
setProxyMessage(undefined);
|
|
533
|
+
setProxyTestResult(undefined);
|
|
534
|
+
setProxyOpen(true);
|
|
535
|
+
}, [proxy]);
|
|
536
|
+
const saveProxy = useCallback(async () => {
|
|
537
|
+
if (rpc === undefined)
|
|
538
|
+
return;
|
|
539
|
+
setProxySaving(true);
|
|
540
|
+
setProxyMessage(undefined);
|
|
541
|
+
try {
|
|
542
|
+
const view = await callSubscriptionsAuth(rpc, 'proxySet', {
|
|
543
|
+
enabled: proxyEnabled,
|
|
544
|
+
url: proxyUrl.trim(),
|
|
545
|
+
username: proxyUsername,
|
|
546
|
+
...proxyClearPassword ? { password: null } : proxyPassword !== '' ? { password: proxyPassword } : {},
|
|
547
|
+
bypass: proxyBypass.split(/[,\n]/).map(entry => entry.trim()).filter(entry => entry !== ''),
|
|
548
|
+
});
|
|
549
|
+
setProxy(view);
|
|
550
|
+
setProxyLoadError(undefined);
|
|
551
|
+
setProxyMessage({ tone: 'success', text: t('proxySaved') });
|
|
552
|
+
setProxyOpen(false);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
setProxyMessage({ tone: 'error', text: t('proxySaveFailed', { message: messageOf(error) }) });
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
setProxySaving(false);
|
|
559
|
+
}
|
|
560
|
+
}, [rpc, proxyEnabled, proxyUrl, proxyUsername, proxyPassword, proxyClearPassword, proxyBypass, t]);
|
|
561
|
+
const testProxy = useCallback(async () => {
|
|
562
|
+
if (rpc === undefined || proxyTesting)
|
|
563
|
+
return;
|
|
564
|
+
setProxyTesting(true);
|
|
565
|
+
setProxyTestResult(undefined);
|
|
566
|
+
try {
|
|
567
|
+
// Test the dialog's current inputs (they do not need to be saved first);
|
|
568
|
+
// the host builds a throwaway agent for the probe.
|
|
569
|
+
setProxyTestResult(await callSubscriptionsAuth(rpc, 'proxyTest', {
|
|
570
|
+
proxy: {
|
|
571
|
+
url: proxyUrl.trim(),
|
|
572
|
+
...proxyUsername.trim() !== '' ? { username: proxyUsername.trim() } : {},
|
|
573
|
+
...proxyPassword !== '' ? { password: proxyPassword } : {},
|
|
574
|
+
},
|
|
575
|
+
}));
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
setProxyTestResult({ ok: false, viaProxy: false, error: messageOf(error) });
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
setProxyTesting(false);
|
|
582
|
+
}
|
|
583
|
+
}, [rpc, proxyTesting, proxyUrl, proxyUsername, proxyPassword]);
|
|
389
584
|
if (rpc === undefined) {
|
|
390
585
|
return _jsx("p", { style: styles.intro, children: t('unavailable') });
|
|
391
586
|
}
|
|
392
|
-
return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }),
|
|
587
|
+
return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), _jsxs("div", { style: styles.proxyCard, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: {
|
|
588
|
+
...styles.dot,
|
|
589
|
+
background: proxy?.enabled === true
|
|
590
|
+
? 'var(--dsw-alias-state-success-primary)'
|
|
591
|
+
: 'var(--dsw-alias-label-dimmed)',
|
|
592
|
+
} }), _jsx("span", { style: styles.name, children: t('proxyTitle') }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: openProxyDialog, children: t('proxyConfigure') })] }), _jsx("p", { style: styles.statusLine, children: proxyStatusText(t, proxy, proxyLoadError) })] }), _jsx("div", { style: styles.separator }), PROVIDERS.map(({ id, name }) => {
|
|
393
593
|
const status = statuses[id];
|
|
394
594
|
const busy = status?.busy === true;
|
|
595
|
+
const deviceCode = deviceCodes[id];
|
|
395
596
|
const usage = usages[id];
|
|
396
597
|
const usageError = usageErrors[id];
|
|
397
598
|
// Providers without a usage endpoint answer supported:false — no block.
|
|
@@ -401,6 +602,15 @@ export function SubscriptionsSection(props) {
|
|
|
401
602
|
const percent = Math.min(100, Math.max(0, window.usedPercent));
|
|
402
603
|
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${String(Math.round(percent))}%`, window.resetsAt !== undefined
|
|
403
604
|
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${String(percent)}%`, background: usageBarColor(percent) } }) })] }, index));
|
|
404
|
-
})] })), busy && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
405
|
-
})] }))
|
|
605
|
+
})] })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
606
|
+
}), proxyOpen && (_jsx("div", { style: styles.modalOverlay, onClick: () => setProxyOpen(false), children: _jsxs("div", { style: styles.modal, onClick: event => event.stopPropagation(), children: [_jsxs("div", { style: styles.modalHeader, children: [_jsx("span", { style: styles.modalTitle, children: t('proxyDialogTitle') }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto' }, onClick: () => setProxyOpen(false), children: t('proxyDialogClose') })] }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyEnabled, onChange: event => setProxyEnabled(event.target.checked) }), _jsx("span", { children: t('proxyEnabled') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUrl') }), _jsx("input", { style: styles.proxyInput, value: proxyUrl, placeholder: t('proxyUrlPlaceholder'), onChange: event => setProxyUrl(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyUrlHint') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUsername') }), _jsx("input", { style: styles.proxyInput, value: proxyUsername, placeholder: t('proxyUsernamePlaceholder'), onChange: event => setProxyUsername(event.target.value) })] }), _jsxs("div", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyPassword') }), _jsx("input", { type: "password", style: styles.proxyInput, value: proxyPassword, placeholder: t('proxyPasswordPlaceholder'), onChange: event => setProxyPassword(event.target.value) }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyClearPassword, onChange: event => setProxyClearPassword(event.target.checked) }), _jsx("span", { children: t('proxyClearPassword') })] })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyBypass') }), _jsx("input", { style: styles.proxyInput, value: proxyBypass, placeholder: t('proxyBypassPlaceholder'), onChange: event => setProxyBypass(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyBypassHint') })] }), _jsx("p", { style: styles.proxyHint, children: t('proxyNote') }), proxyMessage !== undefined && (_jsx("p", { style: { ...styles.proxyMessage, color: messageColor(proxyMessage.tone) }, children: proxyMessage.text })), proxyTestResult !== undefined && (_jsx("p", { style: {
|
|
607
|
+
...styles.proxyMessage,
|
|
608
|
+
color: proxyTestResult.ok
|
|
609
|
+
? 'var(--dsw-alias-state-success-primary)'
|
|
610
|
+
: 'var(--dsw-alias-state-error-primary)',
|
|
611
|
+
}, children: proxyTestResult.ok
|
|
612
|
+
? (proxyTestResult.viaProxy
|
|
613
|
+
? t('proxyTestOk', { status: String(proxyTestResult.status), ms: String(proxyTestResult.latencyMs) })
|
|
614
|
+
: t('proxyTestOkDirect', { status: String(proxyTestResult.status), ms: String(proxyTestResult.latencyMs) }))
|
|
615
|
+
: t('proxyTestFail', { message: proxyTestResult.error ?? '' }) })), _jsxs("div", { style: styles.proxyActions, children: [_jsx("button", { type: "button", style: { ...styles.button, ...proxyTesting ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: proxyTesting, onClick: () => { void testProxy(); }, children: proxyTesting ? t('proxyTesting') : t('proxyTest') }), _jsx("button", { type: "button", style: { ...styles.button, ...proxySaving ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: proxySaving, onClick: () => { void saveProxy(); }, children: proxySaving ? t('proxySaving') : t('proxySave') }), _jsx("button", { type: "button", style: styles.button, onClick: () => setProxyOpen(false), children: t('proxyCancel') })] })] }) }))] }));
|
|
406
616
|
}
|
package/lib/client/index.js
CHANGED
|
@@ -22,6 +22,17 @@ export const inject = ['slots', 'connection', 'locale'];
|
|
|
22
22
|
*/
|
|
23
23
|
export function apply(ctx) {
|
|
24
24
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-subscriptions: copy dictionaries');
|
|
25
|
+
// Settings-shell nudge: the panel (nav title + header row + section body)
|
|
26
|
+
// sits flush against the panel's top edge; push it down a little to leave
|
|
27
|
+
// breathing room. Scoped by the settings panel's own dialog role + nav child
|
|
28
|
+
// so other aria-modal dialogs (e.g. the attachment lightbox) are untouched.
|
|
29
|
+
ctx.effect(() => {
|
|
30
|
+
const style = document.createElement('style');
|
|
31
|
+
style.setAttribute('data-plugin', 'dsh-plugin-subscriptions');
|
|
32
|
+
style.textContent = 'div[role="dialog"][aria-modal="true"]:has(> nav) { padding-top: 14px; }';
|
|
33
|
+
document.head.appendChild(style);
|
|
34
|
+
return () => style.remove();
|
|
35
|
+
}, 'dsh-plugin-subscriptions: settings panel breathing room');
|
|
25
36
|
// The client-runtime Context merge types `connection` as the host handle;
|
|
26
37
|
// in the browser shell the same key holds the full client ConnectionHandle.
|
|
27
38
|
const connection = ctx.get('connection');
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export declare const en: {
|
|
|
19
19
|
manualPlaceholder: string;
|
|
20
20
|
submit: string;
|
|
21
21
|
loginMissingUrl: string;
|
|
22
|
+
deviceCodePrompt: string;
|
|
23
|
+
deviceCodeCopy: string;
|
|
24
|
+
deviceCodeCopied: string;
|
|
25
|
+
deviceCodeOpenPage: string;
|
|
22
26
|
usageTitle: string;
|
|
23
27
|
usageRefresh: string;
|
|
24
28
|
usageLoading: string;
|
|
@@ -47,6 +51,38 @@ export declare const en: {
|
|
|
47
51
|
speedFastDescription: string;
|
|
48
52
|
commandFast: string;
|
|
49
53
|
commandFastUnavailable: string;
|
|
54
|
+
proxyTitle: string;
|
|
55
|
+
proxyStatusNone: string;
|
|
56
|
+
proxyStatusEnabled: string;
|
|
57
|
+
proxyStatusError: string;
|
|
58
|
+
proxyConfigure: string;
|
|
59
|
+
proxyDialogTitle: string;
|
|
60
|
+
proxyDialogClose: string;
|
|
61
|
+
proxyEnabled: string;
|
|
62
|
+
proxyUrl: string;
|
|
63
|
+
proxyUrlPlaceholder: string;
|
|
64
|
+
proxyUrlHint: string;
|
|
65
|
+
proxyUsername: string;
|
|
66
|
+
proxyUsernamePlaceholder: string;
|
|
67
|
+
proxyPassword: string;
|
|
68
|
+
proxyPasswordPlaceholder: string;
|
|
69
|
+
proxyClearPassword: string;
|
|
70
|
+
proxyBypass: string;
|
|
71
|
+
proxyBypassPlaceholder: string;
|
|
72
|
+
proxyBypassHint: string;
|
|
73
|
+
proxyTest: string;
|
|
74
|
+
proxyTesting: string;
|
|
75
|
+
proxyTestOk: string;
|
|
76
|
+
proxyTestOkDirect: string;
|
|
77
|
+
proxyTestFail: string;
|
|
78
|
+
proxySave: string;
|
|
79
|
+
proxyCancel: string;
|
|
80
|
+
proxySaving: string;
|
|
81
|
+
proxyLoading: string;
|
|
82
|
+
proxyLoadFailed: string;
|
|
83
|
+
proxySaved: string;
|
|
84
|
+
proxySaveFailed: string;
|
|
85
|
+
proxyNote: string;
|
|
50
86
|
};
|
|
51
87
|
/** zh strings, one per {@link en} key. */
|
|
52
88
|
export declare const zh: {
|
|
@@ -68,6 +104,10 @@ export declare const zh: {
|
|
|
68
104
|
manualPlaceholder: string;
|
|
69
105
|
submit: string;
|
|
70
106
|
loginMissingUrl: string;
|
|
107
|
+
deviceCodePrompt: string;
|
|
108
|
+
deviceCodeCopy: string;
|
|
109
|
+
deviceCodeCopied: string;
|
|
110
|
+
deviceCodeOpenPage: string;
|
|
71
111
|
usageTitle: string;
|
|
72
112
|
usageRefresh: string;
|
|
73
113
|
usageLoading: string;
|
|
@@ -96,6 +136,38 @@ export declare const zh: {
|
|
|
96
136
|
speedFastDescription: string;
|
|
97
137
|
commandFast: string;
|
|
98
138
|
commandFastUnavailable: string;
|
|
139
|
+
proxyTitle: string;
|
|
140
|
+
proxyStatusNone: string;
|
|
141
|
+
proxyStatusEnabled: string;
|
|
142
|
+
proxyStatusError: string;
|
|
143
|
+
proxyConfigure: string;
|
|
144
|
+
proxyDialogTitle: string;
|
|
145
|
+
proxyDialogClose: string;
|
|
146
|
+
proxyEnabled: string;
|
|
147
|
+
proxyUrl: string;
|
|
148
|
+
proxyUrlPlaceholder: string;
|
|
149
|
+
proxyUrlHint: string;
|
|
150
|
+
proxyUsername: string;
|
|
151
|
+
proxyUsernamePlaceholder: string;
|
|
152
|
+
proxyPassword: string;
|
|
153
|
+
proxyPasswordPlaceholder: string;
|
|
154
|
+
proxyClearPassword: string;
|
|
155
|
+
proxyBypass: string;
|
|
156
|
+
proxyBypassPlaceholder: string;
|
|
157
|
+
proxyBypassHint: string;
|
|
158
|
+
proxyTest: string;
|
|
159
|
+
proxyTesting: string;
|
|
160
|
+
proxyTestOk: string;
|
|
161
|
+
proxyTestOkDirect: string;
|
|
162
|
+
proxyTestFail: string;
|
|
163
|
+
proxySave: string;
|
|
164
|
+
proxyCancel: string;
|
|
165
|
+
proxySaving: string;
|
|
166
|
+
proxyLoading: string;
|
|
167
|
+
proxyLoadFailed: string;
|
|
168
|
+
proxySaved: string;
|
|
169
|
+
proxySaveFailed: string;
|
|
170
|
+
proxyNote: string;
|
|
99
171
|
};
|
|
100
172
|
/** The Subscriptions namespace key union (en is the key-set source of truth). */
|
|
101
173
|
export type SubscriptionsKey = keyof typeof en;
|
package/lib/client/locales.js
CHANGED
|
@@ -19,6 +19,10 @@ export const en = {
|
|
|
19
19
|
manualPlaceholder: 'Paste the callback URL or code',
|
|
20
20
|
submit: 'Submit',
|
|
21
21
|
loginMissingUrl: 'login answered without an authorizeUrl',
|
|
22
|
+
deviceCodePrompt: 'Enter this code on the GitHub verification page:',
|
|
23
|
+
deviceCodeCopy: 'Copy code',
|
|
24
|
+
deviceCodeCopied: 'Copied',
|
|
25
|
+
deviceCodeOpenPage: 'Open GitHub verification page',
|
|
22
26
|
usageTitle: 'Usage',
|
|
23
27
|
usageRefresh: 'Refresh',
|
|
24
28
|
usageLoading: 'Loading usage…',
|
|
@@ -47,6 +51,38 @@ export const en = {
|
|
|
47
51
|
speedFastDescription: '1.5x speed, more usage',
|
|
48
52
|
commandFast: 'Switch the Codex speed tier (Standard/Fast)',
|
|
49
53
|
commandFastUnavailable: 'The current model has no fast tier; /fast only works on Codex models whose catalog advertises one',
|
|
54
|
+
proxyTitle: 'Proxy',
|
|
55
|
+
proxyStatusNone: 'Not configured — subscription requests go direct.',
|
|
56
|
+
proxyStatusEnabled: 'Enabled · {url}',
|
|
57
|
+
proxyStatusError: 'Config error: {message}',
|
|
58
|
+
proxyConfigure: 'Configure…',
|
|
59
|
+
proxyDialogTitle: 'Proxy settings',
|
|
60
|
+
proxyDialogClose: 'Close',
|
|
61
|
+
proxyEnabled: 'Route subscription requests through a proxy',
|
|
62
|
+
proxyUrl: 'Proxy URL',
|
|
63
|
+
proxyUrlPlaceholder: 'http://localhost:7890',
|
|
64
|
+
proxyUrlHint: 'HTTP or HTTPS proxy only (Clash/mihomo, v2rayN…); socks is not supported.',
|
|
65
|
+
proxyUsername: 'Username (optional)',
|
|
66
|
+
proxyUsernamePlaceholder: 'Proxy username',
|
|
67
|
+
proxyPassword: 'Password',
|
|
68
|
+
proxyPasswordPlaceholder: 'Leave blank to keep the saved password',
|
|
69
|
+
proxyClearPassword: 'Clear the saved password',
|
|
70
|
+
proxyBypass: 'Bypass hosts',
|
|
71
|
+
proxyBypassPlaceholder: '127.0.0.1, localhost, *.example.com',
|
|
72
|
+
proxyBypassHint: 'Comma-separated hostnames that keep going direct.',
|
|
73
|
+
proxyTest: 'Test',
|
|
74
|
+
proxyTesting: 'Testing…',
|
|
75
|
+
proxyTestOk: 'OK · HTTP {status} · {ms} ms',
|
|
76
|
+
proxyTestOkDirect: 'OK (direct, bypassed) · HTTP {status} · {ms} ms',
|
|
77
|
+
proxyTestFail: 'Failed: {message}',
|
|
78
|
+
proxySave: 'Save',
|
|
79
|
+
proxyCancel: 'Cancel',
|
|
80
|
+
proxySaving: 'Saving…',
|
|
81
|
+
proxyLoading: 'Loading proxy settings…',
|
|
82
|
+
proxyLoadFailed: 'Failed to load proxy settings: {message}',
|
|
83
|
+
proxySaved: 'Saved — new requests use the proxy.',
|
|
84
|
+
proxySaveFailed: 'Save failed: {message}',
|
|
85
|
+
proxyNote: 'Applies to token exchange, model APIs, usage lookups, image/video generation and x_search. The OAuth authorization page opens in your browser and follows the browser/system proxy, not this setting.',
|
|
50
86
|
};
|
|
51
87
|
/** zh strings, one per {@link en} key. */
|
|
52
88
|
export const zh = {
|
|
@@ -68,6 +104,10 @@ export const zh = {
|
|
|
68
104
|
manualPlaceholder: '粘贴回调 URL 或授权码',
|
|
69
105
|
submit: '提交',
|
|
70
106
|
loginMissingUrl: 'login 响应缺少 authorizeUrl',
|
|
107
|
+
deviceCodePrompt: '在 GitHub 验证页面输入此验证码:',
|
|
108
|
+
deviceCodeCopy: '复制验证码',
|
|
109
|
+
deviceCodeCopied: '已复制',
|
|
110
|
+
deviceCodeOpenPage: '打开 GitHub 验证页面',
|
|
71
111
|
usageTitle: '用量',
|
|
72
112
|
usageRefresh: '刷新',
|
|
73
113
|
usageLoading: '用量加载中…',
|
|
@@ -96,4 +136,36 @@ export const zh = {
|
|
|
96
136
|
speedFastDescription: '约 1.5 倍速度,消耗更多用量',
|
|
97
137
|
commandFast: '切换 Codex 速度档(标准/快速)',
|
|
98
138
|
commandFastUnavailable: '当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用',
|
|
139
|
+
proxyTitle: '代理',
|
|
140
|
+
proxyStatusNone: '未配置 —— 订阅请求直连。',
|
|
141
|
+
proxyStatusEnabled: '已启用 · {url}',
|
|
142
|
+
proxyStatusError: '配置错误:{message}',
|
|
143
|
+
proxyConfigure: '配置…',
|
|
144
|
+
proxyDialogTitle: '代理设置',
|
|
145
|
+
proxyDialogClose: '关闭',
|
|
146
|
+
proxyEnabled: '让订阅相关请求走代理',
|
|
147
|
+
proxyUrl: '代理地址',
|
|
148
|
+
proxyUrlPlaceholder: 'http://localhost:7890',
|
|
149
|
+
proxyUrlHint: '仅支持 HTTP/HTTPS 代理(Clash/mihomo、v2rayN 等);不支持 socks。',
|
|
150
|
+
proxyUsername: '用户名(可选)',
|
|
151
|
+
proxyUsernamePlaceholder: '代理用户名',
|
|
152
|
+
proxyPassword: '密码',
|
|
153
|
+
proxyPasswordPlaceholder: '留空则保留已保存的密码',
|
|
154
|
+
proxyClearPassword: '清除已保存的密码',
|
|
155
|
+
proxyBypass: '绕过主机',
|
|
156
|
+
proxyBypassPlaceholder: '127.0.0.1, localhost, *.example.com',
|
|
157
|
+
proxyBypassHint: '逗号分隔的主机名,这些主机保持直连。',
|
|
158
|
+
proxyTest: '测试',
|
|
159
|
+
proxyTesting: '测试中…',
|
|
160
|
+
proxyTestOk: '成功 · HTTP {status} · {ms} ms',
|
|
161
|
+
proxyTestOkDirect: '成功(直连,已绕过)· HTTP {status} · {ms} ms',
|
|
162
|
+
proxyTestFail: '失败:{message}',
|
|
163
|
+
proxySave: '保存',
|
|
164
|
+
proxyCancel: '取消',
|
|
165
|
+
proxySaving: '保存中…',
|
|
166
|
+
proxyLoading: '代理设置加载中…',
|
|
167
|
+
proxyLoadFailed: '代理设置加载失败:{message}',
|
|
168
|
+
proxySaved: '已保存 —— 后续请求将走代理。',
|
|
169
|
+
proxySaveFailed: '保存失败:{message}',
|
|
170
|
+
proxyNote: '作用于 token 交换、模型 API、用量查询、图片/视频生成与 x_search。OAuth 授权页在浏览器中打开,走的是浏览器/系统代理,不受此设置影响。',
|
|
99
171
|
};
|