@tokenbuddy/tb-admin 1.0.33 → 1.0.34
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/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +28 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/seller.d.ts +40 -1
- package/dist/src/seller.d.ts.map +1 -1
- package/dist/src/seller.js +132 -2
- package/dist/src/seller.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +8 -6
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.d.ts +1 -0
- package/dist/src/ui-command.d.ts.map +1 -1
- package/dist/src/ui-command.js +7 -2
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.js +17 -0
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +29 -0
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +455 -111
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +262 -143
- package/dist/src/ui-static.js.map +1 -1
- package/dist/src/upstream-balance-probe.d.ts +2 -40
- package/dist/src/upstream-balance-probe.d.ts.map +1 -1
- package/dist/src/upstream-balance-probe.js +1 -378
- package/dist/src/upstream-balance-probe.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +31 -0
- package/src/seller.ts +179 -3
- package/src/ui-actions.ts +10 -6
- package/src/ui-command.ts +7 -2
- package/src/ui-server.ts +18 -1
- package/src/ui-state.ts +533 -111
- package/src/ui-static.ts +262 -143
- package/src/upstream-balance-probe.ts +13 -505
- package/tests/admin.test.ts +416 -39
- package/tests/seller.test.ts +51 -0
- package/tests/ui-state-fleet.test.ts +272 -3
- package/tests/ui-static-row.test.ts +273 -8
package/dist/src/ui-state.js
CHANGED
|
@@ -6,6 +6,7 @@ export class AdminUiState {
|
|
|
6
6
|
options;
|
|
7
7
|
fetchJson;
|
|
8
8
|
balanceCache = new BalanceProbeCache();
|
|
9
|
+
machineSpecsCache = new Map();
|
|
9
10
|
constructor(options) {
|
|
10
11
|
this.options = options;
|
|
11
12
|
this.configManager = options.configPath ? new ConfigManager(options.configPath) : options.configManager;
|
|
@@ -84,7 +85,7 @@ export class AdminUiState {
|
|
|
84
85
|
this.fetchFlyApps().catch((err) => {
|
|
85
86
|
return { __error: err.message };
|
|
86
87
|
}),
|
|
87
|
-
this.
|
|
88
|
+
this.fetchManagedRegistry().catch((err) => {
|
|
88
89
|
return { __error: err.message, sellers: [] };
|
|
89
90
|
})
|
|
90
91
|
]);
|
|
@@ -117,21 +118,25 @@ export class AdminUiState {
|
|
|
117
118
|
const rows = [];
|
|
118
119
|
const consumedFly = new Set();
|
|
119
120
|
const consumedRegistry = new Set();
|
|
121
|
+
const specsByApp = await this.fetchFlyMachineSpecsForApps(Array.from(flyByName.keys()));
|
|
122
|
+
const registryTargets = [];
|
|
120
123
|
// Phase 1: registry first (因为有 id + url + 详细 metadata)
|
|
121
124
|
for (const entry of registryDoc.sellers || []) {
|
|
122
|
-
const flyMatch =
|
|
125
|
+
const flyMatch = findFlyAppForEntry(flyByName, entry);
|
|
123
126
|
const dataSource = flyMatch ? "both" : "registry";
|
|
124
127
|
if (flyMatch) {
|
|
125
128
|
consumedFly.add(flyMatch.name);
|
|
126
129
|
}
|
|
127
130
|
consumedRegistry.add(entry.id);
|
|
128
|
-
|
|
129
|
-
rows.push(snapshot.row);
|
|
131
|
+
registryTargets.push({ entry, flyMatch, dataSource });
|
|
130
132
|
}
|
|
133
|
+
rows.push(...await Promise.all(registryTargets.map(async ({ entry, flyMatch, dataSource }) => {
|
|
134
|
+
const snapshot = await this.sellerSnapshot(entry, flyMatch, dataSource, { balanceTimeoutMs: 8000, machineSpecs: flyMatch ? specsByApp.get(flyMatch.name) : undefined });
|
|
135
|
+
return snapshot.row;
|
|
136
|
+
})));
|
|
131
137
|
// Phase 2: fly-only apps (registry 没有)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
continue;
|
|
138
|
+
const flyOnlyApps = Array.from(flyByName.values()).filter((app) => !consumedFly.has(app.name));
|
|
139
|
+
rows.push(...await Promise.all(flyOnlyApps.map(async (app) => {
|
|
135
140
|
const stubEntry = {
|
|
136
141
|
id: app.name,
|
|
137
142
|
name: app.name,
|
|
@@ -141,18 +146,72 @@ export class AdminUiState {
|
|
|
141
146
|
paymentMethods: [],
|
|
142
147
|
models: []
|
|
143
148
|
};
|
|
144
|
-
const snapshot = await this.sellerSnapshot(stubEntry, app, "fly");
|
|
145
|
-
|
|
149
|
+
const snapshot = await this.sellerSnapshot(stubEntry, app, "fly", { includeOperator: true, balanceTimeoutMs: 8000, machineSpecs: specsByApp.get(app.name) });
|
|
150
|
+
return snapshot.row;
|
|
151
|
+
})));
|
|
152
|
+
return rows;
|
|
153
|
+
}
|
|
154
|
+
async sellerRegistryRows() {
|
|
155
|
+
const registryDoc = await this.fetchManagedRegistry();
|
|
156
|
+
return (registryDoc.sellers || []).map((entry) => {
|
|
157
|
+
const match = this.matchSellerProfile(entry);
|
|
158
|
+
return {
|
|
159
|
+
...baseSellerRow(entry, match.name, "registry"),
|
|
160
|
+
publishStatus: "checking",
|
|
161
|
+
detailStatus: "pending"
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async sellerInventory() {
|
|
166
|
+
const registryDoc = await this.fetchManagedRegistry();
|
|
167
|
+
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
168
|
+
const flyByName = new Map();
|
|
169
|
+
for (const app of flyApps) {
|
|
170
|
+
if (app?.name) {
|
|
171
|
+
flyByName.set(app.name, app);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const specsByApp = await this.fetchFlyMachineSpecsForApps(Array.from(flyByName.keys()));
|
|
175
|
+
const rows = [];
|
|
176
|
+
const consumedFly = new Set();
|
|
177
|
+
for (const entry of registryDoc.sellers || []) {
|
|
178
|
+
const flyMatch = findFlyAppForEntry(flyByName, entry);
|
|
179
|
+
const dataSource = flyMatch ? "both" : "registry";
|
|
180
|
+
if (flyMatch) {
|
|
181
|
+
consumedFly.add(flyMatch.name);
|
|
182
|
+
}
|
|
183
|
+
const match = this.matchSellerProfile(entry);
|
|
184
|
+
const row = baseSellerRow(entry, match.name, dataSource, flyMatch, flyMatch ? specsByApp.get(flyMatch.name) : undefined);
|
|
185
|
+
row.publishStatus = flyMatch ? "published" : "registry_only";
|
|
186
|
+
row.detailStatus = dataSource === "registry" ? "skipped" : "pending";
|
|
187
|
+
if (dataSource === "registry") {
|
|
188
|
+
row.registryAlert = true;
|
|
189
|
+
row.alertReason = "registry 收录了但 fly app 失踪 — 严重事故, 立即下线";
|
|
190
|
+
row.removeHint = "立即下线 (registry-only)";
|
|
191
|
+
}
|
|
192
|
+
rows.push(row);
|
|
193
|
+
}
|
|
194
|
+
const flyOnlyApps = Array.from(flyByName.values()).filter((app) => !consumedFly.has(app.name));
|
|
195
|
+
for (const app of flyOnlyApps) {
|
|
196
|
+
const stubEntry = sellerEntryFromFlyApp(app);
|
|
197
|
+
const row = baseSellerRow(stubEntry, undefined, "fly", app, specsByApp.get(app.name));
|
|
198
|
+
row.publishStatus = "unpublished";
|
|
199
|
+
row.detailStatus = "pending";
|
|
200
|
+
row.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
201
|
+
rows.push(row);
|
|
146
202
|
}
|
|
147
203
|
return rows;
|
|
148
204
|
}
|
|
205
|
+
async refreshSellerRows(rows) {
|
|
206
|
+
return await Promise.all(rows.map((row) => this.refreshSellerRow(row)));
|
|
207
|
+
}
|
|
149
208
|
/**
|
|
150
209
|
* Step 13 v1.1: 拉 flyctl apps list --json. 默认走 seller.ts 真实
|
|
151
210
|
* flyctl spawn, 测试或受限环境可注入 options.flyApps closure.
|
|
152
211
|
*/
|
|
153
212
|
async fetchFlyApps() {
|
|
154
213
|
if (this.options.flyApps) {
|
|
155
|
-
return await this.options.flyApps();
|
|
214
|
+
return (await this.options.flyApps()).filter((app) => isSellerFlyAppName(app.name));
|
|
156
215
|
}
|
|
157
216
|
// 默认路径: 走 SellerCommandRunner.ls(true) 真 spawn flyctl.
|
|
158
217
|
// 避免在 ui-state.ts 里 import 整个 seller module 引入循环依赖,
|
|
@@ -170,7 +229,7 @@ export class AdminUiState {
|
|
|
170
229
|
const runner = new mod.SellerCommandRunner(this.configManager);
|
|
171
230
|
const result = await runner.ls(true);
|
|
172
231
|
if (result && typeof result === "object" && "apps" in result) {
|
|
173
|
-
return result.apps;
|
|
232
|
+
return result.apps.filter((app) => isSellerFlyAppName(app.name));
|
|
174
233
|
}
|
|
175
234
|
return [];
|
|
176
235
|
}
|
|
@@ -178,37 +237,40 @@ export class AdminUiState {
|
|
|
178
237
|
return [];
|
|
179
238
|
}
|
|
180
239
|
}
|
|
181
|
-
async
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const document = await this.fetchRegistry();
|
|
185
|
-
let entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
186
|
-
let dataSource = "registry";
|
|
187
|
-
let flyApp;
|
|
188
|
-
if (!entry) {
|
|
189
|
-
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
190
|
-
const flyMatch = flyApps.find((app) => app.name === id || app.name === `tb-seller-${id}` || app.name === id.replace(/^tb-seller-/, ""));
|
|
191
|
-
if (!flyMatch) {
|
|
192
|
-
throw new Error(`seller \`${id}\` not found in fly.io apps or bootstrap registry`);
|
|
193
|
-
}
|
|
194
|
-
entry = {
|
|
195
|
-
id: flyMatch.name,
|
|
196
|
-
name: flyMatch.name,
|
|
197
|
-
app: flyMatch.name,
|
|
198
|
-
url: `https://${flyMatch.name}.fly.dev`,
|
|
199
|
-
supportedProtocols: [],
|
|
200
|
-
paymentMethods: [],
|
|
201
|
-
models: []
|
|
202
|
-
};
|
|
203
|
-
flyApp = flyMatch;
|
|
204
|
-
dataSource = "fly";
|
|
240
|
+
async fetchFlyMachineSpecsForApps(appNames) {
|
|
241
|
+
if (!this.options.flyMachineSpecs && this.options.flyApps) {
|
|
242
|
+
return new Map(appNames.map((appName) => [appName, undefined]));
|
|
205
243
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
244
|
+
const entries = [];
|
|
245
|
+
for (const appName of appNames) {
|
|
246
|
+
entries.push([appName, await this.fetchFlyMachineSpecs(appName)]);
|
|
247
|
+
}
|
|
248
|
+
return new Map(entries);
|
|
249
|
+
}
|
|
250
|
+
async fetchFlyMachineSpecs(appName) {
|
|
251
|
+
if (this.machineSpecsCache.has(appName)) {
|
|
252
|
+
return this.machineSpecsCache.get(appName);
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const specs = this.options.flyMachineSpecs
|
|
256
|
+
? await this.options.flyMachineSpecs(appName)
|
|
257
|
+
: await this.defaultFlyMachineSpecs(appName);
|
|
258
|
+
this.machineSpecsCache.set(appName, specs);
|
|
259
|
+
return specs;
|
|
210
260
|
}
|
|
211
|
-
|
|
261
|
+
catch {
|
|
262
|
+
this.machineSpecsCache.set(appName, undefined);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async defaultFlyMachineSpecs(appName) {
|
|
267
|
+
const mod = await import("./seller.js");
|
|
268
|
+
const runner = new mod.SellerCommandRunner(this.configManager);
|
|
269
|
+
return runner.machineSpecs(appName);
|
|
270
|
+
}
|
|
271
|
+
async sellerDetail(id) {
|
|
272
|
+
const { entry, flyApp, dataSource } = await this.resolveSellerTarget(id);
|
|
273
|
+
const snapshot = await this.sellerSnapshot(entry, flyApp, dataSource, { includeOperator: true, balanceTimeoutMs: 8000 });
|
|
212
274
|
const config = snapshot.config?.config || snapshot.config || {};
|
|
213
275
|
const upstreams = snapshot.upstreams || {};
|
|
214
276
|
return {
|
|
@@ -238,12 +300,73 @@ export class AdminUiState {
|
|
|
238
300
|
models: modelRows(upstreams, config, snapshot.status)
|
|
239
301
|
};
|
|
240
302
|
}
|
|
241
|
-
async
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
throw new Error(`seller \`${id}\` not found in bootstrap registry`);
|
|
303
|
+
async refreshSellerRow(row) {
|
|
304
|
+
const dataSource = row.dataSource || "both";
|
|
305
|
+
if (dataSource === "registry") {
|
|
306
|
+
return { ...row, nodeStatus: "unknown", detailStatus: "skipped", detailUpdatedAt: new Date().toISOString() };
|
|
246
307
|
}
|
|
308
|
+
const entry = sellerEntryFromRow(row);
|
|
309
|
+
const match = this.matchSellerProfile(entry);
|
|
310
|
+
const manifestPromise = this.probeManifest(entry.url);
|
|
311
|
+
if (!match.profile) {
|
|
312
|
+
const manifestOk = await manifestPromise;
|
|
313
|
+
return {
|
|
314
|
+
...row,
|
|
315
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
316
|
+
detailStatus: manifestOk ? "fresh" : "error",
|
|
317
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
318
|
+
error: manifestOk ? row.error : (row.error || "No matching local admin profile; /manifest probe failed")
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const [manifestOk, status, upstreams, config] = await Promise.all([
|
|
322
|
+
manifestPromise,
|
|
323
|
+
this.fetchSellerAdminJson(match.profile, "/operator/status").catch((err) => ({ error: err.message })),
|
|
324
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/upstreams").catch((err) => ({ error: err.message })),
|
|
325
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/config").catch((err) => ({ error: err.message }))
|
|
326
|
+
]);
|
|
327
|
+
const balance = await this.operatorBalanceSnapshot(match.profile, 8000).catch(() => unavailableBalanceSnapshot("seller balance endpoint unavailable"));
|
|
328
|
+
const configDocument = config?.config || config || {};
|
|
329
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
330
|
+
const upstreamUrl = stringValue(configDocument.upstreamUrl || normalizedUpstreams?.upstreamUrl || status?.upstream?.url || status?.upstreamUrl);
|
|
331
|
+
if (status?.error) {
|
|
332
|
+
return {
|
|
333
|
+
...row,
|
|
334
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
335
|
+
upstreamDomain: upstreamUrl ? (hostName(upstreamUrl) || row.upstreamDomain) : row.upstreamDomain,
|
|
336
|
+
upstreamStatus: upstreamStatus(normalizedUpstreams?.status || row.upstreamStatus),
|
|
337
|
+
discountRatio: numberValue(configDocument.discountRatio ?? normalizedUpstreams?.discountRatio) ?? row.discountRatio,
|
|
338
|
+
modelsCount: numberValue(normalizedUpstreams?.models?.length ?? row.modelsCount) ?? row.modelsCount,
|
|
339
|
+
...balanceFields(balance, row),
|
|
340
|
+
detailStatus: "error",
|
|
341
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
342
|
+
error: status.error
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const capacity = status?.capacity || {};
|
|
346
|
+
return {
|
|
347
|
+
...row,
|
|
348
|
+
nodeStatus: manifestOk ? "active" : nodeStatus(status?.status || row.nodeStatus),
|
|
349
|
+
upstreamDomain: upstreamUrl ? (hostName(upstreamUrl) || row.upstreamDomain) : row.upstreamDomain,
|
|
350
|
+
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status || row.upstreamStatus),
|
|
351
|
+
discountRatio: numberValue(configDocument.discountRatio ?? normalizedUpstreams?.discountRatio) ?? row.discountRatio,
|
|
352
|
+
capacityUsed: numberValue(capacity.activeConnections) ?? row.capacityUsed,
|
|
353
|
+
capacityLimit: numberValue(capacity.maxConnections) ?? row.capacityLimit,
|
|
354
|
+
...runtimeUsageFields(status?.runtime, row),
|
|
355
|
+
ttftMs: numberValue(status?.latency?.ttftMs) ?? row.ttftMs,
|
|
356
|
+
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs) ?? row.avgInferenceMs,
|
|
357
|
+
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs) ?? row.lastInferenceMs,
|
|
358
|
+
avgTokensPerSecond: numberValue(status?.latency?.avgTokensPerSecond) ?? row.avgTokensPerSecond,
|
|
359
|
+
lastTokensPerSecond: numberValue(status?.latency?.lastTokensPerSecond) ?? row.lastTokensPerSecond,
|
|
360
|
+
latencySamples: numberValue(status?.latency?.sampleCount) ?? row.latencySamples,
|
|
361
|
+
modelsCount: numberValue(normalizedUpstreams?.models?.length ?? row.modelsCount) ?? row.modelsCount,
|
|
362
|
+
...balanceFields(balance, row),
|
|
363
|
+
detailStatus: "fresh",
|
|
364
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
365
|
+
error: undefined
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
async rawSellerConfig(id) {
|
|
369
|
+
const { entry } = await this.resolveSellerTarget(id);
|
|
247
370
|
const match = this.matchSellerProfile(entry);
|
|
248
371
|
if (!match.profile) {
|
|
249
372
|
throw new Error(`seller \`${entry.id}\` has no matching local admin profile`);
|
|
@@ -251,17 +374,66 @@ export class AdminUiState {
|
|
|
251
374
|
const response = await this.fetchSellerAdminJson(match.profile, "/operator/admin/config");
|
|
252
375
|
return { entry, profileName: match.localProfile ? match.name : undefined, config: response.config || response };
|
|
253
376
|
}
|
|
377
|
+
async resolveSellerTarget(id) {
|
|
378
|
+
let document;
|
|
379
|
+
let registryError;
|
|
380
|
+
try {
|
|
381
|
+
document = await this.fetchManagedRegistry();
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
registryError = err;
|
|
385
|
+
document = { version: 0, sellers: [] };
|
|
386
|
+
}
|
|
387
|
+
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
388
|
+
const flyByName = new Map(flyApps.map((app) => [app.name, app]));
|
|
389
|
+
const entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
390
|
+
if (entry) {
|
|
391
|
+
const flyApp = findFlyAppForEntry(flyByName, entry);
|
|
392
|
+
return { entry, flyApp, dataSource: flyApp ? "both" : "registry" };
|
|
393
|
+
}
|
|
394
|
+
const flyApp = flyApps.find((app) => app.name === id || app.name === `tb-seller-${id}` || app.name === id.replace(/^tb-seller-/, ""));
|
|
395
|
+
if (flyApp) {
|
|
396
|
+
return { entry: sellerEntryFromFlyApp(flyApp), flyApp, dataSource: "fly" };
|
|
397
|
+
}
|
|
398
|
+
if (registryError) {
|
|
399
|
+
throw registryError;
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`seller \`${id}\` not found in fly.io apps or bootstrap registry`);
|
|
402
|
+
}
|
|
254
403
|
async fetchRegistry() {
|
|
255
404
|
const profile = this.activeBootstrapProfile();
|
|
256
405
|
const baseUrl = this.options.url || profile.profile?.url;
|
|
257
406
|
if (!baseUrl) {
|
|
258
407
|
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url.");
|
|
259
408
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
409
|
+
return normalizeRegistryDocument(await this.fetchBootstrapJson(`${trimSlash(baseUrl)}/registry/sellers`), "bootstrap registry response");
|
|
410
|
+
}
|
|
411
|
+
async fetchManagedRegistry() {
|
|
412
|
+
const profile = this.activeBootstrapProfile();
|
|
413
|
+
const baseUrl = this.options.url || profile.profile?.url;
|
|
414
|
+
const token = this.options.token || profile.profile?.token;
|
|
415
|
+
if (!baseUrl) {
|
|
416
|
+
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url.");
|
|
417
|
+
}
|
|
418
|
+
if (!token) {
|
|
419
|
+
return this.fetchRegistry();
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const managed = normalizeRegistryDocument(await this.fetchBootstrapJson(`${trimSlash(baseUrl)}/platform/sellers`, {
|
|
423
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
424
|
+
}), "platform sellers response");
|
|
425
|
+
const publicDoc = await this.fetchRegistry().catch(() => undefined);
|
|
426
|
+
if (!publicDoc) {
|
|
427
|
+
return managed;
|
|
428
|
+
}
|
|
429
|
+
return mergeRegistryDocuments(publicDoc, managed);
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
if (isUnavailablePlatformSellersEndpoint(err)) {
|
|
433
|
+
return this.fetchRegistry();
|
|
434
|
+
}
|
|
435
|
+
throw err;
|
|
263
436
|
}
|
|
264
|
-
return document;
|
|
265
437
|
}
|
|
266
438
|
activeBootstrapProfile() {
|
|
267
439
|
const envProfile = process.env.TOKENBUDDY_ADMIN_PROFILE;
|
|
@@ -318,9 +490,9 @@ export class AdminUiState {
|
|
|
318
490
|
* (**不**用 registry entry.status 决定绿点)
|
|
319
491
|
* - registry-only 行 (dataSource="registry") → 标红 (registryAlert=true)
|
|
320
492
|
*/
|
|
321
|
-
async sellerSnapshot(entry, flyApp, dataSource) {
|
|
493
|
+
async sellerSnapshot(entry, flyApp, dataSource, options = {}) {
|
|
322
494
|
const match = this.matchSellerProfile(entry);
|
|
323
|
-
const baseRow = baseSellerRow(entry, match.name, dataSource, flyApp);
|
|
495
|
+
const baseRow = baseSellerRow(entry, match.name, dataSource, flyApp, options.machineSpecs);
|
|
324
496
|
// Step 13: 立即下线 / Apply 按钮 hint 文案
|
|
325
497
|
if (dataSource === "registry") {
|
|
326
498
|
baseRow.registryAlert = true;
|
|
@@ -330,85 +502,71 @@ export class AdminUiState {
|
|
|
330
502
|
else if (dataSource === "fly") {
|
|
331
503
|
baseRow.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
332
504
|
}
|
|
333
|
-
if (dataSource === "
|
|
334
|
-
// fly-only 行没 registry entry, 也可能没 profile. 仍然跑 manifest
|
|
335
|
-
// probe (entry.url 来自 flyApp 推断 https://<name>.fly.dev), 但
|
|
336
|
-
// 没 profile 时只算 "unknown" (灰点) — 因为 vendor 没法管控一个
|
|
337
|
-
// 还没发布的 instance, 不该 throw auth_unknown 把 UI 卡死.
|
|
338
|
-
const manifestOk = await this.probeManifest(entry.url);
|
|
505
|
+
if (dataSource === "registry") {
|
|
339
506
|
return {
|
|
340
507
|
row: {
|
|
341
508
|
...baseRow,
|
|
342
|
-
nodeStatus:
|
|
343
|
-
error: manifestOk ? undefined : "未发布到 registry, /manifest 探测失败 (不视为事故)"
|
|
509
|
+
nodeStatus: "unknown"
|
|
344
510
|
}
|
|
345
511
|
};
|
|
346
512
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
513
|
+
let manifestOk = false;
|
|
514
|
+
const manifestPromise = this.probeManifest(entry.url).then((ok) => {
|
|
515
|
+
manifestOk = ok;
|
|
516
|
+
return ok;
|
|
517
|
+
});
|
|
518
|
+
if (dataSource === "fly" && (!options.includeOperator || !match.profile)) {
|
|
519
|
+
// fly-only 行没 registry entry, 也可能没 profile. 仍然跑 manifest
|
|
520
|
+
// probe (entry.url 来自 flyApp 推断 https://<name>.fly.dev), 但
|
|
521
|
+
// 没 profile 时只算 "unknown" (灰点) — 因为 vendor 没法管控一个
|
|
522
|
+
// 还没发布的 instance, 不该 throw auth_unknown 把 UI 卡死.
|
|
523
|
+
manifestOk = await manifestPromise;
|
|
350
524
|
return {
|
|
351
525
|
row: {
|
|
352
526
|
...baseRow,
|
|
353
|
-
nodeStatus: "unknown",
|
|
354
|
-
error: "
|
|
527
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
528
|
+
error: manifestOk ? undefined : "未发布到 registry, /manifest 探测失败 (不视为事故)"
|
|
355
529
|
}
|
|
356
530
|
};
|
|
357
531
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (dataSource === "registry") {
|
|
532
|
+
if (!match.profile) {
|
|
533
|
+
manifestOk = await manifestPromise;
|
|
361
534
|
return {
|
|
362
535
|
row: {
|
|
363
536
|
...baseRow,
|
|
364
|
-
nodeStatus: "unknown"
|
|
537
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
538
|
+
error: manifestOk ? "No matching local admin profile" : "No matching local admin profile; /manifest probe failed"
|
|
365
539
|
}
|
|
366
540
|
};
|
|
367
541
|
}
|
|
368
542
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const manifestOk = await this.probeManifest(entry.url);
|
|
372
|
-
if (manifestOk) {
|
|
373
|
-
return {
|
|
374
|
-
row: {
|
|
375
|
-
...baseRow,
|
|
376
|
-
nodeStatus: "active"
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
// /manifest 失败: 试 /operator/status (老路径, 需要 vendor profile)
|
|
381
|
-
if (!match.profile) {
|
|
382
|
-
return {
|
|
383
|
-
row: {
|
|
384
|
-
...baseRow,
|
|
385
|
-
nodeStatus: "auth_unknown",
|
|
386
|
-
error: "No matching local admin profile (post /manifest fallback)"
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
const [status, service, upstreams, config] = await Promise.all([
|
|
543
|
+
const [, status, service, upstreams, config] = await Promise.all([
|
|
544
|
+
manifestPromise,
|
|
391
545
|
this.fetchSellerAdminJson(match.profile, "/operator/status").catch((err) => ({ error: err.message })),
|
|
392
546
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/service").catch((err) => ({ error: err.message })),
|
|
393
547
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/upstreams").catch((err) => ({ error: err.message })),
|
|
394
548
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/config").catch((err) => ({ error: err.message }))
|
|
395
549
|
]);
|
|
396
550
|
const configDocument = config?.config || config || {};
|
|
397
|
-
const balance = await this.balanceSnapshot(configDocument, upstreams);
|
|
551
|
+
const balance = await this.balanceSnapshot(match.profile, configDocument, upstreams, options.balanceTimeoutMs);
|
|
398
552
|
return {
|
|
399
553
|
status,
|
|
400
554
|
service,
|
|
401
555
|
upstreams,
|
|
402
556
|
config,
|
|
403
557
|
balance,
|
|
404
|
-
row: mergeSellerRow(
|
|
558
|
+
row: mergeSellerRow({
|
|
559
|
+
...baseRow,
|
|
560
|
+
nodeStatus: manifestOk ? "active" : baseRow.nodeStatus
|
|
561
|
+
}, entry, status, service, upstreams, configDocument, balance, manifestOk)
|
|
405
562
|
};
|
|
406
563
|
}
|
|
407
564
|
catch (err) {
|
|
565
|
+
await manifestPromise.catch(() => false);
|
|
408
566
|
return {
|
|
409
567
|
row: {
|
|
410
568
|
...baseRow,
|
|
411
|
-
nodeStatus: "unknown",
|
|
569
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
412
570
|
error: err.message
|
|
413
571
|
}
|
|
414
572
|
};
|
|
@@ -435,21 +593,58 @@ export class AdminUiState {
|
|
|
435
593
|
return false;
|
|
436
594
|
}
|
|
437
595
|
}
|
|
438
|
-
async fetchSellerAdminJson(profile, pathName) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
596
|
+
async fetchSellerAdminJson(profile, pathName, options = {}) {
|
|
597
|
+
const controller = options.timeoutMs ? new AbortController() : undefined;
|
|
598
|
+
const timer = controller ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
599
|
+
try {
|
|
600
|
+
return await this.fetchJson(`${trimSlash(profile.url)}${pathName}`, {
|
|
601
|
+
headers: {
|
|
602
|
+
"Content-Type": "application/json",
|
|
603
|
+
Authorization: `Bearer ${profile.token}`
|
|
604
|
+
},
|
|
605
|
+
...(controller ? { signal: controller.signal } : {})
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
if (timer) {
|
|
610
|
+
clearTimeout(timer);
|
|
443
611
|
}
|
|
444
|
-
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async fetchBootstrapJson(url, init) {
|
|
615
|
+
const controller = new AbortController();
|
|
616
|
+
const timer = setTimeout(() => controller.abort(), 10000);
|
|
617
|
+
try {
|
|
618
|
+
return await this.fetchJson(url, { ...init, signal: controller.signal });
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
clearTimeout(timer);
|
|
622
|
+
}
|
|
445
623
|
}
|
|
446
|
-
async balanceSnapshot(config, upstreams) {
|
|
624
|
+
async balanceSnapshot(profile, config, upstreams, timeoutMs) {
|
|
447
625
|
if (config?.error) {
|
|
448
626
|
return undefined;
|
|
449
627
|
}
|
|
450
628
|
if (stringValue(config.upstreamBalanceProbe?.template) === "none") {
|
|
451
629
|
return undefined;
|
|
452
630
|
}
|
|
631
|
+
const operatorBalance = await this.operatorBalanceSnapshot(profile, timeoutMs).catch(() => undefined);
|
|
632
|
+
if (operatorBalance) {
|
|
633
|
+
return operatorBalance;
|
|
634
|
+
}
|
|
635
|
+
if (isRedactedConfigSecret(config.upstreamApiKey)) {
|
|
636
|
+
return {
|
|
637
|
+
rawAmount: null,
|
|
638
|
+
amountUsdMicros: null,
|
|
639
|
+
currency: null,
|
|
640
|
+
source: "unknown",
|
|
641
|
+
fetchedAt: Date.now(),
|
|
642
|
+
error: {
|
|
643
|
+
httpStatus: 0,
|
|
644
|
+
message: "seller balance endpoint unavailable"
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
453
648
|
return probeUpstreamBalance({
|
|
454
649
|
upstreamUrl: stringValue(config.upstreamUrl || upstreams?.upstreamUrl),
|
|
455
650
|
upstreamBalanceUrl: stringValue(config.upstreamBalanceUrl || upstreams?.upstreamBalanceUrl),
|
|
@@ -458,20 +653,31 @@ export class AdminUiState {
|
|
|
458
653
|
upstreamBalanceProbe: objectValue(config.upstreamBalanceProbe)
|
|
459
654
|
}, {
|
|
460
655
|
fetch: this.options.balanceFetch,
|
|
461
|
-
cache: this.balanceCache
|
|
656
|
+
cache: this.balanceCache,
|
|
657
|
+
timeoutMs
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async operatorBalanceSnapshot(profile, timeoutMs) {
|
|
661
|
+
return await this.fetchSellerAdminJson(profile, "/operator/admin/upstream-balance", { timeoutMs })
|
|
662
|
+
.then((response) => {
|
|
663
|
+
const responseObject = objectValue(response);
|
|
664
|
+
const balance = objectValue(responseObject?.balance) || responseObject;
|
|
665
|
+
return balance;
|
|
462
666
|
});
|
|
463
667
|
}
|
|
464
668
|
}
|
|
465
669
|
async function defaultFetchJson(url, init) {
|
|
466
670
|
// Step 13 v1.1: 3s timeout 避免坏 host / DNS 卡死整个 admin web.
|
|
467
|
-
const controller = new AbortController();
|
|
468
|
-
const timer = setTimeout(() => controller.abort(), 3000);
|
|
671
|
+
const controller = init?.signal ? undefined : new AbortController();
|
|
672
|
+
const timer = controller ? setTimeout(() => controller.abort(), 3000) : undefined;
|
|
469
673
|
let response;
|
|
470
674
|
try {
|
|
471
|
-
response = await fetch(url, { ...init, signal: controller.signal });
|
|
675
|
+
response = await fetch(url, controller ? { ...init, signal: controller.signal } : init);
|
|
472
676
|
}
|
|
473
677
|
finally {
|
|
474
|
-
|
|
678
|
+
if (timer) {
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
}
|
|
475
681
|
}
|
|
476
682
|
if (!response.ok) {
|
|
477
683
|
const text = await response.text();
|
|
@@ -480,7 +686,8 @@ async function defaultFetchJson(url, init) {
|
|
|
480
686
|
const text = await response.text();
|
|
481
687
|
return text ? JSON.parse(text) : {};
|
|
482
688
|
}
|
|
483
|
-
function baseSellerRow(entry, profile, dataSource = "registry", flyApp) {
|
|
689
|
+
function baseSellerRow(entry, profile, dataSource = "registry", flyApp, machineSpecs) {
|
|
690
|
+
const primaryRegion = entry.region || machineSpecs?.regions?.[0];
|
|
484
691
|
return {
|
|
485
692
|
id: entry.id,
|
|
486
693
|
name: entry.id,
|
|
@@ -492,15 +699,25 @@ function baseSellerRow(entry, profile, dataSource = "registry", flyApp) {
|
|
|
492
699
|
// Step 13 v1.1: 绿点 base 改 unknown, 真正值由 sellerSnapshot
|
|
493
700
|
// 拿 /manifest 200 决定. 老逻辑直接复用 entry.status 是错的.
|
|
494
701
|
nodeStatus: "unknown",
|
|
495
|
-
region:
|
|
702
|
+
region: primaryRegion,
|
|
496
703
|
upstreamDomain: hostName(entry.url) || "unknown",
|
|
497
704
|
upstreamStatus: "unknown",
|
|
498
705
|
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length,
|
|
499
706
|
specs: {
|
|
500
|
-
|
|
707
|
+
cpuKind: machineSpecs?.cpuKind,
|
|
708
|
+
cpuCores: machineSpecs?.cpuCores,
|
|
709
|
+
memoryMb: machineSpecs?.memoryMb,
|
|
710
|
+
memoryGb: machineSpecs?.memoryMb ? Number((machineSpecs.memoryMb / 1024).toFixed(2)) : undefined,
|
|
711
|
+
machines: machineSpecs?.machines,
|
|
712
|
+
runningMachines: machineSpecs?.runningMachines,
|
|
713
|
+
volumeGb: machineSpecs?.volumeGb,
|
|
714
|
+
region: primaryRegion,
|
|
715
|
+
regions: machineSpecs?.regions,
|
|
501
716
|
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length
|
|
502
717
|
},
|
|
503
718
|
dataSource,
|
|
719
|
+
publishStatus: dataSource === "both" ? "published" : dataSource === "fly" ? "unpublished" : "unknown",
|
|
720
|
+
detailStatus: "pending",
|
|
504
721
|
flyApp: flyApp ? {
|
|
505
722
|
name: flyApp.name,
|
|
506
723
|
status: flyApp.status,
|
|
@@ -509,19 +726,61 @@ function baseSellerRow(entry, profile, dataSource = "registry", flyApp) {
|
|
|
509
726
|
} : undefined
|
|
510
727
|
};
|
|
511
728
|
}
|
|
512
|
-
function
|
|
729
|
+
function isSellerFlyAppName(name) {
|
|
730
|
+
return Boolean(name && (name.startsWith("tbs-") || name.startsWith("tb-seller-")) && name !== "tb-seller");
|
|
731
|
+
}
|
|
732
|
+
function sellerEntryFromFlyApp(app) {
|
|
733
|
+
return {
|
|
734
|
+
id: app.name,
|
|
735
|
+
name: app.name,
|
|
736
|
+
app: app.name,
|
|
737
|
+
url: `https://${app.name}.fly.dev`,
|
|
738
|
+
supportedProtocols: [],
|
|
739
|
+
paymentMethods: [],
|
|
740
|
+
models: []
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function findFlyAppForEntry(flyByName, entry) {
|
|
744
|
+
for (const key of [entry.app, entry.id, entry.name]) {
|
|
745
|
+
const normalized = stringValue(key);
|
|
746
|
+
if (!normalized) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const match = flyByName.get(normalized);
|
|
750
|
+
if (match) {
|
|
751
|
+
return match;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
function sellerEntryFromRow(row) {
|
|
757
|
+
return {
|
|
758
|
+
id: row.id,
|
|
759
|
+
name: row.name,
|
|
760
|
+
app: row.app || row.flyApp?.name || row.id,
|
|
761
|
+
url: row.url,
|
|
762
|
+
status: row.registryStatus,
|
|
763
|
+
region: row.region,
|
|
764
|
+
modelsCount: row.modelsCount,
|
|
765
|
+
supportedProtocols: [],
|
|
766
|
+
paymentMethods: [],
|
|
767
|
+
models: []
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function mergeSellerRow(base, entry, status, service, upstreams, config, balance, manifestOk = false) {
|
|
513
771
|
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
514
772
|
const capacity = status?.capacity || service?.capacity || {};
|
|
515
773
|
const upstreamUrl = stringValue(config?.upstreamUrl || normalizedUpstreams?.upstreamUrl || service?.upstreamUrl) || entry.url;
|
|
516
774
|
const error = firstError(status, service, upstreams);
|
|
517
775
|
return {
|
|
518
776
|
...base,
|
|
519
|
-
nodeStatus: error ? "unknown" : nodeStatus(status?.status || entry.status),
|
|
777
|
+
nodeStatus: error ? (manifestOk ? "active" : "unknown") : (manifestOk ? "active" : nodeStatus(status?.status || entry.status)),
|
|
520
778
|
upstreamDomain: hostName(upstreamUrl) || base.upstreamDomain,
|
|
521
779
|
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status),
|
|
522
780
|
discountRatio: numberValue(config?.discountRatio ?? normalizedUpstreams?.discountRatio),
|
|
523
781
|
capacityUsed: numberValue(capacity.activeConnections),
|
|
524
782
|
capacityLimit: numberValue(capacity.maxConnections),
|
|
783
|
+
...runtimeUsageFields(status?.runtime, base),
|
|
525
784
|
ttftMs: numberValue(status?.latency?.ttftMs),
|
|
526
785
|
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs),
|
|
527
786
|
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs),
|
|
@@ -537,12 +796,46 @@ function mergeSellerRow(base, entry, status, service, upstreams, config, balance
|
|
|
537
796
|
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount),
|
|
538
797
|
specs: {
|
|
539
798
|
...base.specs,
|
|
540
|
-
region: entry.region,
|
|
799
|
+
region: entry.region || base.specs?.region,
|
|
541
800
|
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount)
|
|
542
801
|
},
|
|
543
802
|
error
|
|
544
803
|
};
|
|
545
804
|
}
|
|
805
|
+
function runtimeUsageFields(runtime, fallback) {
|
|
806
|
+
return {
|
|
807
|
+
resourceCpuPercent: numberValue(runtime?.cpuPercent) ?? fallback.resourceCpuPercent,
|
|
808
|
+
resourceMemoryPercent: numberValue(runtime?.memoryPercent) ?? fallback.resourceMemoryPercent,
|
|
809
|
+
resourceMemoryRssMb: numberValue(runtime?.memoryRssMb) ?? fallback.resourceMemoryRssMb,
|
|
810
|
+
resourceMemoryLimitMb: numberValue(runtime?.memoryLimitMb) ?? fallback.resourceMemoryLimitMb
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function balanceFields(balance, fallback) {
|
|
814
|
+
if (!balance) {
|
|
815
|
+
return {};
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
upstreamBalanceUsdMicros: Number.isFinite(balance.amountUsdMicros ?? NaN) ? balance.amountUsdMicros : undefined,
|
|
819
|
+
upstreamBalanceCurrency: typeof balance.currency === "string" ? balance.currency : undefined,
|
|
820
|
+
upstreamBalanceSource: balance.source,
|
|
821
|
+
upstreamBalanceFetchedAt: new Date(balance.fetchedAt).toISOString(),
|
|
822
|
+
upstreamBalanceError: balance.error?.message,
|
|
823
|
+
upstreamRechargeUrl: fallback.upstreamRechargeUrl
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function unavailableBalanceSnapshot(message) {
|
|
827
|
+
return {
|
|
828
|
+
rawAmount: null,
|
|
829
|
+
amountUsdMicros: null,
|
|
830
|
+
currency: null,
|
|
831
|
+
source: "unknown",
|
|
832
|
+
fetchedAt: Date.now(),
|
|
833
|
+
error: {
|
|
834
|
+
httpStatus: 0,
|
|
835
|
+
message
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
546
839
|
function modelRows(upstreams, config, status) {
|
|
547
840
|
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
548
841
|
const aliases = config.modelAliases || normalizedUpstreams.modelAliases || {};
|
|
@@ -600,6 +893,9 @@ export function maskApiKey(value) {
|
|
|
600
893
|
if (!normalized) {
|
|
601
894
|
return undefined;
|
|
602
895
|
}
|
|
896
|
+
if (isRedactedConfigSecret(normalized)) {
|
|
897
|
+
return "configured";
|
|
898
|
+
}
|
|
603
899
|
const tail = normalized.replace(/\s+/g, "").slice(-4);
|
|
604
900
|
return tail ? `**** **** **** ${tail}` : "****";
|
|
605
901
|
}
|
|
@@ -640,6 +936,9 @@ function objectValue(value) {
|
|
|
640
936
|
? value
|
|
641
937
|
: undefined;
|
|
642
938
|
}
|
|
939
|
+
function isRedactedConfigSecret(value) {
|
|
940
|
+
return value === "[redacted]";
|
|
941
|
+
}
|
|
643
942
|
function priceString(value) {
|
|
644
943
|
const parsed = numberValue(value);
|
|
645
944
|
if (parsed === undefined) {
|
|
@@ -663,4 +962,49 @@ function firstError(...values) {
|
|
|
663
962
|
const hit = values.find((value) => value?.error);
|
|
664
963
|
return hit?.error;
|
|
665
964
|
}
|
|
965
|
+
function normalizeRegistryDocument(value, label) {
|
|
966
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.sellers)) {
|
|
967
|
+
throw new Error(`${label} did not include sellers`);
|
|
968
|
+
}
|
|
969
|
+
const document = value;
|
|
970
|
+
return {
|
|
971
|
+
version: numberValue(document.version) ?? 0,
|
|
972
|
+
updatedAt: stringValue(document.updatedAt),
|
|
973
|
+
purpose: stringValue(document.purpose),
|
|
974
|
+
defaultSeller: stringValue(document.defaultSeller),
|
|
975
|
+
notes: Array.isArray(document.notes) ? document.notes.map((note) => stringValue(note)).filter((note) => Boolean(note)) : undefined,
|
|
976
|
+
sellers: document.sellers
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function mergeRegistryDocuments(publicDoc, managedDoc) {
|
|
980
|
+
const sellers = publicDoc.sellers.map((seller) => ({ ...seller }));
|
|
981
|
+
for (const managed of managedDoc.sellers) {
|
|
982
|
+
const index = sellers.findIndex((seller) => sameSellerEntry(seller, managed));
|
|
983
|
+
if (index >= 0) {
|
|
984
|
+
sellers[index] = { ...sellers[index], ...managed };
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
sellers.push(managed);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
version: publicDoc.version || managedDoc.version,
|
|
992
|
+
updatedAt: publicDoc.updatedAt || managedDoc.updatedAt,
|
|
993
|
+
purpose: publicDoc.purpose || managedDoc.purpose,
|
|
994
|
+
defaultSeller: publicDoc.defaultSeller || managedDoc.defaultSeller,
|
|
995
|
+
notes: publicDoc.notes || managedDoc.notes,
|
|
996
|
+
sellers
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function sameSellerEntry(a, b) {
|
|
1000
|
+
const aKeys = new Set([a.id, a.app, a.name].map((value) => stringValue(value)).filter((value) => Boolean(value)));
|
|
1001
|
+
return [b.id, b.app, b.name].some((value) => {
|
|
1002
|
+
const normalized = stringValue(value);
|
|
1003
|
+
return normalized ? aKeys.has(normalized) : false;
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function isUnavailablePlatformSellersEndpoint(err) {
|
|
1007
|
+
const message = err instanceof Error ? err.message : String(err || "");
|
|
1008
|
+
return /HTTP Error (401|403|404)|Cannot GET \/platform\/sellers|vendor_auth|not found/i.test(message);
|
|
1009
|
+
}
|
|
666
1010
|
//# sourceMappingURL=ui-state.js.map
|