dsh-plugin-subscriptions 0.5.1 → 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 CHANGED
@@ -132,6 +132,12 @@ catalog does not know would otherwise default to `/chat/completions`, which
132
132
  responses-only families (gpt-5.5/5.6, …) reject. Pinning `chat-completions` also opts
133
133
  out of the tools+effort auto-reroute described above.
134
134
 
135
+ ## Proxy
136
+
137
+ Every subscription request — token exchanges, model-API streams, usage lookups, model discovery, and the `x_search` / `image_generate` / `video_generate` tools — can be routed through an HTTP(S) proxy. Configure it in **Settings → Subscriptions → Proxy → Configure…**: enable the flag, enter the proxy URL (`http://127.0.0.1:7890`), optional username/password, and an optional comma-separated bypass list of hostnames that stay direct (`127.0.0.1`, `localhost`, `*.example.com`). The password is stored in `~/.dsh/plugins/subscriptions/proxy.json` (mode 0600) and is never returned to the browser. A "Test" button probes one endpoint through the current configuration and shows the HTTP status/latency.
138
+
139
+ Changes apply immediately to subsequent requests — no restart needed. The OAuth authorization page opens in your browser and follows the browser/system proxy, not this setting. SOCKS proxies are not supported.
140
+
135
141
  ## Develop
136
142
 
137
143
  ```sh
package/README.zh.md CHANGED
@@ -131,6 +131,12 @@ GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions`
131
131
  responses-only 系列(gpt-5.5/5.6 等)会拒绝该端点。固定为 `chat-completions` 也会退出上文所述
132
132
  tools+effort 的自动改道。
133
133
 
134
+ ## 代理
135
+
136
+ 所有订阅相关请求 —— token 交换、模型 API 流式调用、用量查询、模型目录发现,以及 `x_search` / `image_generate` / `video_generate` 工具 —— 都可以通过 HTTP(S) 代理发出。在 **设置 → 订阅 → 代理 → 配置…** 中设置:勾选启用,填写代理地址(`http://127.0.0.1:7890`)、可选用户名/密码,以及可选的逗号分隔绕过列表(保持直连的主机名,如 `127.0.0.1`、`localhost`、`*.example.com`)。密码保存在 `~/.dsh/plugins/subscriptions/proxy.json`(权限 0600),不会回传给浏览器;「测试」按钮会用当前配置探测一次端点,显示 HTTP 状态码与耗时。
137
+
138
+ 保存后立即对后续请求生效,无需重启。OAuth 授权页在浏览器中打开,走浏览器/系统自身的代理设置,不受此配置影响;不支持 socks 代理。
139
+
134
140
  ## 开发
135
141
 
136
142
  ```sh
@@ -1,12 +1,3 @@
1
- /**
2
- * GitHub OAuth device-authorization flow (RFC 8628) for providers that cannot
3
- * use the loopback redirect engine: no redirect URI, no PKCE, no client
4
- * secret. The user opens a verification URL and types a short code while the
5
- * plugin polls the token endpoint until GitHub releases the access token.
6
- * The management model (one attempt per provider, `isBusy`/`pending`/`cancel`)
7
- * mirrors {@link OAuthFlowManager} so the auth controller can treat both
8
- * engines uniformly.
9
- */
10
1
  /** Static per-provider device-flow facts. */
11
2
  export interface DeviceFlowSpec {
12
3
  /** OAuth App / GitHub App client id the device code is requested for. */
@@ -7,6 +7,7 @@
7
7
  * mirrors {@link OAuthFlowManager} so the auth controller can treat both
8
8
  * engines uniformly.
9
9
  */
10
+ import { proxiedFetch } from '../http.js';
10
11
  /** Default poll interval when the device-code response omits one. */
11
12
  const DEFAULT_INTERVAL_SEC = 5;
12
13
  /** Default device-code lifetime when the response omits one (GitHub: 15 minutes). */
@@ -64,7 +65,7 @@ export class DeviceFlowManager {
64
65
  if (this.attempts.has(provider)) {
65
66
  throw new Error(`a ${provider} login attempt is already in progress`);
66
67
  }
67
- const fetchFn = spec.fetchFn ?? fetch;
68
+ const fetchFn = spec.fetchFn ?? proxiedFetch;
68
69
  const response = await fetchFn(spec.deviceCodeUrl, {
69
70
  method: 'POST',
70
71
  headers: {
package/lib/auth/rpc.d.ts CHANGED
@@ -8,6 +8,7 @@ import type { Context } from '@deepseek-ai/cordis';
8
8
  import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
9
9
  import { type ProviderId } from './store.js';
10
10
  import type { ProviderUsage } from '../providers/common.js';
11
+ import type { ProxyConfigView, ProxyDraft, ProxyInput, ProxyTestResult } from '../http.js';
11
12
  /** The RPC channel this plugin registers on the host connection. */
12
13
  export declare const SUBSCRIPTIONS_AUTH_CHANNEL = "/subscriptions-auth";
13
14
  /** Decoded image bytes returned by the `image` endpoint. */
@@ -49,6 +50,18 @@ export interface ProviderStatus {
49
50
  /** Subscription detail (plan) or the last login error. */
50
51
  detail?: string;
51
52
  }
53
+ /** Proxy config operations behind the `proxyGet/proxySet/proxyTest` endpoints. */
54
+ export interface ProxyConfigController {
55
+ /** Current proxy configuration (secrets omitted). */
56
+ get(): Promise<ProxyConfigView>;
57
+ /** Validate, persist, and apply one config. */
58
+ set(input: ProxyInput): Promise<ProxyConfigView>;
59
+ /** Probe one destination through the draft (unsaved) or stored proxy. */
60
+ test(payload: {
61
+ url?: string;
62
+ proxy?: ProxyDraft;
63
+ }): Promise<ProxyTestResult>;
64
+ }
52
65
  /** Provider-agnostic auth operations the RPC handler delegates to. */
53
66
  export interface AuthController {
54
67
  /** Current status of one provider. */
@@ -102,5 +115,6 @@ export interface AuthController {
102
115
  * @param ctx - the plugin context (headless profiles have no `connection`).
103
116
  * @param controller - the auth operations backing the endpoints.
104
117
  * @param speed - the per-session speed-tier state backing the Speed toggle.
118
+ * @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
105
119
  */
106
- export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController): void;
120
+ export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, proxy?: ProxyConfigController | undefined): void;
package/lib/auth/rpc.js CHANGED
@@ -102,7 +102,89 @@ function readSessionId(payload) {
102
102
  throw new BadRequest('payload must be an object');
103
103
  return readString(payload, 'sessionId');
104
104
  }
105
- async function dispatch(controller, speed, endpoint, payload, signal) {
105
+ /** Validate a `proxySet` payload into a shape `ProxyInput` accepts. */
106
+ function readProxyInput(payload) {
107
+ if (typeof payload !== 'object' || payload === null)
108
+ throw new BadRequest('payload must be an object');
109
+ const record = payload;
110
+ if (typeof record.enabled !== 'boolean')
111
+ throw new BadRequest('payload.enabled must be a boolean');
112
+ if (typeof record.url !== 'string')
113
+ throw new BadRequest('payload.url must be a string');
114
+ let username;
115
+ if (record.username !== undefined) {
116
+ if (typeof record.username !== 'string')
117
+ throw new BadRequest('payload.username must be a string when present');
118
+ username = record.username;
119
+ }
120
+ let password;
121
+ if (record.password !== undefined) {
122
+ if (record.password !== null && typeof record.password !== 'string') {
123
+ throw new BadRequest('payload.password must be a string or null when present');
124
+ }
125
+ password = record.password;
126
+ }
127
+ let bypass;
128
+ if (record.bypass !== undefined) {
129
+ if (!Array.isArray(record.bypass) || record.bypass.some(entry => typeof entry !== 'string')) {
130
+ throw new BadRequest('payload.bypass must be an array of strings when present');
131
+ }
132
+ bypass = record.bypass;
133
+ }
134
+ return {
135
+ enabled: record.enabled,
136
+ url: record.url,
137
+ ...username === undefined ? {} : { username },
138
+ ...password === undefined ? {} : { password },
139
+ ...bypass === undefined ? {} : { bypass },
140
+ };
141
+ }
142
+ /** Validate a `proxyTest` payload (the destination URL and an optional draft). */
143
+ function readProxyTestPayload(payload) {
144
+ if (typeof payload !== 'object' || payload === null)
145
+ return {};
146
+ const record = payload;
147
+ const url = record.url;
148
+ if (url === undefined && record.proxy === undefined)
149
+ return {};
150
+ if (url !== undefined && (typeof url !== 'string' || url.length === 0)) {
151
+ throw new BadRequest('payload.url must be a non-empty string when present');
152
+ }
153
+ let proxy;
154
+ if (record.proxy !== undefined) {
155
+ if (typeof record.proxy !== 'object' || record.proxy === null) {
156
+ throw new BadRequest('payload.proxy must be an object when present');
157
+ }
158
+ const draftRecord = record.proxy;
159
+ if (typeof draftRecord.url !== 'string' || draftRecord.url.length === 0) {
160
+ throw new BadRequest('payload.proxy.url must be a non-empty string');
161
+ }
162
+ let username;
163
+ if (draftRecord.username !== undefined) {
164
+ if (typeof draftRecord.username !== 'string') {
165
+ throw new BadRequest('payload.proxy.username must be a string when present');
166
+ }
167
+ username = draftRecord.username;
168
+ }
169
+ let password;
170
+ if (draftRecord.password !== undefined) {
171
+ if (typeof draftRecord.password !== 'string') {
172
+ throw new BadRequest('payload.proxy.password must be a string when present');
173
+ }
174
+ password = draftRecord.password;
175
+ }
176
+ proxy = {
177
+ url: draftRecord.url,
178
+ ...username === undefined ? {} : { username },
179
+ ...password === undefined ? {} : { password },
180
+ };
181
+ }
182
+ return {
183
+ ...url === undefined ? {} : { url },
184
+ ...proxy === undefined ? {} : { proxy },
185
+ };
186
+ }
187
+ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
106
188
  switch (endpoint) {
107
189
  case 'status': {
108
190
  const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
@@ -132,6 +214,18 @@ async function dispatch(controller, speed, endpoint, payload, signal) {
132
214
  case 'setSpeed':
133
215
  await speed.setSpeed(readSessionId(payload), readSpeedTier(payload));
134
216
  return ok({ ok: true });
217
+ case 'proxyGet':
218
+ if (proxy === undefined)
219
+ throw new BadRequest('proxy configuration is unavailable');
220
+ return ok(await proxy.get());
221
+ case 'proxySet':
222
+ if (proxy === undefined)
223
+ throw new BadRequest('proxy configuration is unavailable');
224
+ return ok(await proxy.set(readProxyInput(payload)));
225
+ case 'proxyTest':
226
+ if (proxy === undefined)
227
+ throw new BadRequest('proxy configuration is unavailable');
228
+ return ok(await proxy.test(readProxyTestPayload(payload)));
135
229
  default:
136
230
  throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
137
231
  }
@@ -141,8 +235,9 @@ async function dispatch(controller, speed, endpoint, payload, signal) {
141
235
  * @param ctx - the plugin context (headless profiles have no `connection`).
142
236
  * @param controller - the auth operations backing the endpoints.
143
237
  * @param speed - the per-session speed-tier state backing the Speed toggle.
238
+ * @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
144
239
  */
145
- export function registerAuthRpc(ctx, controller, speed) {
240
+ export function registerAuthRpc(ctx, controller, speed, proxy = undefined) {
146
241
  // `connection` is not in this plugin's inject list (headless compositions
147
242
  // lack it), so its startup order is unconstrained: defer registration until
148
243
  // the service exists instead of probing once at apply time.
@@ -150,7 +245,7 @@ export function registerAuthRpc(ctx, controller, speed) {
150
245
  const connection = ctx.get('connection');
151
246
  ctx.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
152
247
  try {
153
- return await dispatch(controller, speed, endpoint, payload, signal);
248
+ return await dispatch(controller, speed, proxy, endpoint, payload, signal);
154
249
  }
155
250
  catch (error) {
156
251
  return failure(error);
@@ -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. */
@@ -78,6 +78,10 @@ const styles = {
78
78
  border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12,
79
79
  padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
80
80
  },
81
+ proxyCard: {
82
+ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
83
+ },
84
+ separator: { borderTop: '1px solid var(--dsw-alias-border-l2)' },
81
85
  cardHeader: { display: 'flex', alignItems: 'center', gap: 8 },
82
86
  dot: { width: 8, height: 8, borderRadius: '50%', flexShrink: 0 },
83
87
  name: { fontWeight: 500, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
@@ -132,6 +136,36 @@ const styles = {
132
136
  fontFamily: 'monospace', fontSize: 18, lineHeight: '24px', letterSpacing: 2,
133
137
  color: 'var(--dsw-alias-label-primary)', userSelect: 'all',
134
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)' },
135
169
  };
136
170
  /** Status dot color for one provider state. */
137
171
  function dotColor(status) {
@@ -188,6 +222,24 @@ function usageBarColor(usedPercent) {
188
222
  return 'var(--dsw-alias-state-warn-label)';
189
223
  return 'var(--dsw-alias-state-success-primary)';
190
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
+ }
191
243
  /**
192
244
  * The Subscriptions settings page component.
193
245
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -211,6 +263,21 @@ export function SubscriptionsSection(props) {
211
263
  const pollersRef = useRef(new Map());
212
264
  /** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
213
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);
214
281
  const setProviderError = useCallback((provider, message) => {
215
282
  if (!mountedRef.current)
216
283
  return;
@@ -427,10 +494,102 @@ export function SubscriptionsSection(props) {
427
494
  }, 1500);
428
495
  }).catch(() => undefined);
429
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]);
430
584
  if (rpc === undefined) {
431
585
  return _jsx("p", { style: styles.intro, children: t('unavailable') });
432
586
  }
433
- return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), PROVIDERS.map(({ id, name }) => {
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 }) => {
434
593
  const status = statuses[id];
435
594
  const busy = status?.busy === true;
436
595
  const deviceCode = deviceCodes[id];
@@ -444,5 +603,14 @@ export function SubscriptionsSection(props) {
444
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
445
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));
446
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));
447
- })] }));
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') })] })] }) }))] }));
448
616
  }
@@ -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');
@@ -51,6 +51,38 @@ export declare const en: {
51
51
  speedFastDescription: string;
52
52
  commandFast: string;
53
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;
54
86
  };
55
87
  /** zh strings, one per {@link en} key. */
56
88
  export declare const zh: {
@@ -104,6 +136,38 @@ export declare const zh: {
104
136
  speedFastDescription: string;
105
137
  commandFast: string;
106
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;
107
171
  };
108
172
  /** The Subscriptions namespace key union (en is the key-set source of truth). */
109
173
  export type SubscriptionsKey = keyof typeof en;
@@ -51,6 +51,38 @@ export const en = {
51
51
  speedFastDescription: '1.5x speed, more usage',
52
52
  commandFast: 'Switch the Codex speed tier (Standard/Fast)',
53
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.',
54
86
  };
55
87
  /** zh strings, one per {@link en} key. */
56
88
  export const zh = {
@@ -104,4 +136,36 @@ export const zh = {
104
136
  speedFastDescription: '约 1.5 倍速度,消耗更多用量',
105
137
  commandFast: '切换 Codex 速度档(标准/快速)',
106
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 授权页在浏览器中打开,走的是浏览器/系统代理,不受此设置影响。',
107
171
  };