dsh-agent-proxy 0.1.0 → 0.2.0

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.
Files changed (4) hide show
  1. package/README.md +5 -4
  2. package/client.js +105 -23
  3. package/index.js +61 -8
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -31,11 +31,12 @@ http_proxy https_proxy all_proxy(及大写形式)、NODE_USE_ENV_PROXY=1
31
31
 
32
32
  按优先级:
33
33
 
34
- 1. 插件配置里的 `proxyUrl`;
35
- 2. Windows 系统代理(HKCU Internet Settings,即 Clash / V2Ray 打开的「系统代理」);
36
- 3. DSH 启动时已导出的 `HTTPS_PROXY` / `HTTP_PROXY`。
34
+ 1. 设置页「代理地址」里填写的地址(留空表示自动);
35
+ 2. 插件配置里的 `proxyUrl`;
36
+ 3. Windows 系统代理(HKCU Internet Settings,即 Clash / V2Ray 打开的「系统代理」);
37
+ 4. DSH 启动时已导出的 `HTTPS_PROXY` / `HTTP_PROXY`。
37
38
 
38
- 设置页开关下方会显示当前生效的地址与来源。
39
+ 设置页开关下方有一个「代理地址」输入框:填了就用它(仅支持 `http://` / `https://`),留空自动使用系统代理。地址在失焦或回车时保存,开关开着会立即用新地址重连;开关下方会显示当前生效的地址与来源。
39
40
 
40
41
  ## 安装
41
42
 
package/client.js CHANGED
@@ -11,30 +11,38 @@ window.__ModuleLoader__.load({
11
11
  title: '代理',
12
12
  rowTitle: '代理 dsh',
13
13
  rowHint: 'dsh 使用 git 等工具超时时可开启代理',
14
+ addressLabel: '代理地址',
15
+ addressPlaceholder: 'http://127.0.0.1:7890',
16
+ addressHint: '仅支持 http 代理;留空则自动使用系统代理',
14
17
  loading: '读取中…',
15
18
  viaSystem: 'Windows 系统代理',
16
- viaConfig: '配置中的代理',
19
+ viaConfig: '自定义地址',
17
20
  viaEnv: '环境变量中的代理',
18
21
  current: '当前代理',
19
- noProxy: '未检测到系统代理,请先打开代理软件的系统代理,或在插件配置里填写 proxyUrl',
22
+ noProxy: '未检测到系统代理,请填写代理地址,或先打开代理软件的系统代理',
20
23
  loadFailed: '读取代理状态失败',
21
24
  toggleFailed: '切换失败',
22
- saveFailed: '已生效,但没能写入配置,重启后会恢复为关闭',
25
+ saveFailed: '保存失败',
26
+ addressBad: '地址无效:仅支持 http:// 或 https://,例如 http://127.0.0.1:7890',
23
27
  };
24
28
 
25
29
  const EN = {
26
30
  title: 'Proxy',
27
31
  rowTitle: 'Proxy for dsh',
28
32
  rowHint: 'Turn this on when git and other tools keep timing out',
33
+ addressLabel: 'Proxy address',
34
+ addressPlaceholder: 'http://127.0.0.1:7890',
35
+ addressHint: 'HTTP proxies only; leave empty to use the system proxy',
29
36
  loading: 'Loading…',
30
37
  viaSystem: 'Windows system proxy',
31
- viaConfig: 'proxy from config',
38
+ viaConfig: 'custom address',
32
39
  viaEnv: 'proxy from environment',
33
40
  current: 'Current proxy',
34
- noProxy: 'No system proxy detected. Turn on your proxy client system proxy, or set proxyUrl in the plugin config.',
41
+ noProxy: 'No system proxy detected. Enter an address, or turn on your proxy client system proxy.',
35
42
  loadFailed: 'Failed to read the proxy state',
36
43
  toggleFailed: 'Failed to switch',
37
- saveFailed: 'Applied, but the setting could not be saved; it reverts to off after a restart',
44
+ saveFailed: 'Failed to save',
45
+ addressBad: 'Invalid address: http:// or https:// only, e.g. http://127.0.0.1:7890',
38
46
  };
39
47
 
40
48
  const styles = {
@@ -45,7 +53,7 @@ window.__ModuleLoader__.load({
45
53
  padding: '16px',
46
54
  display: 'flex',
47
55
  flexDirection: 'column',
48
- gap: '8px',
56
+ gap: '12px',
49
57
  boxSizing: 'border-box',
50
58
  },
51
59
  row: {
@@ -87,6 +95,28 @@ window.__ModuleLoader__.load({
87
95
  lineHeight: 1.5,
88
96
  color: 'var(--dsw-alias-label-danger, #cf222e)',
89
97
  },
98
+ field: {
99
+ display: 'flex',
100
+ flexDirection: 'column',
101
+ gap: '6px',
102
+ },
103
+ fieldLabel: {
104
+ fontSize: '13px',
105
+ lineHeight: 1.4,
106
+ color: 'var(--dsw-alias-label-secondary, #57606a)',
107
+ },
108
+ input: {
109
+ boxSizing: 'border-box',
110
+ width: '100%',
111
+ border: '1px solid var(--dsw-alias-border-l2, #d0d7de)',
112
+ background: 'var(--dsw-alias-bg-layer-2, #ffffff)',
113
+ color: 'var(--dsw-alias-label-primary, #1f2329)',
114
+ borderRadius: '8px',
115
+ padding: '7px 10px',
116
+ fontSize: '13px',
117
+ lineHeight: 1.4,
118
+ outline: 'none',
119
+ },
90
120
  };
91
121
 
92
122
  function Switch({ checked, disabled, label, onChange }) {
@@ -136,6 +166,7 @@ window.__ModuleLoader__.load({
136
166
  function AgentProxySection({ t }) {
137
167
  const [state, setState] = React.useState(null);
138
168
  const [busy, setBusy] = React.useState(false);
169
+ const [draft, setDraft] = React.useState('');
139
170
  const [message, setMessage] = React.useState(null);
140
171
 
141
172
  const load = React.useCallback(async () => {
@@ -147,6 +178,7 @@ window.__ModuleLoader__.load({
147
178
  const body = await response.json();
148
179
  if (!body || body.ok !== true || !body.data) throw new Error(body && body.error ? body.error : 'bad response');
149
180
  setState(body.data);
181
+ setDraft(body.data.configuredUrl || '');
150
182
  setMessage(null);
151
183
  } catch (error) {
152
184
  setMessage({ kind: 'bad', text: t('loadFailed') });
@@ -157,33 +189,55 @@ window.__ModuleLoader__.load({
157
189
  load();
158
190
  }, [load]);
159
191
 
192
+ const send = async (path, payload) => {
193
+ const response = await fetch(`${API}${path}`, {
194
+ method: 'POST',
195
+ credentials: 'same-origin',
196
+ headers: { 'content-type': 'application/json', 'x-dsh-agent-proxy': '1' },
197
+ body: JSON.stringify(payload),
198
+ });
199
+ const body = await response.json();
200
+ if (!body || body.ok !== true || !body.data) {
201
+ throw new Error(body && body.error ? body.error : t('saveFailed'));
202
+ }
203
+ return body.data;
204
+ };
205
+
160
206
  const toggle = async () => {
161
207
  if (busy) return;
162
208
  setBusy(true);
163
209
  setMessage(null);
164
210
  try {
165
211
  const next = !(state && state.enabled);
166
- const response = await fetch(`${API}/toggle`, {
167
- method: 'POST',
168
- credentials: 'same-origin',
169
- headers: { 'content-type': 'application/json', 'x-dsh-agent-proxy': '1' },
170
- body: JSON.stringify({ enabled: next }),
171
- });
172
- const body = await response.json();
173
- if (!body || body.ok !== true || !body.data) {
174
- throw new Error(body && body.error ? body.error : t('toggleFailed'));
175
- }
176
- setState(body.data);
177
- if (body.data.saved === false) setMessage({ kind: 'warn', text: t('saveFailed') });
178
- if (body.data.enabled && body.data.error) setMessage({ kind: 'bad', text: body.data.error });
212
+ const data = await send('/toggle', { enabled: next });
213
+ setState(data);
214
+ if (data.saved === false) setMessage({ kind: 'warn', text: t('saveFailed') });
179
215
  } catch (error) {
180
- setMessage({ kind: 'bad', text: String((error && error.message) || error) });
216
+ const text = String((error && error.message) || error);
217
+ setMessage({ kind: 'bad', text: text === 'no-proxy' ? t('noProxy') : text });
181
218
  load();
182
219
  } finally {
183
220
  setBusy(false);
184
221
  }
185
222
  };
186
223
 
224
+ const saveAddress = async () => {
225
+ if (busy) return;
226
+ if (draft === (state && state.configuredUrl ? state.configuredUrl : '')) return;
227
+ setBusy(true);
228
+ setMessage(null);
229
+ try {
230
+ const data = await send('/settings', { proxyUrl: draft.trim() });
231
+ setState(data);
232
+ setDraft(data.configuredUrl || '');
233
+ } catch (error) {
234
+ const text = String((error && error.message) || error);
235
+ setMessage({ kind: 'bad', text: text === 'no-proxy' ? t('noProxy') : text });
236
+ } finally {
237
+ setBusy(false);
238
+ }
239
+ };
240
+
187
241
  const enabled = Boolean(state && state.enabled);
188
242
  const proxyUrl = state && (state.proxyUrl || state.detectedUrl);
189
243
  const sourceLabel = (() => {
@@ -195,7 +249,9 @@ window.__ModuleLoader__.load({
195
249
  })();
196
250
 
197
251
  const lines = [];
198
- if (message) lines.push(h('div', { key: 'msg', style: message.kind === 'warn' ? styles.warn : styles.bad }, message.text));
252
+ if (message) {
253
+ lines.push(h('div', { key: 'msg', style: message.kind === 'warn' ? styles.warn : styles.bad }, message.text));
254
+ }
199
255
  if (state === null && !message) lines.push(h('div', { key: 'loading', style: styles.hint }, t('loading')));
200
256
  if (state && enabled && proxyUrl) {
201
257
  lines.push(
@@ -206,7 +262,9 @@ window.__ModuleLoader__.load({
206
262
  ),
207
263
  );
208
264
  }
209
- if (state && !enabled && !proxyUrl) lines.push(h('div', { key: 'noproxy', style: styles.warn }, t('noProxy')));
265
+ if (state && !enabled && !proxyUrl && !draft) {
266
+ lines.push(h('div', { key: 'noproxy', style: styles.warn }, t('noProxy')));
267
+ }
210
268
 
211
269
  return h(
212
270
  'div',
@@ -227,6 +285,30 @@ window.__ModuleLoader__.load({
227
285
  onChange: toggle,
228
286
  }),
229
287
  ),
288
+ h(
289
+ 'div',
290
+ { style: styles.field },
291
+ h('label', { style: styles.fieldLabel, htmlFor: 'dsh-agent-proxy-url' }, t('addressLabel')),
292
+ h('input', {
293
+ id: 'dsh-agent-proxy-url',
294
+ type: 'text',
295
+ spellCheck: false,
296
+ autoComplete: 'off',
297
+ value: draft,
298
+ placeholder: t('addressPlaceholder'),
299
+ disabled: busy || state === null,
300
+ onChange: (event) => setDraft(event.target.value),
301
+ onBlur: () => {
302
+ saveAddress();
303
+ },
304
+ onKeyDown: (event) => {
305
+ if (event.key === 'Enter') event.currentTarget.blur();
306
+ },
307
+ style: styles.input,
308
+ 'aria-label': t('addressLabel'),
309
+ }),
310
+ h('div', { style: styles.hint }, t('addressHint')),
311
+ ),
230
312
  lines,
231
313
  );
232
314
  }
package/index.js CHANGED
@@ -21,9 +21,10 @@
21
21
  * - `PIP_PROXY` / `PIP_HTTPS_PROXY`
22
22
  *
23
23
  * Proxy address resolution, highest first:
24
- * 1. the row's `config.proxyUrl`
25
- * 2. the Windows system proxy (HKCU Internet Settings, when enabled)
26
- * 3. a proxy already exported in the Harness environment
24
+ * 1. the address saved in the settings page (`$DSH_HOME/agent-proxy.json`)
25
+ * 2. the row's `config.proxyUrl`
26
+ * 3. the Windows system proxy (HKCU Internet Settings, when enabled)
27
+ * 4. a proxy already exported in the Harness environment
27
28
  *
28
29
  * `config.enabled` is only the initial default before the state file exists.
29
30
  */
@@ -524,7 +525,10 @@ export function apply(ctx, rawConfig) {
524
525
  delete globalThis[APPLIED]
525
526
  }
526
527
 
527
- const detect = () => resolveProxy(config.proxyUrl)
528
+ /** Address the user typed in the settings page, or the row's configured default. */
529
+ const configuredUrl = () => trimOrEmpty(readState()?.customProxyUrl) || trimOrEmpty(config.proxyUrl)
530
+
531
+ const detect = () => resolveProxy(configuredUrl())
528
532
 
529
533
  const enable = () => {
530
534
  if (status.enabled) return status
@@ -537,7 +541,7 @@ export function apply(ctx, rawConfig) {
537
541
  const restore = existing?.restore ?? captureToolConfig()
538
542
  applyToolConfig(resolved.url)
539
543
  writeProxyEnvironment(resolved.url)
540
- writeState({ enabled: true, proxyUrl: resolved.url, restore })
544
+ writeState({ ...(existing ?? {}), enabled: true, restore })
541
545
  globalThis[APPLIED] = true
542
546
  status.enabled = true
543
547
  status.proxyUrl = resolved.url
@@ -546,7 +550,7 @@ export function apply(ctx, rawConfig) {
546
550
  return status
547
551
  }
548
552
 
549
- const disable = () => {
553
+ const unapply = () => {
550
554
  const restore = readState()?.restore
551
555
  if (restore) {
552
556
  try {
@@ -557,7 +561,11 @@ export function apply(ctx, rawConfig) {
557
561
  }
558
562
  restoreOriginalEnvironment()
559
563
  delete globalThis[APPLIED]
560
- writeState({ enabled: false })
564
+ }
565
+
566
+ const disable = () => {
567
+ unapply()
568
+ writeState({ ...(readState() ?? {}), enabled: false })
561
569
  status.enabled = false
562
570
  status.proxyUrl = ''
563
571
  status.source = ''
@@ -572,12 +580,38 @@ export function apply(ctx, rawConfig) {
572
580
  enabled: status.enabled,
573
581
  proxyUrl: status.enabled ? status.proxyUrl : detected.url,
574
582
  source: status.enabled ? status.source : detected.source,
583
+ configuredUrl: trimOrEmpty(readState()?.customProxyUrl),
575
584
  detectedUrl: detected.url,
576
585
  detectedSource: detected.source,
577
586
  error: status.error,
578
587
  }
579
588
  }
580
589
 
590
+ /** Store a custom proxy address ('' = keep using the system proxy) and re-apply it. */
591
+ const saveProxyUrl = (raw) => {
592
+ const value = trimOrEmpty(raw)
593
+ const normalized = value ? normalizeProxyUrl(value) : ''
594
+ if (value && !normalized) {
595
+ throw Object.assign(
596
+ new Error('代理地址无效:仅支持 http:// 或 https:// 地址,例如 http://127.0.0.1:7890'),
597
+ { statusCode: 400 },
598
+ )
599
+ }
600
+ const wasEnabled = status.enabled
601
+ if (wasEnabled) {
602
+ unapply()
603
+ status.enabled = false
604
+ status.proxyUrl = ''
605
+ status.source = ''
606
+ }
607
+ const next = { ...(readState() ?? {}), customProxyUrl: normalized }
608
+ // 0.1.x stored the resolved address here; it is not a user preference.
609
+ delete next.proxyUrl
610
+ writeState(next)
611
+ if (wasEnabled) enable()
612
+ return snapshot()
613
+ }
614
+
581
615
  const state = readState()
582
616
  const wantOn = typeof state?.enabled === 'boolean' ? state.enabled : config.enabled === true
583
617
  if (wantOn) enable()
@@ -619,7 +653,7 @@ export function apply(ctx, rawConfig) {
619
653
  if (body.enabled) {
620
654
  enable()
621
655
  if (!status.enabled) {
622
- return fail(409, '未检测到代理地址:请先打开代理软件的系统代理,或在插件配置里填写 proxyUrl')
656
+ return fail(409, '未检测到代理地址:请在下方填写代理地址,或先打开代理软件的系统代理')
623
657
  }
624
658
  } else {
625
659
  disable()
@@ -638,6 +672,25 @@ export function apply(ctx, rawConfig) {
638
672
  }
639
673
  },
640
674
  },
675
+ {
676
+ path: `${API_PREFIX}/settings`,
677
+ methods: ['POST'],
678
+ requestBody: 'buffered',
679
+ async fetch(request) {
680
+ const rejected = guardMutation(request)
681
+ if (rejected) return fail(403, rejected)
682
+ try {
683
+ const body = await readJsonBody(request)
684
+ if (typeof body.proxyUrl !== 'string') {
685
+ return fail(400, 'proxyUrl 必须是字符串')
686
+ }
687
+ const data = saveProxyUrl(body.proxyUrl)
688
+ return jsonResponse(200, { ok: true, data: { ...data, saved: true } })
689
+ } catch (error) {
690
+ return fail(error?.statusCode ?? 500, messageOf(error))
691
+ }
692
+ },
693
+ },
641
694
  ]
642
695
 
643
696
  const mount = (owner) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agent-proxy",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A settings-page switch that turns a local HTTP proxy on for the git, npm and pip commands DSH runs.",
5
5
  "type": "module",
6
6
  "license": "MIT",