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 +8 -0
- package/lib/client/SettingsPage.js +42 -9
- package/lib/client/latency.d.ts +3 -1
- package/lib/client/latency.js +2 -0
- package/lib/client.js +50 -11
- package/lib/host/latency.d.ts +4 -9
- package/lib/host/latency.js +22 -11
- package/lib/host/routes.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
package/lib/client/latency.d.ts
CHANGED
|
@@ -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
|
};
|
package/lib/client/latency.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
451
|
+
if (epoch !== latencyEpoch.current) {
|
|
452
|
+
settle();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
436
455
|
const readings = isOk(response) ? parseLatency(response.body) : void 0;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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,
|
package/lib/host/latency.d.ts
CHANGED
|
@@ -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
|
|
18
|
+
private readonly passes;
|
|
25
19
|
private disposed;
|
|
26
20
|
constructor(lines: () => LineSettings[], options?: LatencyControllerOptions);
|
|
27
|
-
|
|
28
|
-
|
|
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
|
}
|
package/lib/host/latency.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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)
|
package/lib/host/routes.js
CHANGED
|
@@ -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.
|
|
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",
|