@ebony-vinyl/dsh-our-free-model 2.8.3 → 2.8.4
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/README.md +12 -5
- package/client.js +355 -22
- package/index.js +81 -1
- package/package.json +1 -1
- package/src/catalog.js +148 -31
- package/src/model-test.js +280 -2
- package/src/o2a.js +46 -9
- package/src/plugins.js +2 -2
- package/src/store.js +12 -0
- package/src/updater.js +21 -6
- package/vendor/channel-pack/pack.js +7 -2
package/src/model-test.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Model measurement: the text test this lane runs itself, and the figures the
|
|
3
|
+
* site publishes for the same models.
|
|
4
|
+
*
|
|
5
|
+
* The two halves answer the same question — "how is this model actually
|
|
6
|
+
* serving?" — from opposite directions. `measureModelTest` establishes it by
|
|
7
|
+
* sending the smallest request that can produce a verdict; the second half
|
|
8
|
+
* reads the site's own hourly statistics so a card can show the strip the site
|
|
9
|
+
* shows, rather than an invented one.
|
|
10
|
+
*
|
|
11
|
+
* They share a module because the release set has a hard ceiling: updaters from
|
|
12
|
+
* 1.3.1 and 2.5.9 reject any manifest listing more than 80 files, so a new
|
|
13
|
+
* module of this size cannot simply be added. Folding it in beside the
|
|
14
|
+
* measurement it complements keeps old installs upgradable.
|
|
15
|
+
*
|
|
16
|
+
* Text-test validation adapted from upstream PR #491; timing, cancellation and
|
|
17
|
+
* persistence complete issue #462.
|
|
18
|
+
*/
|
|
2
19
|
import { CODE, UpstreamError } from './http.js'
|
|
3
20
|
import { decodeWindow } from './store.js'
|
|
4
21
|
import { windowTokens } from './stream.js'
|
|
5
|
-
|
|
6
22
|
export const MODEL_TEST_TIMEOUT_MS = 300_000
|
|
7
23
|
|
|
8
24
|
/** Only a nonempty, normally finished tool-free answer establishes a successful text test. */
|
|
@@ -92,3 +108,265 @@ export function modelAvailability(id, snapshot, listed = false) {
|
|
|
92
108
|
ttftMs: measured?.ttftMs, latencyMs: measured?.latencyMs,
|
|
93
109
|
}
|
|
94
110
|
}
|
|
111
|
+
|
|
112
|
+
// ── the site's own per-model figures ─────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// The model square at the site root draws one performance strip per card: a
|
|
115
|
+
// success-rate bar of 24 hourly ticks, the average latency, the output speed,
|
|
116
|
+
// and a 详情 button that opens the trend charts. These functions read the same
|
|
117
|
+
// numbers from the same endpoint so the plugin's cards show the same strip
|
|
118
|
+
// rather than an invented one.
|
|
119
|
+
//
|
|
120
|
+
// The endpoint (`GET /api/perf-metrics/summary?hours=24`) is public — no
|
|
121
|
+
// credential — and answers every model in one document, each with its own
|
|
122
|
+
// `recent_success_series`. It sends no CORS headers, so a page served from the
|
|
123
|
+
// harness cannot call it directly; index.js hops through it.
|
|
124
|
+
//
|
|
125
|
+
// Two rules are enforced, both learned from the live data:
|
|
126
|
+
//
|
|
127
|
+
// 1. **A window that spans a channel change is not a verdict.** Two relayed
|
|
128
|
+
// models were moved onto a different upstream mid-window; their 24h rate
|
|
129
|
+
// still averages in the hours of fast failures from the lane they left, so it
|
|
130
|
+
// reads ~3% while the last hour reads ~93%. Such a model is reported as
|
|
131
|
+
// `settling` and its card draws no strip — a wrong number is worse than none.
|
|
132
|
+
//
|
|
133
|
+
// 2. **No endpoint, no strip.** When the site does not answer, the map is empty
|
|
134
|
+
// and every card renders exactly as it did before this feature existed.
|
|
135
|
+
|
|
136
|
+
/** How long one metrics read may take before the page gets an empty map. */
|
|
137
|
+
const METRICS_TIMEOUT_MS = 6000
|
|
138
|
+
|
|
139
|
+
/** The window the strip describes, in hours. The site's own strip says 24. */
|
|
140
|
+
export const WINDOW_HOURS = 24
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A model's window rate and its recent hour may differ by this much before the
|
|
144
|
+
* figure is withheld. The relay move left a 90-point gap; ordinary drift on a
|
|
145
|
+
* busy day stays well inside 20 (the live minimax card swings 57→96 across the
|
|
146
|
+
* day and is still shown).
|
|
147
|
+
*/
|
|
148
|
+
const SETTLING_GAP = 30
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The site root, derived from the lane's own base (`…/eac/v1` → `…`).
|
|
152
|
+
*
|
|
153
|
+
* The metrics endpoint lives beside the panel, not under the signed lane, so
|
|
154
|
+
* the `/eac/v1` suffix is dropped rather than rewritten. A base that does not
|
|
155
|
+
* look like a URL yields null and the feature stays off.
|
|
156
|
+
*/
|
|
157
|
+
export function siteRootFor(base) {
|
|
158
|
+
const text = String(base ?? '').trim()
|
|
159
|
+
if (text === '') return null
|
|
160
|
+
let url
|
|
161
|
+
try { url = new URL(text) } catch { return null }
|
|
162
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
163
|
+
// Drop the lane's own mount (`/eac/v1`, `/v1`) so the result is the site root
|
|
164
|
+
// the panel and its API are served from. A bare `/eac` is dropped too, for a
|
|
165
|
+
// base that names only the mount and no API version.
|
|
166
|
+
const path = url.pathname
|
|
167
|
+
.replace(/\/(?:eac\/)?v1\/?$/i, '')
|
|
168
|
+
.replace(/\/eac\/?$/i, '')
|
|
169
|
+
.replace(/\/+$/, '')
|
|
170
|
+
return `${url.origin}${path}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A finite non-negative number, or null.
|
|
175
|
+
*
|
|
176
|
+
* `null` and `''` must stay null rather than becoming 0: the endpoint uses null
|
|
177
|
+
* for "no sample in this hour", and `Number(null)` is 0 — which would paint a
|
|
178
|
+
* red 0% tick, or drop a latency line to the floor, for an hour that simply had
|
|
179
|
+
* no traffic.
|
|
180
|
+
*/
|
|
181
|
+
function metricOf(value) {
|
|
182
|
+
if (value === null || value === undefined || value === '') return null
|
|
183
|
+
const n = Number(value)
|
|
184
|
+
return Number.isFinite(n) && n >= 0 ? n : null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* One tick of the success bar.
|
|
189
|
+
*
|
|
190
|
+
* The site colours each hourly tick by its own rate — emerald at 99 and above,
|
|
191
|
+
* amber from 90, red below — so a glance reads the shape of the day, not just
|
|
192
|
+
* its average. `null` marks an hour with no sample: the site draws those grey.
|
|
193
|
+
*/
|
|
194
|
+
function tickOf(row) {
|
|
195
|
+
return metricOf(row?.success_rate)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Reduce one model's entry to what the card draws.
|
|
200
|
+
*
|
|
201
|
+
* Returns null when the entry carries nothing worth showing, and
|
|
202
|
+
* `{ settling: true }` when its window rate disagrees with how it is serving
|
|
203
|
+
* now by more than {@link SETTLING_GAP} points.
|
|
204
|
+
*
|
|
205
|
+
* The comparison deliberately skips the final bucket. The series ends with the
|
|
206
|
+
* hour still in progress (the endpoint's `window_end` is "now"), so that sample
|
|
207
|
+
* covers however many minutes have elapsed — three, or fifty-nine. Comparing a
|
|
208
|
+
* partial hour against a 24-hour average compares incomparable things and made
|
|
209
|
+
* the strip appear and disappear as the hour filled: on live data one model
|
|
210
|
+
* read a 34-point gap three minutes into the hour and an 18-point gap minutes
|
|
211
|
+
* later, flipping between hidden and shown. The newest *complete* hour is a
|
|
212
|
+
* settled sample, so the verdict stops flapping without changing which models
|
|
213
|
+
* are withheld.
|
|
214
|
+
*
|
|
215
|
+
* The reference is a recent hour rather than a median of the window on purpose:
|
|
216
|
+
* this endpoint reports hours with no traffic as 0 rather than null, so a median
|
|
217
|
+
* over the whole series is dominated by quiet hours and stops noticing the very
|
|
218
|
+
* upstream switch this check exists to catch.
|
|
219
|
+
*/
|
|
220
|
+
export function perfFrom(entry) {
|
|
221
|
+
if (entry === null || typeof entry !== 'object') return null
|
|
222
|
+
const rate = metricOf(entry.success_rate)
|
|
223
|
+
if (rate === null) return null
|
|
224
|
+
const latency = metricOf(entry.avg_latency_ms)
|
|
225
|
+
const speed = metricOf(entry.avg_tps)
|
|
226
|
+
|
|
227
|
+
const series = Array.isArray(entry.recent_success_series) ? entry.recent_success_series : []
|
|
228
|
+
const ticks = series.map(tickOf)
|
|
229
|
+
// Everything but the in-progress bucket. With a single bucket there is no
|
|
230
|
+
// complete hour to compare against, so the window rate stands unexamined.
|
|
231
|
+
const complete = ticks.slice(0, -1).filter(value => value !== null)
|
|
232
|
+
const reference = complete.length > 0 ? complete[complete.length - 1] : null
|
|
233
|
+
|
|
234
|
+
// A window rate that disagrees violently with the recent complete hour
|
|
235
|
+
// describes a window that spanned a change of upstream, not the model as it
|
|
236
|
+
// serves now.
|
|
237
|
+
if (reference !== null && Math.abs(reference - rate) > SETTLING_GAP) {
|
|
238
|
+
return { settling: true, rate, newest: reference }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
rate,
|
|
243
|
+
latencyMs: latency,
|
|
244
|
+
speed,
|
|
245
|
+
// Every hourly sample, oldest first: the bar the card draws.
|
|
246
|
+
ticks,
|
|
247
|
+
windowHours: series.length > 0 ? series.length : WINDOW_HOURS,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* One point of a trend chart, from the per-model series.
|
|
253
|
+
*
|
|
254
|
+
* The site draws two charts from this one series: latency over time and
|
|
255
|
+
* availability over time. Both read the same hourly rows, so they are reduced
|
|
256
|
+
* together rather than in two passes.
|
|
257
|
+
*/
|
|
258
|
+
function pointOf(row) {
|
|
259
|
+
if (row === null || typeof row !== 'object') return null
|
|
260
|
+
const ts = metricOf(row.ts)
|
|
261
|
+
if (ts === null) return null
|
|
262
|
+
return {
|
|
263
|
+
ts,
|
|
264
|
+
latencyMs: metricOf(row.avg_latency_ms),
|
|
265
|
+
ttftMs: metricOf(row.avg_ttft_ms),
|
|
266
|
+
rate: metricOf(row.success_rate),
|
|
267
|
+
speed: metricOf(row.avg_tps),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The trend series for one model, oldest first.
|
|
273
|
+
*
|
|
274
|
+
* A point with no latency sample keeps its slot so the two charts stay aligned
|
|
275
|
+
* in time; the renderer skips it when drawing the line.
|
|
276
|
+
*/
|
|
277
|
+
export function seriesFrom(entry) {
|
|
278
|
+
const rows = Array.isArray(entry?.series) ? entry.series : []
|
|
279
|
+
return rows.map(pointOf).filter(point => point !== null)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Read one model's full trend series (`GET /api/perf-metrics?model=…`).
|
|
284
|
+
*
|
|
285
|
+
* The summary endpoint answers every model's headline figures in one request;
|
|
286
|
+
* the per-model endpoint is the only one that carries the hourly series. The
|
|
287
|
+
* card asks for this only when its 详情 is opened, so a roster of twelve cards
|
|
288
|
+
* costs one request until the reader actually looks.
|
|
289
|
+
*/
|
|
290
|
+
export async function readSeries({ root, modelId, fetch, signal, timeoutMs = METRICS_TIMEOUT_MS }) {
|
|
291
|
+
if (root === null || root === undefined || typeof modelId !== 'string' || modelId === '') return null
|
|
292
|
+
// Already cancelled: do not spend a round trip on a request nobody will read.
|
|
293
|
+
if (signal?.aborted === true) return null
|
|
294
|
+
const url = `${root}/api/perf-metrics?model=${encodeURIComponent(modelId)}&hours=${WINDOW_HOURS}`
|
|
295
|
+
const controller = new AbortController()
|
|
296
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
297
|
+
timer.unref?.()
|
|
298
|
+
const onAbort = () => controller.abort()
|
|
299
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
302
|
+
if (!response.ok) return null
|
|
303
|
+
const text = await response.text()
|
|
304
|
+
let payload
|
|
305
|
+
try { payload = JSON.parse(text) } catch { return null }
|
|
306
|
+
const series = seriesFrom(payload?.data)
|
|
307
|
+
if (series.length === 0) return null
|
|
308
|
+
return {
|
|
309
|
+
series,
|
|
310
|
+
windowStart: metricOf(payload?.data?.window_start),
|
|
311
|
+
windowEnd: metricOf(payload?.data?.window_end),
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
return null
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timer)
|
|
317
|
+
signal?.removeEventListener?.('abort', onAbort)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Read every model's figures from the site in one request.
|
|
323
|
+
*
|
|
324
|
+
* The endpoint answers all models together, so this is one round trip rather
|
|
325
|
+
* than one per model. The map is keyed by the id the site spells, which is the
|
|
326
|
+
* same id the catalog uses. Models the site does not answer for are absent
|
|
327
|
+
* rather than present-and-null: absence is what the card renders as "no strip".
|
|
328
|
+
*
|
|
329
|
+
* The site's root is deliberately not echoed back: it is the co-paid gateway's
|
|
330
|
+
* host, the seal keeps it server-side, and the page never reads it. Only the
|
|
331
|
+
* figures travel.
|
|
332
|
+
*
|
|
333
|
+
* Never throws — a failure is the same answer as "the site has no figures",
|
|
334
|
+
* which the card already handles by drawing nothing.
|
|
335
|
+
*/
|
|
336
|
+
export async function readPerf({ root, fetch, signal, timeoutMs = METRICS_TIMEOUT_MS }) {
|
|
337
|
+
if (root === null || root === undefined) return { models: {} }
|
|
338
|
+
// Already cancelled: do not spend a round trip on a request nobody will read.
|
|
339
|
+
if (signal?.aborted === true) return { models: {} }
|
|
340
|
+
const url = `${root}/api/perf-metrics/summary?hours=${WINDOW_HOURS}`
|
|
341
|
+
const controller = new AbortController()
|
|
342
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
343
|
+
timer.unref?.()
|
|
344
|
+
const onAbort = () => controller.abort()
|
|
345
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
346
|
+
try {
|
|
347
|
+
const response = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
348
|
+
if (!response.ok) return { models: {} }
|
|
349
|
+
const text = await response.text()
|
|
350
|
+
let payload
|
|
351
|
+
try { payload = JSON.parse(text) } catch { return { models: {} } }
|
|
352
|
+
const rows = payload?.data?.models
|
|
353
|
+
if (!Array.isArray(rows)) return { models: {} }
|
|
354
|
+
const models = {}
|
|
355
|
+
for (const row of rows) {
|
|
356
|
+
const id = typeof row?.model_name === 'string' ? row.model_name.trim() : ''
|
|
357
|
+
if (id === '') continue
|
|
358
|
+
const perf = perfFrom(row)
|
|
359
|
+
if (perf !== null) models[id] = perf
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
models,
|
|
363
|
+
windowStart: metricOf(payload?.data?.window_start),
|
|
364
|
+
windowEnd: metricOf(payload?.data?.window_end),
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
return { models: {} }
|
|
368
|
+
} finally {
|
|
369
|
+
clearTimeout(timer)
|
|
370
|
+
signal?.removeEventListener?.('abort', onAbort)
|
|
371
|
+
}
|
|
372
|
+
}
|
package/src/o2a.js
CHANGED
|
@@ -631,6 +631,26 @@ function renameAndCompleteTools(payload, shape, { keep } = {}) {
|
|
|
631
631
|
dropDanglingToolChoice(payload, shape, known);
|
|
632
632
|
return { renames, injected, known };
|
|
633
633
|
}
|
|
634
|
+
function chatMessageHasContent(message) {
|
|
635
|
+
const keys = ["content", "reasoning_content", "reasoning"];
|
|
636
|
+
for (const key of keys) {
|
|
637
|
+
const value = message[key];
|
|
638
|
+
if (typeof value === "string") {
|
|
639
|
+
if (value !== "") return true;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
if (value.length > 0) return true;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (value !== null && typeof value === "object") {
|
|
647
|
+
if (Object.keys(value).length > 0) return true;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (value !== void 0 && value !== null) return true;
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
634
654
|
function sanitizeToolHistory(payload, shape, known) {
|
|
635
655
|
const fateOf = (name) => {
|
|
636
656
|
if (typeof name !== "string" || name === "") return { verdict: "drop" };
|
|
@@ -644,13 +664,14 @@ function sanitizeToolHistory(payload, shape, known) {
|
|
|
644
664
|
if (messages2 === null) return;
|
|
645
665
|
const droppedCalls2 = /* @__PURE__ */ new Set();
|
|
646
666
|
const droppedFunctions = /* @__PURE__ */ new Set();
|
|
667
|
+
const emptied = /* @__PURE__ */ new Set();
|
|
647
668
|
let changed2 = false;
|
|
648
669
|
for (let i = 0; i < messages2.length; i += 1) {
|
|
649
670
|
const message = messages2[i];
|
|
650
671
|
if (!message || typeof message !== "object") continue;
|
|
651
672
|
if (Array.isArray(message.tool_calls)) {
|
|
652
673
|
const kept = [];
|
|
653
|
-
let touched =
|
|
674
|
+
let touched = message.tool_calls.length === 0;
|
|
654
675
|
for (const call of message.tool_calls) {
|
|
655
676
|
const fate = fateOf(call?.function?.name);
|
|
656
677
|
if (fate.verdict === "keep") {
|
|
@@ -667,33 +688,49 @@ function sanitizeToolHistory(payload, shape, known) {
|
|
|
667
688
|
touched = true;
|
|
668
689
|
}
|
|
669
690
|
if (touched) {
|
|
670
|
-
|
|
691
|
+
if (kept.length === 0) {
|
|
692
|
+
const { tool_calls: _emptied, ...rest } = message;
|
|
693
|
+
messages2[i] = rest;
|
|
694
|
+
if (message.tool_calls.length > 0 && message.function_call === void 0) emptied.add(rest);
|
|
695
|
+
} else {
|
|
696
|
+
messages2[i] = { ...message, tool_calls: kept };
|
|
697
|
+
}
|
|
671
698
|
changed2 = true;
|
|
672
699
|
}
|
|
673
700
|
}
|
|
674
|
-
|
|
675
|
-
|
|
701
|
+
const current = messages2[i];
|
|
702
|
+
if (current.function_call && typeof current.function_call === "object") {
|
|
703
|
+
const fate = fateOf(current.function_call.name);
|
|
676
704
|
if (fate.verdict === "rename") {
|
|
677
|
-
messages2[i] = { ...
|
|
705
|
+
messages2[i] = { ...current, function_call: { ...current.function_call, name: fate.official } };
|
|
678
706
|
changed2 = true;
|
|
679
707
|
} else if (fate.verdict === "drop") {
|
|
680
|
-
if (typeof
|
|
681
|
-
const { function_call: _dropped, ...rest } =
|
|
708
|
+
if (typeof current.function_call.name === "string") droppedFunctions.add(current.function_call.name);
|
|
709
|
+
const { function_call: _dropped, ...rest } = current;
|
|
682
710
|
messages2[i] = rest;
|
|
683
711
|
changed2 = true;
|
|
684
712
|
}
|
|
685
713
|
}
|
|
686
|
-
|
|
714
|
+
const row = messages2[i];
|
|
715
|
+
if (row.role === "tool" && typeof row.tool_call_id === "string" && droppedCalls2.has(row.tool_call_id)) {
|
|
687
716
|
messages2.splice(i, 1);
|
|
688
717
|
i -= 1;
|
|
689
718
|
changed2 = true;
|
|
690
719
|
}
|
|
691
|
-
if (
|
|
720
|
+
if (row.role === "function" && typeof row.name === "string" && droppedFunctions.has(row.name)) {
|
|
692
721
|
messages2.splice(i, 1);
|
|
693
722
|
i -= 1;
|
|
694
723
|
changed2 = true;
|
|
695
724
|
}
|
|
696
725
|
}
|
|
726
|
+
if (emptied.size > 0) {
|
|
727
|
+
for (let i = messages2.length - 1; i >= 0; i -= 1) {
|
|
728
|
+
if (emptied.has(messages2[i]) && !chatMessageHasContent(messages2[i])) {
|
|
729
|
+
messages2.splice(i, 1);
|
|
730
|
+
changed2 = true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
697
734
|
if (changed2 && messages2.length === 0) payload.messages = [];
|
|
698
735
|
return;
|
|
699
736
|
}
|
package/src/plugins.js
CHANGED
|
@@ -660,8 +660,8 @@ export function builtinDescriptors() {
|
|
|
660
660
|
{
|
|
661
661
|
manifest: {
|
|
662
662
|
id: 'core-model-workspace',
|
|
663
|
-
name: '
|
|
664
|
-
description: '
|
|
663
|
+
name: '模型工作台(magpie)',
|
|
664
|
+
description: '内置的本地模型工作台页面与模型目录同步(引擎为 magpie)。',
|
|
665
665
|
icon: 'panels',
|
|
666
666
|
provides: ['ui'],
|
|
667
667
|
},
|
package/src/store.js
CHANGED
|
@@ -256,6 +256,14 @@ export const SETTINGS_INITIAL = {
|
|
|
256
256
|
feedUrl: '',
|
|
257
257
|
/** Minutes between announcement-feed polls; floored at 5. */
|
|
258
258
|
feedPollMinutes: 30,
|
|
259
|
+
/** Where the in-plugin feedback form submits. Empty keeps the built-in
|
|
260
|
+
* project server; an override lets a self-hosting operator point at their
|
|
261
|
+
* own collector. Feedback is never sent anywhere else. */
|
|
262
|
+
feedbackUrl: '',
|
|
263
|
+
/** GitHub login this install completed for the feedback form: `{ token,
|
|
264
|
+
* login, at }`, or null. The token only authorizes feedback submission —
|
|
265
|
+
* it is not an API credential and is never sent to a model endpoint. */
|
|
266
|
+
feedbackAuth: null,
|
|
259
267
|
/** Raise OS-level notifications for new announcements and updates (the
|
|
260
268
|
* browser asks for permission on the user's click). */
|
|
261
269
|
notifyOs: false,
|
|
@@ -276,6 +284,10 @@ export const SETTINGS_INITIAL = {
|
|
|
276
284
|
/** When the running code was hot-reloaded into place, and how many times. */
|
|
277
285
|
reloadedAt: 0,
|
|
278
286
|
reloadCount: 0,
|
|
287
|
+
/** The `reloadedAt` the browser has already acknowledged. Stored here, not
|
|
288
|
+
* in localStorage, so a new origin (another port or the desktop shell's
|
|
289
|
+
* random port) does not replay the same notice. */
|
|
290
|
+
reloadNoticeSeen: 0,
|
|
279
291
|
/** Version installed by the in-app upgrader, for the settings page. */
|
|
280
292
|
installedVersion: '',
|
|
281
293
|
}
|
package/src/updater.js
CHANGED
|
@@ -856,12 +856,26 @@ export function verifyInstalled(pkgDir, manifest) {
|
|
|
856
856
|
* @param {string} pkgDir - the installed package directory
|
|
857
857
|
* @returns {boolean}
|
|
858
858
|
*/
|
|
859
|
-
export function isPackageManagerOwned(pkgDir) {
|
|
860
|
-
const profileDir =
|
|
859
|
+
export function isPackageManagerOwned(pkgDir, packageName = PACKAGE_NAME) {
|
|
860
|
+
const profileDir = profileDirOf(pkgDir)
|
|
861
861
|
const lockfile = readTextFile(path.join(profileDir, 'pnpm-lock.yaml'))
|
|
862
|
-
if (lockfile !== undefined && lockfileDeclaresPackage(lockfile,
|
|
862
|
+
if (lockfile !== undefined && lockfileDeclaresPackage(lockfile, packageName)) return true
|
|
863
863
|
const modules = readTextFile(path.join(profileDir, 'node_modules', '.modules.yaml'))
|
|
864
|
-
return modules !== undefined && modulesHoistsPackage(modules,
|
|
864
|
+
return modules !== undefined && modulesHoistsPackage(modules, packageName)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* The profile root a package directory belongs to.
|
|
869
|
+
*
|
|
870
|
+
* A bare install is <profile>/node_modules/<name>; a scoped one is
|
|
871
|
+
* <profile>/node_modules/@scope/<name>. Walking to node_modules instead of
|
|
872
|
+
* counting two parents keeps both layouts pointed at the profile, where pnpm
|
|
873
|
+
* writes its lockfile and .modules.yaml (issue #773).
|
|
874
|
+
*/
|
|
875
|
+
export function profileDirOf(pkgDir) {
|
|
876
|
+
let dir = path.resolve(pkgDir)
|
|
877
|
+
while (dir !== path.dirname(dir) && path.basename(dir) !== 'node_modules') dir = path.dirname(dir)
|
|
878
|
+
return path.dirname(dir)
|
|
865
879
|
}
|
|
866
880
|
|
|
867
881
|
function readTextFile(file) {
|
|
@@ -879,9 +893,10 @@ function readTextFile(file) {
|
|
|
879
893
|
* full install — a real tree inside the profile declared as a relative
|
|
880
894
|
* `link:`. A `file:`, tarball-URL or registry specifier does record a source
|
|
881
895
|
* pnpm can re-extract, and stays "owned". */
|
|
882
|
-
function lockfileDeclaresPackage(text, name) {
|
|
896
|
+
export function lockfileDeclaresPackage(text, name) {
|
|
883
897
|
const lines = text.split(/\r?\n/)
|
|
884
|
-
const
|
|
898
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
899
|
+
const entry = new RegExp(`^(\\s*)['"]?${escaped}['"]?\\s*:\\s*$`)
|
|
885
900
|
for (let index = 0; index < lines.length; index += 1) {
|
|
886
901
|
const match = entry.exec(lines[index])
|
|
887
902
|
if (match === null) continue
|
|
@@ -58738,7 +58738,10 @@ function registerChannelPackEndpoints(ctx, pool, lobsterai, qoder, qoderCn, trae
|
|
|
58738
58738
|
const values = await collectCreditBalances(accounts, GEMINI, {
|
|
58739
58739
|
resolve: (ref) => ctx.credentials.resolve(ref),
|
|
58740
58740
|
fetchBalanceDetailed: async (credential) => {
|
|
58741
|
-
const { balance, error, tier } = await fetchGeminiCreditBalance(credential, GEMINI, {
|
|
58741
|
+
const { balance, error, tier } = await fetchGeminiCreditBalance(credential, GEMINI, {
|
|
58742
|
+
force: req.force === true,
|
|
58743
|
+
...config.fetcher === void 0 ? {} : { fetcher: config.fetcher }
|
|
58744
|
+
});
|
|
58742
58745
|
const extra = tier === void 0 ? void 0 : { accountTier: { label: tier.label, title: `${tier.name}\uFF08${tier.id}\uFF09` } };
|
|
58743
58746
|
return {
|
|
58744
58747
|
balance,
|
|
@@ -65535,7 +65538,9 @@ function apply(ctx, config = {}) {
|
|
|
65535
65538
|
});
|
|
65536
65539
|
});
|
|
65537
65540
|
registerChannelPackRpc(ctx, pool, lobsterai, qoder, qoderCn, trae, cline, loomy, raccoon, minimax, zcode, gemini, modelAdapters, {
|
|
65538
|
-
...config
|
|
65541
|
+
...config,
|
|
65542
|
+
// ⚠️ 配额等附属查询必须复用同一出口;省略会让 gemini-credits 回落到裸 fetch。
|
|
65543
|
+
fetcher: network.fetch
|
|
65539
65544
|
});
|
|
65540
65545
|
mountOpenAiGateway(ctx, pool);
|
|
65541
65546
|
installLoopResume(ctx);
|