@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/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 the `proxy` settings namespace through the shared
14
- * `ctx.settingsScope` transport (same as dsh-plugin-proxy); runtime facts
15
- * come from the same-origin /dsh-proxy-pro/api endpoints served by the host
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
- // ---- reactive settings scope (the `proxy` namespace) ----
111
- /** @returns `{ value, writable, ready }` from a bound SettingsScope. */
112
- function useProxySnapshot(scope) {
113
- var snapshot = useSyncExternalStore(
114
- function (onChange) { return scope.subscribe(onChange); },
115
- function () { return scope.getSnapshot(); }
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 scope = props.scope;
181
- var settings = useProxySnapshot(scope);
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
- scope.set("enabled", next).catch(function (error) {
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 scope = props.scope;
240
- var settings = useProxySnapshot(scope);
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
- scope.set(field, next).catch(function (error) {
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: bind the `proxy` settings scope, then register the
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 scope = ctx.settingsScope.bind({ namespace: "proxy" });
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, { scope: scope });
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, { scope: scope });
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
  },