@yanglaofish/dsh-proxy-pro 1.0.10 → 2.0.1
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 +1217 -905
- package/lib/client.js +149 -25
- package/lib/index.js +901 -774
- package/lib/proxy-core.js +79 -0
- 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
|
|
@@ -34,6 +35,7 @@ window.__ModuleLoader__.load({
|
|
|
34
35
|
var react = require("react");
|
|
35
36
|
var useState = react.useState;
|
|
36
37
|
var useEffect = react.useEffect;
|
|
38
|
+
var useCallback = react.useCallback;
|
|
37
39
|
var useRef = react.useRef;
|
|
38
40
|
var useSyncExternalStore = react.useSyncExternalStore;
|
|
39
41
|
|
|
@@ -107,18 +109,120 @@ window.__ModuleLoader__.load({
|
|
|
107
109
|
.catch(function () { return null; });
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
// ----
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
// ---- host-backed configuration store (dsh >= 0.2) ------------------
|
|
113
|
+
// The browser settings transport (`ctx.settingsScope`) was removed in dsh
|
|
114
|
+
// 0.2 (LESSONS §33), so the capsule and the settings page read/write the
|
|
115
|
+
// host's own /config API. The snapshot shape mirrors the old settings
|
|
116
|
+
// scope (`{ status, value, writable }`), so the render code below is
|
|
117
|
+
// unchanged; an older host simply has no /config route and the panel
|
|
118
|
+
// reports itself unavailable instead of crashing.
|
|
119
|
+
//
|
|
120
|
+
// ONE store for the whole client half: the header capsule and the settings
|
|
121
|
+
// page live in different React trees (the settings shell renders its own
|
|
122
|
+
// root), and per-apply stores left them showing different snapshots —
|
|
123
|
+
// "header ON but panel OFF" (LESSONS §39). A module-level singleton makes
|
|
124
|
+
// every contributor observe exactly one state.
|
|
125
|
+
var sharedProxyStore = null
|
|
126
|
+
|
|
127
|
+
function proxyStore() {
|
|
128
|
+
if (sharedProxyStore === null) sharedProxyStore = createProxyStore()
|
|
129
|
+
return sharedProxyStore
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createProxyStore() {
|
|
133
|
+
var listeners = [];
|
|
134
|
+
var snapshot = { status: "loading", value: undefined, writable: false, error: "" };
|
|
135
|
+
var inflight = null;
|
|
136
|
+
/** Cheap equality so an unchanged payload does not notify: every emit
|
|
137
|
+
* re-renders its subscribers, and a needless emit inside a render path
|
|
138
|
+
* is how a store turns into a request storm (review fix, LESSONS §40). */
|
|
139
|
+
function unchanged(a, b) {
|
|
140
|
+
return a.status === b.status
|
|
141
|
+
&& a.writable === b.writable
|
|
142
|
+
&& a.error === b.error
|
|
143
|
+
&& JSON.stringify(a.value) === JSON.stringify(b.value);
|
|
144
|
+
}
|
|
145
|
+
function emit(next) {
|
|
146
|
+
if (unchanged(snapshot, next)) return;
|
|
147
|
+
snapshot = next;
|
|
148
|
+
listeners.slice().forEach(function (fn) {
|
|
149
|
+
try { fn(); } catch (error) { console.error("[dsh-proxy-pro] listener failed:", error); }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Keep the last known state but surface why a write was refused: a
|
|
153
|
+
* silent rollback is how a rejected settings write looks like a UI bug
|
|
154
|
+
* (LESSONS §36 — "点一下马上弹回"). */
|
|
155
|
+
function fail(message) {
|
|
156
|
+
emit({ status: snapshot.status, value: snapshot.value, writable: snapshot.writable, error: message });
|
|
157
|
+
}
|
|
158
|
+
function accept(data) {
|
|
159
|
+
if (data === null || data.ok !== true || data.config === undefined || data.config === null) {
|
|
160
|
+
emit({ status: "unavailable", value: undefined, writable: false, error: (data && data.error) || "" });
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
emit({ status: "ready", value: data.config, writable: data.writable !== false, error: "" });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
function refresh() {
|
|
167
|
+
if (inflight !== null) return inflight;
|
|
168
|
+
inflight = fetchJson("/config").then(function (data) {
|
|
169
|
+
inflight = null;
|
|
170
|
+
accept(data);
|
|
171
|
+
return data;
|
|
172
|
+
}, function () { inflight = null; return null; });
|
|
173
|
+
return inflight;
|
|
174
|
+
}
|
|
175
|
+
function set(field, value) {
|
|
176
|
+
var body = {};
|
|
177
|
+
body[field] = value;
|
|
178
|
+
return fetchJson("/config", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
body: JSON.stringify(body),
|
|
182
|
+
}).then(function (data) {
|
|
183
|
+
if (data === null || data.ok !== true) {
|
|
184
|
+
var message = (data && data.error) || "配置写入失败(宿主未返回原因)";
|
|
185
|
+
fail(message);
|
|
186
|
+
throw new Error(message);
|
|
187
|
+
}
|
|
188
|
+
accept(data);
|
|
189
|
+
}, function (error) {
|
|
190
|
+
fail("无法连接宿主配置接口:" + String(error && error.message ? error.message : error));
|
|
191
|
+
throw error;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
subscribe: function (fn) {
|
|
196
|
+
listeners.push(fn);
|
|
197
|
+
return function () {
|
|
198
|
+
var index = listeners.indexOf(fn);
|
|
199
|
+
if (index >= 0) listeners.splice(index, 1);
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
getSnapshot: function () { return snapshot; },
|
|
203
|
+
refresh: refresh,
|
|
204
|
+
set: set,
|
|
205
|
+
setEnabled: function (next) { return set("enabled", next === true); },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Subscribe to the host-backed config store → `{ value, writable, ready }`.
|
|
210
|
+
* The subscribe/getSnapshot callbacks are identity-stable (`useCallback`)
|
|
211
|
+
* and the first load lives in an effect: calling `refresh()` from inside
|
|
212
|
+
* subscribe re-subscribed on every render and looped GET /config forever
|
|
213
|
+
* (review fix, LESSONS §40). */
|
|
214
|
+
function useProxySettings(store) {
|
|
215
|
+
var subscribe = useCallback(function (onChange) { return store.subscribe(onChange); }, [store]);
|
|
216
|
+
var getSnapshot = useCallback(function () { return store.getSnapshot(); }, [store]);
|
|
217
|
+
var snapshot = useSyncExternalStore(subscribe, getSnapshot);
|
|
218
|
+
useEffect(function () { store.refresh(); }, [store]);
|
|
117
219
|
var ready = snapshot.status === "ready" && snapshot.value !== undefined;
|
|
118
220
|
return {
|
|
119
221
|
value: ready ? snapshot.value : undefined,
|
|
120
222
|
writable: snapshot.writable === true && ready,
|
|
121
223
|
ready: snapshot.status === "unavailable" ? false : snapshot.status === "ready",
|
|
224
|
+
unavailable: snapshot.status === "unavailable",
|
|
225
|
+
error: snapshot.error || "",
|
|
122
226
|
};
|
|
123
227
|
}
|
|
124
228
|
|
|
@@ -177,8 +281,8 @@ window.__ModuleLoader__.load({
|
|
|
177
281
|
|
|
178
282
|
// ---- conversation header capsule button ----
|
|
179
283
|
function ProxyHeaderButton(props) {
|
|
180
|
-
var
|
|
181
|
-
var settings =
|
|
284
|
+
var store = props.store;
|
|
285
|
+
var settings = useProxySettings(store);
|
|
182
286
|
// Local optimistic value (2026-09-18): the capsule flips its visual
|
|
183
287
|
// immediately on click instead of waiting for scope.set's round-trip
|
|
184
288
|
// (the settings-page switch already behaves this way). null means no
|
|
@@ -201,9 +305,14 @@ window.__ModuleLoader__.load({
|
|
|
201
305
|
if (!settings.writable) return;
|
|
202
306
|
var next = !shownEnabled;
|
|
203
307
|
setOptimistic(next);
|
|
204
|
-
|
|
308
|
+
// The shared store receives the host's answer for this write, so the
|
|
309
|
+
// optimistic echo ends as soon as the round trip settles — success or
|
|
310
|
+
// failure. Waiting for "value === optimistic" left the capsule stuck ON
|
|
311
|
+
// whenever the host refused the write (LESSONS §39).
|
|
312
|
+
store.setEnabled(next).then(function () {
|
|
313
|
+
setOptimistic(null);
|
|
314
|
+
}, function (error) {
|
|
205
315
|
console.error("[dsh-proxy-pro] toggle failed:", error);
|
|
206
|
-
// Roll back to the committed value on failure.
|
|
207
316
|
setOptimistic(null);
|
|
208
317
|
});
|
|
209
318
|
// Toggle doubles as a manual "recheck now" even when the settings
|
|
@@ -217,6 +326,7 @@ window.__ModuleLoader__.load({
|
|
|
217
326
|
var title = active
|
|
218
327
|
? "代理已开启(界面开关)· 点击关闭"
|
|
219
328
|
: "代理已关闭 · 点击开启";
|
|
329
|
+
if (settings.error) title = title + "\n最近一次写入失败:" + settings.error;
|
|
220
330
|
return react.createElement(
|
|
221
331
|
"button",
|
|
222
332
|
{
|
|
@@ -236,8 +346,8 @@ window.__ModuleLoader__.load({
|
|
|
236
346
|
|
|
237
347
|
// ---- Settings page: 代理管理 ----
|
|
238
348
|
function ProxyManagerPanel(props) {
|
|
239
|
-
var
|
|
240
|
-
var settings =
|
|
349
|
+
var store = props.store;
|
|
350
|
+
var settings = useProxySettings(store);
|
|
241
351
|
// Re-fetch the host snapshot when the configuration changes (enabled /
|
|
242
352
|
// mode / customUrl / noProxy are the only inputs that move the effKey);
|
|
243
353
|
// no timed polling, mount + change-driven only (§17.3d).
|
|
@@ -260,7 +370,10 @@ window.__ModuleLoader__.load({
|
|
|
260
370
|
|
|
261
371
|
if (!settings.ready) {
|
|
262
372
|
return react.createElement("div", { className: "dsxprx", "data-proxy-settings": true },
|
|
263
|
-
react.createElement("p", { className: "dsxprx-hint" },
|
|
373
|
+
react.createElement("p", { className: "dsxprx-hint" },
|
|
374
|
+
settings.unavailable
|
|
375
|
+
? "宿主未提供配置接口(/dsh-proxy-pro/api/config)——请确认插件已完整安装、宿主已重启。"
|
|
376
|
+
: "代理设置加载中…"));
|
|
264
377
|
}
|
|
265
378
|
var value = settings.value || { enabled: false, mode: "system", customUrl: "", noProxy: "" };
|
|
266
379
|
var writable = settings.writable;
|
|
@@ -306,7 +419,7 @@ window.__ModuleLoader__.load({
|
|
|
306
419
|
|
|
307
420
|
function setField(field) {
|
|
308
421
|
return function (next) {
|
|
309
|
-
|
|
422
|
+
store.set(field, next).catch(function (error) {
|
|
310
423
|
console.error("[dsh-proxy-pro] save failed:", error);
|
|
311
424
|
});
|
|
312
425
|
};
|
|
@@ -358,6 +471,15 @@ window.__ModuleLoader__.load({
|
|
|
358
471
|
}))
|
|
359
472
|
),
|
|
360
473
|
|
|
474
|
+
// --- last write failure (never fail silently: LESSONS §36) ---
|
|
475
|
+
settings.error
|
|
476
|
+
? react.createElement("div", { className: "dsxprx-status", "data-active": "false", "data-proxy-error": true },
|
|
477
|
+
react.createElement("span", { className: "dsxprx-dot" }),
|
|
478
|
+
react.createElement("div", { className: "dsxprx-statusbody" },
|
|
479
|
+
react.createElement("span", { className: "dsxprx-statusline" }, "配置写入失败"),
|
|
480
|
+
react.createElement("span", { className: "dsxprx-subline" }, settings.error)))
|
|
481
|
+
: null,
|
|
482
|
+
|
|
361
483
|
// --- mode ---
|
|
362
484
|
react.createElement("div", { className: "dsxprx-field" },
|
|
363
485
|
react.createElement("label", { htmlFor: "dsxprx-mode" }, "代理地址来源"),
|
|
@@ -457,12 +579,13 @@ window.__ModuleLoader__.load({
|
|
|
457
579
|
}
|
|
458
580
|
|
|
459
581
|
/**
|
|
460
|
-
* Browser plugin body:
|
|
461
|
-
* conversation header button and the 代理配置 settings section.
|
|
582
|
+
* Browser plugin body: create the host-backed config store, then register
|
|
583
|
+
* the conversation header button and the 代理配置 settings section.
|
|
462
584
|
* @param ctx - client cordis context.
|
|
463
585
|
*/
|
|
464
586
|
function apply(ctx) {
|
|
465
|
-
var
|
|
587
|
+
var store = proxyStore();
|
|
588
|
+
ctx.effect(function () { return function () { /* no timers to clear */ }; }, "dsh-proxy-pro: config store");
|
|
466
589
|
ctx.slots.inject("settings.section", function () {
|
|
467
590
|
return ctx.slots.register({
|
|
468
591
|
name: "settings.section",
|
|
@@ -470,7 +593,7 @@ window.__ModuleLoader__.load({
|
|
|
470
593
|
order: 20,
|
|
471
594
|
label: "代理配置",
|
|
472
595
|
}, function Section() {
|
|
473
|
-
return react.createElement(ProxyManagerPanel, {
|
|
596
|
+
return react.createElement(ProxyManagerPanel, { store: store });
|
|
474
597
|
});
|
|
475
598
|
});
|
|
476
599
|
ctx.slots.inject("conversation.session.header.utilities", function () {
|
|
@@ -480,7 +603,7 @@ window.__ModuleLoader__.load({
|
|
|
480
603
|
order: 10,
|
|
481
604
|
label: "代理",
|
|
482
605
|
}, function Header() {
|
|
483
|
-
return react.createElement(ProxyHeaderButton, {
|
|
606
|
+
return react.createElement(ProxyHeaderButton, { store: store });
|
|
484
607
|
});
|
|
485
608
|
});
|
|
486
609
|
}
|
|
@@ -490,10 +613,11 @@ window.__ModuleLoader__.load({
|
|
|
490
613
|
// Cordis service names the browser loader must wait for — NOT package
|
|
491
614
|
// names (those belong in package.json's dsh.client.inject). Writing
|
|
492
615
|
// package names here keeps the plugin permanently pending and blocks
|
|
493
|
-
// web boot (see docs/LESSONS.md §6).
|
|
616
|
+
// web boot (see docs/LESSONS.md §6). `settingsScope` was one of these
|
|
617
|
+
// until dsh 0.2 removed that service; requiring it now would keep the
|
|
618
|
+
// whole client half permanently pending (LESSONS §33).
|
|
494
619
|
exports.inject = [
|
|
495
620
|
"slots",
|
|
496
|
-
"settingsScope",
|
|
497
621
|
];
|
|
498
622
|
return module.exports;
|
|
499
623
|
},
|