dsh-magpie-connector 0.4.1 → 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 +4 -0
- package/lib/client/SettingsPage.js +40 -11
- package/lib/client/latency.d.ts +3 -1
- package/lib/client/latency.js +2 -0
- package/lib/client.js +48 -13
- 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,9 @@
|
|
|
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
|
+
|
|
3
7
|
## 0.4.1
|
|
4
8
|
|
|
5
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.
|
|
@@ -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, onRefreshLatency: () => { void readLatency(); },
|
|
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,
|
|
@@ -477,10 +506,10 @@ function Connection({ settings, saving, mutateSettings, api, usage, latency, onR
|
|
|
477
506
|
return;
|
|
478
507
|
}
|
|
479
508
|
setChecks(current => ({ ...current, [draft.draftId]: { signature, activeLineId: activeId, result: snapshot } }));
|
|
480
|
-
// A completed check is a fresh network sample of this line:
|
|
481
|
-
//
|
|
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).
|
|
482
511
|
if (draft.id)
|
|
483
|
-
onRefreshLatency();
|
|
512
|
+
onRefreshLatency(draft.id);
|
|
484
513
|
}
|
|
485
514
|
catch (error) {
|
|
486
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,8 +621,8 @@ window.__ModuleLoader__.load({
|
|
|
586
621
|
mutateSettings,
|
|
587
622
|
api,
|
|
588
623
|
latency,
|
|
589
|
-
onRefreshLatency: () => {
|
|
590
|
-
readLatency();
|
|
624
|
+
onRefreshLatency: (lineId) => {
|
|
625
|
+
readLatency(lineId);
|
|
591
626
|
},
|
|
592
627
|
usage: (0, react.createElement)(UsageStatus, {
|
|
593
628
|
cards,
|
|
@@ -925,7 +960,7 @@ window.__ModuleLoader__.load({
|
|
|
925
960
|
result: snapshot
|
|
926
961
|
}
|
|
927
962
|
}));
|
|
928
|
-
if (draft.id) onRefreshLatency();
|
|
963
|
+
if (draft.id) onRefreshLatency(draft.id);
|
|
929
964
|
} catch (error) {
|
|
930
965
|
if (epoch === checkEpoch.current) setCheckErrors((current) => ({
|
|
931
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",
|