dsh-magpie-connector 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2
4
+
5
+ - **Only the checked line is re-measured.** 0.4.1 refreshed the whole latency pass after a line check, so every saved row's reading changed even though only one line was checked. `GET /api/dsh-magpie-connector/latency` now accepts `?line=<id>`: the host probes just that saved line and returns only its reading, and the settings page merges it into that row while every other row keeps its previous value. Unknown or unsaved ids measure nothing. The full pass on panel open and after a successful save is unchanged, and there is still no polling.
6
+
7
+ ## 0.4.1
8
+
9
+ - **Check refreshes latency.** A completed line check on a **saved** line now also re-runs the settings-page latency measurement (one `GET /api/dsh-magpie-connector/latency` pass), so the signal reading next to that line reflects the same moment as the check instead of the last panel open or save. Unsaved drafts trigger nothing (they have no latency row), the host measurement stays single-flight with its 600 ms deadline, and there is still no polling.
10
+
3
11
  ## 0.4.0
4
12
 
5
13
  - **Line panel polish.** Settings now collapses every configured line by default (it no longer force-expands the active one); clicking a line header toggles it, and the add/remove/reorder actions are always-visible DSH product-icon buttons with accessible names and tooltips. A newly added line still opens for editing and stays open through its first save (including when it was moved before saving) and when a save fails.
@@ -91,20 +91,49 @@ export function MagpieSettings({ api }) {
91
91
  setCards({ status: 'error', error: errorText(error), cards: [] });
92
92
  }
93
93
  };
94
- /** One measurement per panel open (and after a successful line write); failures stay silent. */
95
- const readLatency = async () => {
94
+ /** One measurement per panel open (and after a successful line write); failures stay silent.
95
+ * With `only`, just that saved line is re-measured and merged in — the other rows keep their
96
+ * reading instead of all flipping to 测速中 (the line-check refresh). */
97
+ const readLatency = async (only) => {
96
98
  const epoch = ++latencyEpoch.current;
97
- setLatency({ phase: 'measuring' });
99
+ if (only === undefined)
100
+ setLatency({ phase: 'measuring' });
101
+ else
102
+ setLatency(current => current.phase === 'done' ? { phase: 'done', readings: current.readings, measuring: new Set([only]) } : current);
103
+ const settle = () => {
104
+ // Superseded by a newer pass: drop the stale result but never leave the row stuck on 测速中.
105
+ if (only === undefined)
106
+ return;
107
+ setLatency(current => current.phase === 'done' ? { phase: 'done', readings: current.readings } : current);
108
+ };
98
109
  try {
99
- const response = await api({ method: 'GET', path: 'latency' });
100
- if (epoch !== latencyEpoch.current)
110
+ const response = await api({ method: 'GET', path: 'latency', ...(only === undefined ? {} : { query: { line: only } }) });
111
+ if (epoch !== latencyEpoch.current) {
112
+ settle();
101
113
  return;
114
+ }
102
115
  const readings = isOk(response) ? parseLatency(response.body) : undefined;
103
- setLatency(readings ? { phase: 'done', readings } : { phase: 'failed' });
116
+ if (only === undefined) {
117
+ setLatency(readings ? { phase: 'done', readings } : { phase: 'failed' });
118
+ return;
119
+ }
120
+ setLatency(current => {
121
+ const merged = new Map(current.phase === 'done' ? current.readings : []);
122
+ if (readings)
123
+ for (const [id, ms] of readings)
124
+ merged.set(id, ms);
125
+ return { phase: 'done', readings: merged };
126
+ });
104
127
  }
105
128
  catch {
106
- if (epoch === latencyEpoch.current)
129
+ if (epoch !== latencyEpoch.current) {
130
+ settle();
131
+ return;
132
+ }
133
+ if (only === undefined)
107
134
  setLatency({ phase: 'failed' });
135
+ else
136
+ settle();
108
137
  }
109
138
  };
110
139
  const clearUsage = () => {
@@ -250,7 +279,7 @@ export function MagpieSettings({ api }) {
250
279
  }
251
280
  };
252
281
  const groupNote = (provider) => noteScope === provider ? { notice, alert: usageAlert } : {};
253
- return h('div', { className: 'dmg-settings' }, h('style', null, settingsCss), h('p', { className: 'dmg-intro' }, '通过自托管的 Magpie 网关使用模型。用量卡片只读,打开本页时读取一次,之后只在你点击卡片上的刷新时重新读取。'), settingsError ? h('p', { className: 'dmg-error', role: 'alert' }, `无法读取设置:${settingsError}`) : null, settings ? h(Connection, { settings, saving: savingSettings, mutateSettings, api, latency,
282
+ return h('div', { className: 'dmg-settings' }, h('style', null, settingsCss), h('p', { className: 'dmg-intro' }, '通过自托管的 Magpie 网关使用模型。用量卡片只读,打开本页时读取一次,之后只在你点击卡片上的刷新时重新读取。'), settingsError ? h('p', { className: 'dmg-error', role: 'alert' }, `无法读取设置:${settingsError}`) : null, settings ? h(Connection, { settings, saving: savingSettings, mutateSettings, api, latency, onRefreshLatency: (lineId) => { void readLatency(lineId); },
254
283
  usage: h(UsageStatus, { cards, busy, ...(noteScope === undefined ? { notice, alert: usageAlert } : {}), onFetchAll: () => { void fetchAll(); }, saving: savingSettings }) })
255
284
  : settingsError ? null : h('p', { role: 'status', className: 'dmg-hint' }, '正在读取设置…'), ...providerOrder(groups, cardList).map(provider => h(ProviderGroup, {
256
285
  key: provider, provider,
@@ -328,7 +357,7 @@ function lineDrafts(settings) {
328
357
  }));
329
358
  }
330
359
  const lineSignature = (draft) => JSON.stringify([draft.id ?? null, draft.baseUrl.trim(), draft.accessMode]);
331
- function Connection({ settings, saving, mutateSettings, api, usage, latency }) {
360
+ function Connection({ settings, saving, mutateSettings, api, usage, latency, onRefreshLatency }) {
332
361
  const baseline = lineDrafts(settings);
333
362
  const [drafts, setDrafts] = useState(baseline);
334
363
  const [expanded, setExpanded] = useState(null);
@@ -477,6 +506,10 @@ function Connection({ settings, saving, mutateSettings, api, usage, latency }) {
477
506
  return;
478
507
  }
479
508
  setChecks(current => ({ ...current, [draft.draftId]: { signature, activeLineId: activeId, result: snapshot } }));
509
+ // A completed check is a fresh network sample of this line: re-measure just that saved
510
+ // line (unsaved drafts have no reading row, so they trigger nothing).
511
+ if (draft.id)
512
+ onRefreshLatency(draft.id);
480
513
  }
481
514
  catch (error) {
482
515
  if (epoch === checkEpoch.current)
@@ -1,4 +1,5 @@
1
- /** One measurement pass over every saved line: readings keyed by line id. */
1
+ /** One measurement pass over every saved line: readings keyed by line id. `measuring` marks a
2
+ * single row being re-measured on its own (line check), so other rows keep their reading. */
2
3
  export type LatencyState = {
3
4
  phase: 'idle';
4
5
  } | {
@@ -6,6 +7,7 @@ export type LatencyState = {
6
7
  } | {
7
8
  phase: 'done';
8
9
  readings: ReadonlyMap<string, number | null>;
10
+ measuring?: ReadonlySet<string>;
9
11
  } | {
10
12
  phase: 'failed';
11
13
  };
@@ -38,6 +38,8 @@ export function lineLatency(state, line) {
38
38
  return { state: 'failed' };
39
39
  if (state.phase !== 'done')
40
40
  return { state: 'measuring' };
41
+ if (state.measuring?.has(line.id))
42
+ return { state: 'measuring' };
41
43
  if (!state.readings.has(line.id))
42
44
  return { state: 'idle' };
43
45
  const ms = state.readings.get(line.id) ?? null;
package/lib/client.js CHANGED
@@ -79,6 +79,7 @@ window.__ModuleLoader__.load({
79
79
  if (!(!!line.id && parseable(line.baseUrl))) return { state: "idle" };
80
80
  if (state.phase === "failed") return { state: "failed" };
81
81
  if (state.phase !== "done") return { state: "measuring" };
82
+ if (state.measuring?.has(line.id)) return { state: "measuring" };
82
83
  if (!state.readings.has(line.id)) return { state: "idle" };
83
84
  const ms = state.readings.get(line.id) ?? null;
84
85
  return ms === null ? {
@@ -423,23 +424,57 @@ window.__ModuleLoader__.load({
423
424
  });
424
425
  }
425
426
  };
426
- /** One measurement per panel open (and after a successful line write); failures stay silent. */
427
- const readLatency = async () => {
427
+ /** One measurement per panel open (and after a successful line write); failures stay silent.
428
+ * With `only`, just that saved line is re-measured and merged in — the other rows keep their
429
+ * reading instead of all flipping to 测速中 (the line-check refresh). */
430
+ const readLatency = async (only) => {
428
431
  const epoch = ++latencyEpoch.current;
429
- setLatency({ phase: "measuring" });
432
+ if (only === void 0) setLatency({ phase: "measuring" });
433
+ else setLatency((current) => current.phase === "done" ? {
434
+ phase: "done",
435
+ readings: current.readings,
436
+ measuring: /* @__PURE__ */ new Set([only])
437
+ } : current);
438
+ const settle = () => {
439
+ if (only === void 0) return;
440
+ setLatency((current) => current.phase === "done" ? {
441
+ phase: "done",
442
+ readings: current.readings
443
+ } : current);
444
+ };
430
445
  try {
431
446
  const response = await api({
432
447
  method: "GET",
433
- path: "latency"
448
+ path: "latency",
449
+ ...only === void 0 ? {} : { query: { line: only } }
434
450
  });
435
- if (epoch !== latencyEpoch.current) return;
451
+ if (epoch !== latencyEpoch.current) {
452
+ settle();
453
+ return;
454
+ }
436
455
  const readings = isOk(response) ? parseLatency(response.body) : void 0;
437
- setLatency(readings ? {
438
- phase: "done",
439
- readings
440
- } : { phase: "failed" });
456
+ if (only === void 0) {
457
+ setLatency(readings ? {
458
+ phase: "done",
459
+ readings
460
+ } : { phase: "failed" });
461
+ return;
462
+ }
463
+ setLatency((current) => {
464
+ const merged = new Map(current.phase === "done" ? current.readings : []);
465
+ if (readings) for (const [id, ms] of readings) merged.set(id, ms);
466
+ return {
467
+ phase: "done",
468
+ readings: merged
469
+ };
470
+ });
441
471
  } catch {
442
- if (epoch === latencyEpoch.current) setLatency({ phase: "failed" });
472
+ if (epoch !== latencyEpoch.current) {
473
+ settle();
474
+ return;
475
+ }
476
+ if (only === void 0) setLatency({ phase: "failed" });
477
+ else settle();
443
478
  }
444
479
  };
445
480
  const clearUsage = () => {
@@ -586,6 +621,9 @@ window.__ModuleLoader__.load({
586
621
  mutateSettings,
587
622
  api,
588
623
  latency,
624
+ onRefreshLatency: (lineId) => {
625
+ readLatency(lineId);
626
+ },
589
627
  usage: (0, react.createElement)(UsageStatus, {
590
628
  cards,
591
629
  busy,
@@ -740,7 +778,7 @@ window.__ModuleLoader__.load({
740
778
  draft.baseUrl.trim(),
741
779
  draft.accessMode
742
780
  ]);
743
- function Connection({ settings, saving, mutateSettings, api, usage, latency }) {
781
+ function Connection({ settings, saving, mutateSettings, api, usage, latency, onRefreshLatency }) {
744
782
  const baseline = lineDrafts(settings);
745
783
  const [drafts, setDrafts] = (0, react.useState)(baseline);
746
784
  const [expanded, setExpanded] = (0, react.useState)(null);
@@ -922,6 +960,7 @@ window.__ModuleLoader__.load({
922
960
  result: snapshot
923
961
  }
924
962
  }));
963
+ if (draft.id) onRefreshLatency(draft.id);
925
964
  } catch (error) {
926
965
  if (epoch === checkEpoch.current) setCheckErrors((current) => ({
927
966
  ...current,
@@ -10,21 +10,16 @@ export interface LatencyControllerOptions {
10
10
  measure?: Measure;
11
11
  timeoutMs?: number;
12
12
  }
13
- /**
14
- * Read-only settings-page measurement: one TCP connect per saved line with a
15
- * parseable address, all in parallel, each capped by the selection deadline.
16
- * Measurements are single-flight (concurrent reads share one pass), never touch
17
- * settings, credentials or the selector cache, and are aborted on dispose.
18
- */
19
13
  export declare class LatencyController {
20
14
  private readonly lines;
21
15
  private readonly measure;
22
16
  private readonly timeoutMs;
23
17
  private readonly controllers;
24
- private inFlight;
18
+ private readonly passes;
25
19
  private disposed;
26
20
  constructor(lines: () => LineSettings[], options?: LatencyControllerOptions);
27
- read(): Promise<Response>;
28
- private measureOnce;
21
+ /** `lineId` narrows the pass to one saved line (the line-check refresh); omit for the full panel pass. */
22
+ read(lineId?: string): Promise<Response>;
23
+ private pass;
29
24
  dispose(): void;
30
25
  }
@@ -6,32 +6,42 @@ const measureSocket = async (host, port, timeoutMs, signal) => {
6
6
  /**
7
7
  * Read-only settings-page measurement: one TCP connect per saved line with a
8
8
  * parseable address, all in parallel, each capped by the selection deadline.
9
- * Measurements are single-flight (concurrent reads share one pass), never touch
10
- * settings, credentials or the selector cache, and are aborted on dispose.
9
+ * A `lineId` request measures only that saved line (the line-check refresh).
10
+ * Passes are single-flight per scope (a full pass also serves line requests),
11
+ * never touch settings, credentials or the selector cache, and are aborted on
12
+ * dispose.
11
13
  */
14
+ const FULL = '*';
12
15
  export class LatencyController {
13
16
  lines;
14
17
  measure;
15
18
  timeoutMs;
16
19
  controllers = new Set();
17
- inFlight;
20
+ passes = new Set();
18
21
  disposed = false;
19
22
  constructor(lines, options = {}) {
20
23
  this.lines = lines;
21
24
  this.measure = options.measure ?? measureSocket;
22
25
  this.timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
23
26
  }
24
- async read() {
27
+ /** `lineId` narrows the pass to one saved line (the line-check refresh); omit for the full panel pass. */
28
+ async read(lineId) {
25
29
  if (this.disposed)
26
30
  return Response.json({ lines: [] });
27
- const readings = await this.measureOnce();
28
- return Response.json({ lines: readings });
31
+ const scope = lineId ?? FULL;
32
+ const readings = await this.pass(scope, lineId);
33
+ return Response.json({ lines: scope === FULL ? readings : readings.filter(reading => reading.id === lineId) });
29
34
  }
30
- measureOnce() {
31
- if (this.inFlight)
32
- return this.inFlight;
35
+ pass(scope, lineId) {
36
+ for (const entry of this.passes) {
37
+ // A running full pass already measures this line; reuse it instead of probing twice.
38
+ if (entry.scope === scope || (scope !== FULL && entry.scope === FULL))
39
+ return entry.promise;
40
+ }
33
41
  const targets = [];
34
42
  for (const line of this.lines()) {
43
+ if (lineId !== undefined && line.id !== lineId)
44
+ continue;
35
45
  const endpoint = endpointOf(line.baseUrl);
36
46
  if (endpoint)
37
47
  targets.push({ id: line.id, host: endpoint.host, port: endpoint.port });
@@ -49,8 +59,9 @@ export class LatencyController {
49
59
  this.controllers.delete(controller);
50
60
  }
51
61
  }));
52
- this.inFlight = measuring.finally(() => { this.inFlight = undefined; });
53
- return this.inFlight;
62
+ const entry = { scope, promise: measuring.finally(() => { this.passes.delete(entry); }) };
63
+ this.passes.add(entry);
64
+ return entry.promise;
54
65
  }
55
66
  dispose() {
56
67
  if (this.disposed)
@@ -73,7 +73,7 @@ export function registerRoutes(fetch, runtime, speed = new SpeedController(), ca
73
73
  return Response.json(speed.get(session, runtime.current.catalog));
74
74
  },
75
75
  lineCheck: async (_request, body) => runtime.lineCheck(body),
76
- latency: async () => latency.read(),
76
+ latency: async (request) => latency.read(new URL(request.url).searchParams.get('line') ?? undefined),
77
77
  };
78
78
  /** GET-only latency diagnostics; every other resource keeps the GET/POST surface. */
79
79
  const resources = ['settings', 'cards', 'speed', 'latency'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-magpie-connector",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "DeepSeek Harness plugin: connect a self-hosted Magpie gateway with one real provider route per upstream owner (grouped model picker on stock DSH), automatic multi-line failover, read-only usage cards, model visibility, default effort and per-session fast mode",
5
5
  "license": "MIT",
6
6
  "author": "prgrmrwy",