@yanglaofish/dsh-proxy-pro 1.0.9 → 1.1.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.
- package/docs/LESSONS.md +994 -817
- package/lib/client.js +117 -34
- package/lib/index.js +83 -25
- package/lib/proxy-core.js +64 -4
- package/package.json +5 -5
package/lib/client.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* NOT derived from settings), the editable config fields (mode / custom
|
|
11
11
|
* URL / NO_PROXY), and a per-URL route diagnostic.
|
|
12
12
|
*
|
|
13
|
-
* Both read and write
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
13
|
+
* Both read and write configuration through the host's own
|
|
14
|
+
* `/dsh-proxy-pro/api/config` endpoint (the browser settings transport
|
|
15
|
+
* `ctx.settingsScope` was removed in dsh 0.2 — LESSONS §33); runtime facts
|
|
16
|
+
* come from the same-origin `/dsh-proxy-pro/api` endpoints served by the host
|
|
16
17
|
* half (browser-trust fenced, see lib/index.js).
|
|
17
18
|
*
|
|
18
19
|
* This hand-written module mirrors the loader format the in-repo client
|
|
@@ -107,18 +108,85 @@ window.__ModuleLoader__.load({
|
|
|
107
108
|
.catch(function () { return null; });
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
// ----
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// ---- host-backed configuration store (dsh >= 0.2) ------------------
|
|
112
|
+
// The browser settings transport (`ctx.settingsScope`) was removed in dsh
|
|
113
|
+
// 0.2 (LESSONS §33), so the capsule and the settings page read/write the
|
|
114
|
+
// host's own /config API. The snapshot shape mirrors the old settings
|
|
115
|
+
// scope (`{ status, value, writable }`), so the render code below is
|
|
116
|
+
// unchanged; an older host simply has no /config route and the panel
|
|
117
|
+
// reports itself unavailable instead of crashing.
|
|
118
|
+
function createProxyStore() {
|
|
119
|
+
var listeners = [];
|
|
120
|
+
var snapshot = { status: "loading", value: undefined, writable: false };
|
|
121
|
+
var inflight = null;
|
|
122
|
+
function emit(next) {
|
|
123
|
+
snapshot = next;
|
|
124
|
+
listeners.slice().forEach(function (fn) {
|
|
125
|
+
try { fn(); } catch (error) { console.error("[dsh-proxy-pro] listener failed:", error); }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function accept(data) {
|
|
129
|
+
if (data === null || data.ok !== true || data.config === undefined || data.config === null) {
|
|
130
|
+
emit({ status: "unavailable", value: undefined, writable: false });
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
emit({ status: "ready", value: data.config, writable: data.writable !== false });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
function refresh() {
|
|
137
|
+
if (inflight !== null) return inflight;
|
|
138
|
+
inflight = fetchJson("/config").then(function (data) {
|
|
139
|
+
inflight = null;
|
|
140
|
+
accept(data);
|
|
141
|
+
return data;
|
|
142
|
+
}, function () { inflight = null; return null; });
|
|
143
|
+
return inflight;
|
|
144
|
+
}
|
|
145
|
+
function set(field, value) {
|
|
146
|
+
var body = {};
|
|
147
|
+
body[field] = value;
|
|
148
|
+
return fetchJson("/config", {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: { "content-type": "application/json" },
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
}).then(function (data) {
|
|
153
|
+
if (data === null || data.ok !== true) {
|
|
154
|
+
throw new Error((data && data.error) || "config write failed");
|
|
155
|
+
}
|
|
156
|
+
accept(data);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
subscribe: function (fn) {
|
|
161
|
+
listeners.push(fn);
|
|
162
|
+
return function () {
|
|
163
|
+
var index = listeners.indexOf(fn);
|
|
164
|
+
if (index >= 0) listeners.splice(index, 1);
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
getSnapshot: function () { return snapshot; },
|
|
168
|
+
refresh: refresh,
|
|
169
|
+
set: set,
|
|
170
|
+
setEnabled: function (next) { return set("enabled", next === true); },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Subscribe to the host-backed config store → `{ value, writable, ready }`. */
|
|
175
|
+
function useProxySettings(store) {
|
|
113
176
|
var snapshot = useSyncExternalStore(
|
|
114
|
-
function (onChange) {
|
|
115
|
-
|
|
177
|
+
function (onChange) {
|
|
178
|
+
var unsubscribe = store.subscribe(onChange);
|
|
179
|
+
store.refresh();
|
|
180
|
+
return unsubscribe;
|
|
181
|
+
},
|
|
182
|
+
function () { return store.getSnapshot(); }
|
|
116
183
|
);
|
|
117
184
|
var ready = snapshot.status === "ready" && snapshot.value !== undefined;
|
|
118
185
|
return {
|
|
119
186
|
value: ready ? snapshot.value : undefined,
|
|
120
187
|
writable: snapshot.writable === true && ready,
|
|
121
188
|
ready: snapshot.status === "unavailable" ? false : snapshot.status === "ready",
|
|
189
|
+
unavailable: snapshot.status === "unavailable",
|
|
122
190
|
};
|
|
123
191
|
}
|
|
124
192
|
|
|
@@ -177,26 +245,35 @@ window.__ModuleLoader__.load({
|
|
|
177
245
|
|
|
178
246
|
// ---- conversation header capsule button ----
|
|
179
247
|
function ProxyHeaderButton(props) {
|
|
180
|
-
var
|
|
181
|
-
var settings =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
var store = props.store;
|
|
249
|
+
var settings = useProxySettings(store);
|
|
250
|
+
// Local optimistic value (2026-09-18): the capsule flips its visual
|
|
251
|
+
// immediately on click instead of waiting for scope.set's round-trip
|
|
252
|
+
// (the settings-page switch already behaves this way). null means no
|
|
253
|
+
// unconfirmed value — the capsule mirrors settings.value.
|
|
254
|
+
var optimistic = useState(null);
|
|
255
|
+
var setOptimistic = optimistic[1];
|
|
188
256
|
var config = settings.value || {};
|
|
189
|
-
var
|
|
190
|
-
var
|
|
257
|
+
var valueEnabled = config.enabled === true;
|
|
258
|
+
var shownEnabled = optimistic[0] !== null ? optimistic[0] : valueEnabled;
|
|
259
|
+
var active = shownEnabled === true && (config.mode || "system") !== "none";
|
|
191
260
|
var mode = config.mode || "system";
|
|
192
261
|
var modeLabel = mode === "system" ? "system" : mode === "custom" ? "custom" : "none";
|
|
262
|
+
// Once the host has committed the new value, drop the optimistic echo.
|
|
263
|
+
useEffect(function () {
|
|
264
|
+
if (optimistic[0] !== null && settings.ready && settings.value !== undefined && settings.value.enabled === optimistic[0]) {
|
|
265
|
+
setOptimistic(null);
|
|
266
|
+
}
|
|
267
|
+
}, [optimistic[0], settings.value, settings.ready]);
|
|
193
268
|
function toggle() {
|
|
194
269
|
if (!settings.writable) return;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
270
|
+
var next = !shownEnabled;
|
|
271
|
+
setOptimistic(next);
|
|
272
|
+
store.setEnabled(next).catch(function (error) {
|
|
198
273
|
console.error("[dsh-proxy-pro] toggle failed:", error);
|
|
199
|
-
|
|
274
|
+
// Roll back to the committed value on failure.
|
|
275
|
+
setOptimistic(null);
|
|
276
|
+
});
|
|
200
277
|
// Toggle doubles as a manual "recheck now" even when the settings
|
|
201
278
|
// panel is not mounted: pulling /status makes the host re-read the
|
|
202
279
|
// system facts (idempotent — snapshot refresh unless the proxy really
|
|
@@ -216,7 +293,7 @@ window.__ModuleLoader__.load({
|
|
|
216
293
|
"data-active": active ? "true" : "false",
|
|
217
294
|
"data-proxy-toggle": true,
|
|
218
295
|
onClick: toggle,
|
|
219
|
-
disabled: !settings.writable
|
|
296
|
+
disabled: !settings.writable,
|
|
220
297
|
title: title,
|
|
221
298
|
"aria-pressed": active,
|
|
222
299
|
},
|
|
@@ -227,8 +304,8 @@ window.__ModuleLoader__.load({
|
|
|
227
304
|
|
|
228
305
|
// ---- Settings page: 代理管理 ----
|
|
229
306
|
function ProxyManagerPanel(props) {
|
|
230
|
-
var
|
|
231
|
-
var settings =
|
|
307
|
+
var store = props.store;
|
|
308
|
+
var settings = useProxySettings(store);
|
|
232
309
|
// Re-fetch the host snapshot when the configuration changes (enabled /
|
|
233
310
|
// mode / customUrl / noProxy are the only inputs that move the effKey);
|
|
234
311
|
// no timed polling, mount + change-driven only (§17.3d).
|
|
@@ -251,7 +328,10 @@ window.__ModuleLoader__.load({
|
|
|
251
328
|
|
|
252
329
|
if (!settings.ready) {
|
|
253
330
|
return react.createElement("div", { className: "dsxprx", "data-proxy-settings": true },
|
|
254
|
-
react.createElement("p", { className: "dsxprx-hint" },
|
|
331
|
+
react.createElement("p", { className: "dsxprx-hint" },
|
|
332
|
+
settings.unavailable
|
|
333
|
+
? "宿主未提供配置接口(/dsh-proxy-pro/api/config)——请确认插件已完整安装、宿主已重启。"
|
|
334
|
+
: "代理设置加载中…"));
|
|
255
335
|
}
|
|
256
336
|
var value = settings.value || { enabled: false, mode: "system", customUrl: "", noProxy: "" };
|
|
257
337
|
var writable = settings.writable;
|
|
@@ -297,7 +377,7 @@ window.__ModuleLoader__.load({
|
|
|
297
377
|
|
|
298
378
|
function setField(field) {
|
|
299
379
|
return function (next) {
|
|
300
|
-
|
|
380
|
+
store.set(field, next).catch(function (error) {
|
|
301
381
|
console.error("[dsh-proxy-pro] save failed:", error);
|
|
302
382
|
});
|
|
303
383
|
};
|
|
@@ -422,6 +502,7 @@ window.__ModuleLoader__.load({
|
|
|
422
502
|
? react.createElement("span", null, "诊断失败:" + diag.error)
|
|
423
503
|
: [
|
|
424
504
|
react.createElement("span", { key: "r" }, "策略判定: " + diag.route + (diag.bypassed ? "(NO_PROXY 命中)" : "")),
|
|
505
|
+
react.createElement("span", { key: "codes" }, "状态码: 走代理 " + ((diag.proxy && diag.proxy.status) || "—") + " · 直连 " + ((diag.direct && diag.direct.status) || "—")),
|
|
425
506
|
react.createElement("span", { key: "px", className: "dsxprx-diagprobe" },
|
|
426
507
|
react.createElement("span", { className: "dsxprx-diotag" }, "走代理"),
|
|
427
508
|
react.createElement("span", { className: "dsxprx-verdict", "data-tone": channelBadge("proxyBadge").tone }, channelBadge("proxyBadge").text)
|
|
@@ -447,12 +528,13 @@ window.__ModuleLoader__.load({
|
|
|
447
528
|
}
|
|
448
529
|
|
|
449
530
|
/**
|
|
450
|
-
* Browser plugin body:
|
|
451
|
-
* conversation header button and the 代理配置 settings section.
|
|
531
|
+
* Browser plugin body: create the host-backed config store, then register
|
|
532
|
+
* the conversation header button and the 代理配置 settings section.
|
|
452
533
|
* @param ctx - client cordis context.
|
|
453
534
|
*/
|
|
454
535
|
function apply(ctx) {
|
|
455
|
-
var
|
|
536
|
+
var store = createProxyStore();
|
|
537
|
+
ctx.effect(function () { return function () { /* no timers to clear */ }; }, "dsh-proxy-pro: config store");
|
|
456
538
|
ctx.slots.inject("settings.section", function () {
|
|
457
539
|
return ctx.slots.register({
|
|
458
540
|
name: "settings.section",
|
|
@@ -460,7 +542,7 @@ window.__ModuleLoader__.load({
|
|
|
460
542
|
order: 20,
|
|
461
543
|
label: "代理配置",
|
|
462
544
|
}, function Section() {
|
|
463
|
-
return react.createElement(ProxyManagerPanel, {
|
|
545
|
+
return react.createElement(ProxyManagerPanel, { store: store });
|
|
464
546
|
});
|
|
465
547
|
});
|
|
466
548
|
ctx.slots.inject("conversation.session.header.utilities", function () {
|
|
@@ -470,7 +552,7 @@ window.__ModuleLoader__.load({
|
|
|
470
552
|
order: 10,
|
|
471
553
|
label: "代理",
|
|
472
554
|
}, function Header() {
|
|
473
|
-
return react.createElement(ProxyHeaderButton, {
|
|
555
|
+
return react.createElement(ProxyHeaderButton, { store: store });
|
|
474
556
|
});
|
|
475
557
|
});
|
|
476
558
|
}
|
|
@@ -480,10 +562,11 @@ window.__ModuleLoader__.load({
|
|
|
480
562
|
// Cordis service names the browser loader must wait for — NOT package
|
|
481
563
|
// names (those belong in package.json's dsh.client.inject). Writing
|
|
482
564
|
// package names here keeps the plugin permanently pending and blocks
|
|
483
|
-
// web boot (see docs/LESSONS.md §6).
|
|
565
|
+
// web boot (see docs/LESSONS.md §6). `settingsScope` was one of these
|
|
566
|
+
// until dsh 0.2 removed that service; requiring it now would keep the
|
|
567
|
+
// whole client half permanently pending (LESSONS §33).
|
|
484
568
|
exports.inject = [
|
|
485
569
|
"slots",
|
|
486
|
-
"settingsScope",
|
|
487
570
|
];
|
|
488
571
|
return module.exports;
|
|
489
572
|
},
|
package/lib/index.js
CHANGED
|
@@ -33,10 +33,14 @@ import {
|
|
|
33
33
|
classifyProbeStatus,
|
|
34
34
|
classifyTargetFailure,
|
|
35
35
|
combineDualProbe,
|
|
36
|
+
configPayload,
|
|
37
|
+
DEFAULT_ENTRY_ID,
|
|
38
|
+
discoverEntryId,
|
|
36
39
|
headNeedsGetRetry,
|
|
37
40
|
keepaliveHint,
|
|
38
41
|
makeSystemProxyReader,
|
|
39
42
|
normalizeProxyUrl,
|
|
43
|
+
readEntryConfig,
|
|
40
44
|
resolveProxyState,
|
|
41
45
|
summarize,
|
|
42
46
|
systemFactsEqual,
|
|
@@ -114,8 +118,7 @@ function makeEnvLike(effective) {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
/** Build the agent-facing status text from the latest sync snapshot. */
|
|
118
|
-
function renderStatusText(snapshot) {
|
|
121
|
+
/** Build the agent-facing status text from the latest sync snapshot. */function renderStatusText(snapshot) {
|
|
119
122
|
const effective = snapshot?.effective
|
|
120
123
|
const config = snapshot?.config
|
|
121
124
|
if (effective === undefined || config === undefined) {
|
|
@@ -478,23 +481,39 @@ function apply(ctx, config) {
|
|
|
478
481
|
return syncing
|
|
479
482
|
}
|
|
480
483
|
|
|
481
|
-
// Configuration source
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
484
|
+
// Configuration source — two settings generations (LESSONS §26, §33):
|
|
485
|
+
// - dsh <= 0.1.x: a per-plugin namespace registered through the settings
|
|
486
|
+
// service (register(ns, schema, {base}) → scope.get()/watch()).
|
|
487
|
+
// - dsh >= 0.2: register()/watch() are gone. The plugin's OWN composition
|
|
488
|
+
// entry IS the settings form (namespace = entry id, schema = this Config),
|
|
489
|
+
// writes go through settings.update(entryId, patch) and every commit
|
|
490
|
+
// emits `settings/document-updated`, which is what re-syncs us live.
|
|
488
491
|
const settingsService = ctx.get('settings')
|
|
489
|
-
|
|
492
|
+
const configEditor = ctx.get('configEditor')
|
|
493
|
+
const legacyRegister = typeof settingsService?.register === 'function'
|
|
494
|
+
const entryId = discoverEntryId(configEditor, ctx) ?? DEFAULT_ENTRY_ID
|
|
495
|
+
if (legacyRegister) {
|
|
490
496
|
const proxyScope = settingsService.register(PROXY_NS, Config, { base: config })
|
|
491
497
|
source = () => proxyScope.get()
|
|
492
498
|
proxyScope.watch(() => {
|
|
493
499
|
void requestSync().catch(() => {})
|
|
494
500
|
})
|
|
501
|
+
} else if (typeof settingsService?.update === 'function') {
|
|
502
|
+
source = () => readEntryConfig(configEditor, entryId, config)
|
|
503
|
+
ctx.on('settings/document-updated', (ns) => {
|
|
504
|
+
if (ns === entryId) void requestSync().catch(() => {})
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
// Without either generation the plugin still runs read-only on the
|
|
508
|
+
// composition config (source keeps returning it).
|
|
509
|
+
|
|
510
|
+
/** Persist one config patch through whichever settings generation is present. */
|
|
511
|
+
const writeSettings = async (patch) => {
|
|
512
|
+
if (settingsService === undefined) {
|
|
513
|
+
throw new Error('the settings service is unavailable in this composition — change the proxy configuration in the composition instead')
|
|
514
|
+
}
|
|
515
|
+
await settingsService.update(legacyRegister ? PROXY_NS : entryId, patch)
|
|
495
516
|
}
|
|
496
|
-
// Without a settings service in this composition, `source` keeps the
|
|
497
|
-
// composition default (initialized above) and the plugin still runs, read-only.
|
|
498
517
|
|
|
499
518
|
// ---------- Agent tools ----------
|
|
500
519
|
|
|
@@ -538,11 +557,11 @@ function apply(ctx, config) {
|
|
|
538
557
|
}],
|
|
539
558
|
},
|
|
540
559
|
execute: async (args) => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
560
|
+
try {
|
|
561
|
+
await writeSettings({ enabled: args.enabled === true })
|
|
562
|
+
} catch (error) {
|
|
563
|
+
throw new Error(`proxy_set: ${error?.message ?? error}`)
|
|
544
564
|
}
|
|
545
|
-
await settings.update(PROXY_NS, { enabled: args.enabled === true })
|
|
546
565
|
await requestSync()
|
|
547
566
|
return summarize(snapshot)
|
|
548
567
|
},
|
|
@@ -565,6 +584,7 @@ function apply(ctx, config) {
|
|
|
565
584
|
const lines = [
|
|
566
585
|
`路由: ${value.route}`,
|
|
567
586
|
`NO_PROXY 命中: ${value.bypassed}`,
|
|
587
|
+
`状态码: 走代理 ${value.proxy?.status ?? '—'} · 直连 ${value.direct?.status ?? '—'}`,
|
|
568
588
|
`走代理: ${badge(value.proxyBadge)}`,
|
|
569
589
|
`直连: ${badge(value.directBadge)}`,
|
|
570
590
|
`结论: ${value.short ?? '未知'}`,
|
|
@@ -603,10 +623,6 @@ function apply(ctx, config) {
|
|
|
603
623
|
}],
|
|
604
624
|
},
|
|
605
625
|
execute: async (args) => {
|
|
606
|
-
const settings = ctx.get('settings')
|
|
607
|
-
if (settings === undefined) {
|
|
608
|
-
throw new Error('proxy_config: the settings service is unavailable in this composition — change the proxy configuration in Settings instead')
|
|
609
|
-
}
|
|
610
626
|
const patch = {}
|
|
611
627
|
if (args.enabled !== undefined) patch.enabled = args.enabled === true
|
|
612
628
|
if (args.mode !== undefined) patch.mode = args.mode
|
|
@@ -623,7 +639,7 @@ function apply(ctx, config) {
|
|
|
623
639
|
}
|
|
624
640
|
}
|
|
625
641
|
if (args.noProxy !== undefined) patch.noProxy = String(args.noProxy).trim()
|
|
626
|
-
if (Object.keys(patch).length > 0) await
|
|
642
|
+
if (Object.keys(patch).length > 0) await writeSettings(patch)
|
|
627
643
|
await requestSync()
|
|
628
644
|
return summarize(snapshot)
|
|
629
645
|
},
|
|
@@ -708,16 +724,58 @@ function apply(ctx, config) {
|
|
|
708
724
|
}
|
|
709
725
|
if (is('/toggle', 'POST')) {
|
|
710
726
|
const body = await readJsonBody(req)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
727
|
+
try {
|
|
728
|
+
await writeSettings({ enabled: body.enabled === true })
|
|
729
|
+
} catch (error) {
|
|
730
|
+
sendJson(res, 503, { ok: false, error: String(error?.message ?? error) })
|
|
714
731
|
return
|
|
715
732
|
}
|
|
716
|
-
await settings.update(PROXY_NS, { enabled: body.enabled === true })
|
|
717
733
|
await requestSync()
|
|
718
734
|
sendJson(res, 200, { ok: true, status: summarize(snapshot) })
|
|
719
735
|
return
|
|
720
736
|
}
|
|
737
|
+
// Editable configuration for the client panel. dsh >= 0.2 removed
|
|
738
|
+
// the browser settings transport (`ctx.settingsScope`), so the panel
|
|
739
|
+
// reads and writes through this API instead (LESSONS §33).
|
|
740
|
+
if (is('/config', 'GET')) {
|
|
741
|
+
sendJson(res, 200, { ok: true, ...configPayload(source()) })
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
if (is('/config', 'POST')) {
|
|
745
|
+
const body = await readJsonBody(req)
|
|
746
|
+
const patch = {}
|
|
747
|
+
if (body.enabled !== undefined) patch.enabled = body.enabled === true
|
|
748
|
+
if (body.mode !== undefined) {
|
|
749
|
+
if (body.mode !== 'system' && body.mode !== 'custom') {
|
|
750
|
+
sendJson(res, 400, { ok: false, error: 'mode must be "system" or "custom"' })
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
patch.mode = body.mode
|
|
754
|
+
}
|
|
755
|
+
if (body.customUrl !== undefined) {
|
|
756
|
+
const raw = String(body.customUrl).trim()
|
|
757
|
+
if (raw === '') {
|
|
758
|
+
patch.customUrl = ''
|
|
759
|
+
} else {
|
|
760
|
+
const normalized = normalizeProxyUrl(raw)
|
|
761
|
+
if (normalized === null) {
|
|
762
|
+
sendJson(res, 400, { ok: false, error: `"${body.customUrl}" is not a usable proxy address — expected http(s)://host:port` })
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
patch.customUrl = normalized
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (body.noProxy !== undefined) patch.noProxy = String(body.noProxy).trim()
|
|
769
|
+
try {
|
|
770
|
+
if (Object.keys(patch).length > 0) await writeSettings(patch)
|
|
771
|
+
} catch (error) {
|
|
772
|
+
sendJson(res, 503, { ok: false, error: String(error?.message ?? error) })
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
await requestSync()
|
|
776
|
+
sendJson(res, 200, { ok: true, ...configPayload(source()) })
|
|
777
|
+
return
|
|
778
|
+
}
|
|
721
779
|
sendJson(res, 404, { ok: false, error: 'not found' })
|
|
722
780
|
} catch (error) {
|
|
723
781
|
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
package/lib/proxy-core.js
CHANGED
|
@@ -15,6 +15,60 @@
|
|
|
15
15
|
const PROXY_REG_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
|
|
16
16
|
const LOCAL_ENTRIES = ['localhost', '127.0.0.1', '::1']
|
|
17
17
|
|
|
18
|
+
/** Bundle row id (cordis.patch.yml) — also the settings namespace on dsh >= 0.2,
|
|
19
|
+
* where a plugin's OWN composition entry is its settings form (LESSONS §33). */
|
|
20
|
+
export const DEFAULT_ENTRY_ID = 'dsh-proxy-pro'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve this plugin's composition entry id — the settings namespace on
|
|
24
|
+
* dsh >= 0.2 (LESSONS §33). Prefers the row whose fiber IS this context, then
|
|
25
|
+
* the known bundle id; undefined when the editor cannot be read.
|
|
26
|
+
*/
|
|
27
|
+
export function discoverEntryId(configEditor, ctx) {
|
|
28
|
+
try {
|
|
29
|
+
const rows = configEditor?.configuration?.() ?? []
|
|
30
|
+
const own = rows.find((row) => row?.entry?.fiber === ctx?.fiber)
|
|
31
|
+
?? rows.find((row) => row?.entry?.options?.id === DEFAULT_ENTRY_ID)
|
|
32
|
+
const id = own?.entry?.options?.id
|
|
33
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Effective config of one entry: the composed layers, then the profile patch.
|
|
41
|
+
* Every Config field is a scalar, so a shallow merge is exact.
|
|
42
|
+
*/
|
|
43
|
+
export function readEntryConfig(configEditor, entryId, fallback) {
|
|
44
|
+
try {
|
|
45
|
+
const rows = configEditor?.configuration?.() ?? []
|
|
46
|
+
const own = rows.find((row) => row?.entry?.options?.id === entryId)
|
|
47
|
+
if (own !== undefined) return { ...fallback, ...(own.inherited ?? {}), ...(own.override ?? {}) }
|
|
48
|
+
} catch {
|
|
49
|
+
/* fall through to the apply-time config */
|
|
50
|
+
}
|
|
51
|
+
return fallback
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Editable configuration payload for the client panel (`/config` API). The
|
|
56
|
+
* browser cannot reach the settings transport on dsh >= 0.2, so the host hands
|
|
57
|
+
* the panel exactly the fields it edits (LESSONS §33).
|
|
58
|
+
*/
|
|
59
|
+
export function configPayload(resolved) {
|
|
60
|
+
return {
|
|
61
|
+
config: {
|
|
62
|
+
enabled: resolved?.enabled === true,
|
|
63
|
+
mode: resolved?.mode ?? 'none',
|
|
64
|
+
customUrl: resolved?.customUrl ?? '',
|
|
65
|
+
noProxy: resolved?.noProxy ?? '',
|
|
66
|
+
systemPollMs: resolved?.systemPollMs ?? 0,
|
|
67
|
+
},
|
|
68
|
+
writable: true,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
18
72
|
/** Normalize a bare `host:port` or already-schemed URL into an http(s) URL. */
|
|
19
73
|
export function normalizeProxyUrl(value) {
|
|
20
74
|
if (typeof value !== 'string') return null
|
|
@@ -429,14 +483,18 @@ export function classifyTargetFailure(error) {
|
|
|
429
483
|
* 链路不通 → 截图"代理与直连均失败"误导(2026-09-18 用户实况)。
|
|
430
484
|
*/
|
|
431
485
|
function dualBadge(probe) {
|
|
432
|
-
|
|
486
|
+
// Keep the HTTP code in the copy (2026-09-18 用户要求:展示原因时直接看到
|
|
487
|
+
// 状态码,而不是只有人话)。连接层失败没有 status,保持原文案。
|
|
488
|
+
const detail = (s) => String(s ?? '')
|
|
433
489
|
if (!probe) return { text: '结果未知', tone: 'warn' }
|
|
434
|
-
if (probe.verdict === 'usable')
|
|
490
|
+
if (probe.verdict === 'usable') {
|
|
491
|
+
return { text: Number.isFinite(probe.status) ? `链路通 · 目标正常响应(HTTP ${probe.status})` : '链路通 · 目标正常响应', tone: 'ok' }
|
|
492
|
+
}
|
|
435
493
|
if (probe.verdict === 'degraded') {
|
|
436
494
|
return { text: `链路通 · 目标拒绝(${detail(probe.short) || '请求被拒'})`, tone: 'warn' }
|
|
437
495
|
}
|
|
438
496
|
if (probe.kind === 'gateway' || probe.kind === 'server-error') {
|
|
439
|
-
return { text: `链路通 · 目标/网关报错(${detail(probe.short) ||
|
|
497
|
+
return { text: `链路通 · 目标/网关报错(${detail(probe.short) || `HTTP ${probe.status ?? '5xx'}`})`, tone: 'warn' }
|
|
440
498
|
}
|
|
441
499
|
if (probe.kind === 'auth') return { text: '链路不通 · 代理要求认证', tone: 'bad' }
|
|
442
500
|
return { text: `链路不通 · ${detail(probe.short) || '连接失败'}`, tone: 'bad' }
|
|
@@ -474,7 +532,9 @@ export function combineDualProbe(proxyProbe, directProbe) {
|
|
|
474
532
|
|| (p.verdict === 'unusable' && /NTLM|407/.test(String(p.short ?? '')))
|
|
475
533
|
|
|
476
534
|
const notReachable = (probe, label) => {
|
|
477
|
-
|
|
535
|
+
// Keep the HTTP code — the reason line should name the status
|
|
536
|
+
// (2026-09-18: user asked for the code to be visible where the cause is shown).
|
|
537
|
+
const detail = probe?.short ?? '未知原因'
|
|
478
538
|
return `${label}:${detail}`
|
|
479
539
|
}
|
|
480
540
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yanglaofish/dsh-proxy-pro",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "DSH proxy manager: route model requests, web fetches and spawned tools through the Windows system proxy or a custom address; per-URL route diagnosis, one-click toggle from the conversation header, a dedicated Settings page, and keepalive-aware 407 hints.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@deepseek-ai/cordis": "^4.0.0",
|
|
43
|
-
"@deepseek-ai/dsh-http-proxy": "
|
|
44
|
-
"@deepseek-ai/dsh-settings": "
|
|
45
|
-
"@deepseek-ai/dsh-system-prompt": "
|
|
46
|
-
"@deepseek-ai/dsh-tools": "
|
|
43
|
+
"@deepseek-ai/dsh-http-proxy": ">=0.1.5-rc.2 <0.3.0",
|
|
44
|
+
"@deepseek-ai/dsh-settings": ">=0.1.5-rc.2 <0.3.0",
|
|
45
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.1.5-rc.2 <0.3.0",
|
|
46
|
+
"@deepseek-ai/dsh-tools": ">=0.1.5-rc.2 <0.3.0",
|
|
47
47
|
"@deepseek-ai/schemastery": "^3.18.0"
|
|
48
48
|
},
|
|
49
49
|
"dsh": {
|