@tokenbuddy/tb-admin 1.0.36 → 1.0.37
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.js +92 -19
- package/dist/src/config.d.ts +7 -1
- package/dist/src/config.js +16 -4
- package/dist/src/display-format.js +6 -14
- package/dist/src/init-command.d.ts +50 -0
- package/dist/src/init-command.js +347 -0
- package/dist/src/providers/fly-io.d.ts +3 -0
- package/dist/src/providers/fly-io.js +137 -0
- package/dist/src/providers/provider-definition.d.ts +38 -0
- package/dist/src/providers/provider-definition.js +2 -0
- package/dist/src/seller.d.ts +2 -0
- package/dist/src/seller.js +30 -13
- package/dist/src/server-cmd.d.ts +1 -0
- package/dist/src/server-cmd.js +9 -2
- package/dist/src/ui-actions.d.ts +3 -0
- package/dist/src/ui-actions.js +199 -27
- package/dist/src/ui-command.js +3 -2
- package/dist/src/ui-state.d.ts +1 -3
- package/dist/src/ui-state.js +4 -8
- package/dist/src/ui-static.js +43 -15
- package/dist/src/workdir.d.ts +21 -0
- package/dist/src/workdir.js +50 -0
- package/package.json +8 -2
- package/templates/providers/fly.io/admin.toml.example +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
- package/templates/providers/fly.io/env/deploy.env.example +12 -0
- package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
- package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
- package/templates/providers/fly.io/provider.toml.example +10 -0
- package/dist/src/bootstrap-registry.d.ts.map +0 -1
- package/dist/src/bootstrap-registry.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js.map +0 -1
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/display-format.d.ts.map +0 -1
- package/dist/src/display-format.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/provider.d.ts.map +0 -1
- package/dist/src/provider.js.map +0 -1
- package/dist/src/seller.d.ts.map +0 -1
- package/dist/src/seller.js.map +0 -1
- package/dist/src/server-cmd.d.ts.map +0 -1
- package/dist/src/server-cmd.js.map +0 -1
- package/dist/src/ui-actions.d.ts.map +0 -1
- package/dist/src/ui-actions.js.map +0 -1
- package/dist/src/ui-command.d.ts.map +0 -1
- package/dist/src/ui-command.js.map +0 -1
- package/dist/src/ui-server.d.ts.map +0 -1
- package/dist/src/ui-server.js.map +0 -1
- package/dist/src/ui-state.d.ts.map +0 -1
- package/dist/src/ui-state.js.map +0 -1
- package/dist/src/ui-static.d.ts.map +0 -1
- package/dist/src/ui-static.js.map +0 -1
- package/dist/src/upstream-balance-probe.d.ts.map +0 -1
- package/dist/src/upstream-balance-probe.js.map +0 -1
- package/dist/src/vendor-client.d.ts.map +0 -1
- package/dist/src/vendor-client.js.map +0 -1
- package/dist/src/vendor-commands.d.ts.map +0 -1
- package/dist/src/vendor-commands.js.map +0 -1
- package/src/bootstrap-registry.ts +0 -90
- package/src/cli.ts +0 -1614
- package/src/client.ts +0 -179
- package/src/config.ts +0 -194
- package/src/display-format.ts +0 -411
- package/src/index.ts +0 -11
- package/src/provider.ts +0 -150
- package/src/seller.ts +0 -538
- package/src/server-cmd.ts +0 -362
- package/src/ui-actions.ts +0 -1040
- package/src/ui-command.ts +0 -44
- package/src/ui-server.ts +0 -353
- package/src/ui-state.ts +0 -1318
- package/src/ui-static.ts +0 -673
- package/src/upstream-balance-probe.ts +0 -13
- package/src/vendor-client.ts +0 -23
- package/src/vendor-commands.ts +0 -65
- package/tests/admin.test.ts +0 -2162
- package/tests/seller.test.ts +0 -388
- package/tests/ui-state-fleet.test.ts +0 -526
- package/tests/ui-static-row.test.ts +0 -467
- package/tests/vendor-cli.test.ts +0 -241
- package/tsconfig.json +0 -8
package/dist/src/ui-state.js
CHANGED
|
@@ -197,7 +197,6 @@ export class AdminUiState {
|
|
|
197
197
|
const row = baseSellerRow(stubEntry, undefined, "fly", app, specsByApp.get(app.name));
|
|
198
198
|
row.publishStatus = "unpublished";
|
|
199
199
|
row.detailStatus = "pending";
|
|
200
|
-
row.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
201
200
|
rows.push(row);
|
|
202
201
|
}
|
|
203
202
|
return rows;
|
|
@@ -211,7 +210,7 @@ export class AdminUiState {
|
|
|
211
210
|
*/
|
|
212
211
|
async fetchFlyApps() {
|
|
213
212
|
if (this.options.flyApps) {
|
|
214
|
-
return (await this.options.flyApps()).filter((app) =>
|
|
213
|
+
return (await this.options.flyApps()).filter((app) => isVisibleFlyInventoryApp(app.name));
|
|
215
214
|
}
|
|
216
215
|
// 默认路径: 走 SellerCommandRunner.ls(true) 真 spawn flyctl.
|
|
217
216
|
// 避免在 ui-state.ts 里 import 整个 seller module 引入循环依赖,
|
|
@@ -229,7 +228,7 @@ export class AdminUiState {
|
|
|
229
228
|
const runner = new mod.SellerCommandRunner(this.configManager);
|
|
230
229
|
const result = await runner.ls(true);
|
|
231
230
|
if (result && typeof result === "object" && "apps" in result) {
|
|
232
|
-
return result.apps.filter((app) =>
|
|
231
|
+
return result.apps.filter((app) => isVisibleFlyInventoryApp(app.name));
|
|
233
232
|
}
|
|
234
233
|
return [];
|
|
235
234
|
}
|
|
@@ -499,9 +498,6 @@ export class AdminUiState {
|
|
|
499
498
|
baseRow.alertReason = "registry 收录了但 fly app 失踪 — 严重事故, 立即下线";
|
|
500
499
|
baseRow.removeHint = "立即下线 (registry-only)";
|
|
501
500
|
}
|
|
502
|
-
else if (dataSource === "fly") {
|
|
503
|
-
baseRow.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
504
|
-
}
|
|
505
501
|
if (dataSource === "registry") {
|
|
506
502
|
return {
|
|
507
503
|
row: {
|
|
@@ -726,8 +722,8 @@ function baseSellerRow(entry, profile, dataSource = "registry", flyApp, machineS
|
|
|
726
722
|
} : undefined
|
|
727
723
|
};
|
|
728
724
|
}
|
|
729
|
-
function
|
|
730
|
-
return Boolean(name &&
|
|
725
|
+
function isVisibleFlyInventoryApp(name) {
|
|
726
|
+
return Boolean(name && name !== "tb-registry");
|
|
731
727
|
}
|
|
732
728
|
function sellerEntryFromFlyApp(app) {
|
|
733
729
|
return {
|
package/dist/src/ui-static.js
CHANGED
|
@@ -56,6 +56,13 @@ export function adminUiHtml() {
|
|
|
56
56
|
.panel,.bootstrap-card{border:1px solid var(--hairline);background:var(--panel);border-radius:12px;box-shadow:var(--shadow);overflow:hidden}
|
|
57
57
|
.panel-head{min-height:56px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--hairline);background:var(--panel)}
|
|
58
58
|
.title{margin:0;color:var(--ink);font-size:20px;line-height:28px;font-weight:750}
|
|
59
|
+
.fleet-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}
|
|
60
|
+
.fleet-toggle{min-height:38px;border:1px solid var(--hairline-strong);border-radius:8px;background:var(--panel-subtle);color:var(--muted);padding:0 10px;display:inline-flex;align-items:center;gap:8px;font-size:12px;font-weight:800}
|
|
61
|
+
.fleet-toggle input{position:absolute;opacity:0;pointer-events:none}
|
|
62
|
+
.fleet-toggle .switch-track{width:34px;height:20px;border-radius:999px;background:#d8d0ea;box-shadow:inset 0 0 0 1px var(--hairline-strong);position:relative;flex:0 0 auto}
|
|
63
|
+
.fleet-toggle .switch-track:after{content:"";position:absolute;left:3px;top:3px;width:14px;height:14px;border-radius:999px;background:#fff;box-shadow:0 1px 3px rgba(60,41,112,.2);transition:transform .18s ease}
|
|
64
|
+
.fleet-toggle input:checked + .switch-track{background:var(--primary)}
|
|
65
|
+
.fleet-toggle input:checked + .switch-track:after{transform:translateX(14px)}
|
|
59
66
|
/* ---- Buttons ------------------------------------------------------ */
|
|
60
67
|
.btn{border:1px solid var(--hairline-strong);border-radius:8px;min-height:38px;padding:0 12px;background:#fff;color:var(--ink);font-size:13px;font-weight:750}
|
|
61
68
|
.btn:hover{border-color:var(--primary);color:var(--primary)}
|
|
@@ -97,8 +104,6 @@ export function adminUiHtml() {
|
|
|
97
104
|
.remove-hint-btn{margin-top:6px;background:var(--danger);color:#fff;border:0;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;display:inline-block}
|
|
98
105
|
.remove-hint-btn:hover{background:#d63d5a}
|
|
99
106
|
.remove-hint-btn::before{content:"! ";margin-right:2px}
|
|
100
|
-
.publish-hint-btn{margin-top:6px;background:#fff;color:var(--primary);border:1px solid var(--hairline-strong);border-radius:6px;padding:4px 10px;font-size:11px;font-weight:800;display:inline-block}
|
|
101
|
-
.publish-hint-btn:hover{background:#f5f3ff}
|
|
102
107
|
/* Status dot — five spec tones (green/amber/red/blue/gray) */
|
|
103
108
|
.app-dot{width:10px;height:10px;border-radius:999px;background:#c8ced8;box-shadow:0 0 0 4px #edf1f8}
|
|
104
109
|
.app-dot.tone-green{background:var(--success);box-shadow:0 0 0 4px rgba(16,185,129,.18)}
|
|
@@ -222,7 +227,11 @@ export function adminUiHtml() {
|
|
|
222
227
|
.progress-title .spinner{width:14px;height:14px;border-width:2px}
|
|
223
228
|
.progress-meta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
224
229
|
.progress-toggle{color:var(--primary);font-size:var(--label-fs);font-weight:var(--label-weight);text-transform:uppercase;letter-spacing:var(--label-spacing)}
|
|
225
|
-
.progress-log{
|
|
230
|
+
.progress-log-wrap{position:relative;margin-top:4px}
|
|
231
|
+
.progress-log{margin:4px 0 0;max-height:150px;overflow:auto;border:1px solid var(--hairline);border-radius:8px;background:var(--ink);color:#f3f0ff;padding:36px 9px 9px;font-size:11px;white-space:pre-wrap;font-family:var(--font-mono)}
|
|
232
|
+
.progress-copy{position:absolute;right:7px;top:7px;min-height:28px;border:1px solid rgba(255,255,255,.18);border-radius:6px;background:rgba(255,255,255,.08);color:#f3f0ff;padding:0 8px;display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:800}
|
|
233
|
+
.progress-copy:hover{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.28);color:#fff}
|
|
234
|
+
.progress-copy svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.1;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
226
235
|
.spinner{width:19px;height:19px;border:2px solid rgba(124,61,240,.18);border-top-color:var(--primary);border-radius:999px;animation:spin .75s linear infinite}
|
|
227
236
|
.inline-spinner{width:13px;height:13px;border:2px solid rgba(124,61,240,.18);border-top-color:var(--primary);border-radius:999px;animation:spin .75s linear infinite;display:inline-block;vertical-align:middle}
|
|
228
237
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
@@ -255,7 +264,14 @@ export function adminUiHtml() {
|
|
|
255
264
|
<div>
|
|
256
265
|
<h1 class="title">Seller fleet</h1>
|
|
257
266
|
</div>
|
|
258
|
-
<
|
|
267
|
+
<div class="fleet-controls">
|
|
268
|
+
<label class="fleet-toggle" title="Hide Fly apps that have no Machines instances">
|
|
269
|
+
<input id="hideNoInstanceApps" type="checkbox" checked>
|
|
270
|
+
<span class="switch-track" aria-hidden="true"></span>
|
|
271
|
+
<span>Hide no-instance apps</span>
|
|
272
|
+
</label>
|
|
273
|
+
<button id="createSeller" class="btn primary">Create Seller</button>
|
|
274
|
+
</div>
|
|
259
275
|
</div>
|
|
260
276
|
<div class="app-list">
|
|
261
277
|
<div class="app-table-head" role="row">
|
|
@@ -369,7 +385,7 @@ async function api(path, options={}){
|
|
|
369
385
|
function uiErrorMessage(err){ const message = err instanceof Error ? err.message : String(err || ""); if (/failed to fetch|networkerror|load failed|fetch failed/i.test(message)) return "Admin UI connection lost. Restart tb-admin ui and reload this page."; if (/operator_auth_required/i.test(message)) return "Admin profile authentication failed. Check the configured operator token."; if (/HTTP Error 401/i.test(message)) return "Authentication failed while loading admin data. Check the local admin profile."; return message || "Request failed."; }
|
|
370
386
|
const sellerStatusRefreshIntervalMs = 30000;
|
|
371
387
|
const sellerDetailConcurrency = 2;
|
|
372
|
-
let currentDetail = null; let sellerRowsCache = []; let editing = false; let deleteReady = false; let sellerInventoryInFlight = false; let sellerDetailQueue = []; let sellerDetailQueueKeys = new Set(); let sellerDetailInFlight = new Set(); let sellerDetailTimers = new Map(); let sellerClockTimer = null; let sellerRefreshLoaded = false; let sellerRefreshStage = "Starting"; let sellerRefreshError = ""; let createJobTimer = null; let currentCreateJob = null; let expandedProgressSteps = new Set(); let createAppSuffix = "";
|
|
388
|
+
let currentDetail = null; let sellerRowsCache = []; let editing = false; let deleteReady = false; let hideNoInstanceApps = true; let sellerInventoryInFlight = false; let sellerDetailQueue = []; let sellerDetailQueueKeys = new Set(); let sellerDetailInFlight = new Set(); let sellerDetailTimers = new Map(); let sellerClockTimer = null; let sellerRefreshLoaded = false; let sellerRefreshStage = "Starting"; let sellerRefreshError = ""; let createJobTimer = null; let currentCreateJob = null; let expandedProgressSteps = new Set(); let copiedProgressStepId = ""; let createAppSuffix = "";
|
|
373
389
|
const esc = value => String(value ?? "").replace(/[&<>"']/g, c => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c]));
|
|
374
390
|
const missing = value => '<span class="muted-value">'+esc(value)+'</span>';
|
|
375
391
|
const dash = () => '<span class="muted-value">'+window.__tbFmt.UNKNOWN_VALUE+'</span>';
|
|
@@ -405,8 +421,10 @@ async function loadSellers(options={}){
|
|
|
405
421
|
updateSellerRefreshMeta();
|
|
406
422
|
}
|
|
407
423
|
}
|
|
408
|
-
function statusRefreshRow(row){ return { id:row.id, name:row.name, app:row.app, url:row.url, registryStatus:row.registryStatus, nodeStatus:row.nodeStatus, region:row.region, upstreamDomain:row.upstreamDomain, upstreamStatus:row.upstreamStatus, upstreamBalanceUsdMicros:row.upstreamBalanceUsdMicros, upstreamBalanceCurrency:row.upstreamBalanceCurrency, upstreamBalanceSource:row.upstreamBalanceSource, upstreamBalanceFetchedAt:row.upstreamBalanceFetchedAt, upstreamBalanceError:row.upstreamBalanceError, upstreamRechargeUrl:row.upstreamRechargeUrl, discountRatio:row.discountRatio, capacityUsed:row.capacityUsed, capacityLimit:row.capacityLimit, resourceCpuPercent:row.resourceCpuPercent, resourceMemoryPercent:row.resourceMemoryPercent, resourceMemoryRssMb:row.resourceMemoryRssMb, resourceMemoryLimitMb:row.resourceMemoryLimitMb, ttftMs:row.ttftMs, avgInferenceMs:row.avgInferenceMs, lastInferenceMs:row.lastInferenceMs, avgTokensPerSecond:row.avgTokensPerSecond, lastTokensPerSecond:row.lastTokensPerSecond, latencySamples:row.latencySamples, lastSwitchAt:row.lastSwitchAt, modelsCount:row.modelsCount, specs:row.specs, error:row.error, dataSource:row.dataSource, registryAlert:row.registryAlert, alertReason:row.alertReason,
|
|
409
|
-
function
|
|
424
|
+
function statusRefreshRow(row){ return { id:row.id, name:row.name, app:row.app, url:row.url, registryStatus:row.registryStatus, nodeStatus:row.nodeStatus, region:row.region, upstreamDomain:row.upstreamDomain, upstreamStatus:row.upstreamStatus, upstreamBalanceUsdMicros:row.upstreamBalanceUsdMicros, upstreamBalanceCurrency:row.upstreamBalanceCurrency, upstreamBalanceSource:row.upstreamBalanceSource, upstreamBalanceFetchedAt:row.upstreamBalanceFetchedAt, upstreamBalanceError:row.upstreamBalanceError, upstreamRechargeUrl:row.upstreamRechargeUrl, discountRatio:row.discountRatio, capacityUsed:row.capacityUsed, capacityLimit:row.capacityLimit, resourceCpuPercent:row.resourceCpuPercent, resourceMemoryPercent:row.resourceMemoryPercent, resourceMemoryRssMb:row.resourceMemoryRssMb, resourceMemoryLimitMb:row.resourceMemoryLimitMb, ttftMs:row.ttftMs, avgInferenceMs:row.avgInferenceMs, lastInferenceMs:row.lastInferenceMs, avgTokensPerSecond:row.avgTokensPerSecond, lastTokensPerSecond:row.lastTokensPerSecond, latencySamples:row.latencySamples, lastSwitchAt:row.lastSwitchAt, modelsCount:row.modelsCount, specs:row.specs, error:row.error, dataSource:row.dataSource, registryAlert:row.registryAlert, alertReason:row.alertReason, removeHint:row.removeHint, flyApp:row.flyApp, publishStatus:row.publishStatus, detailStatus:row.detailStatus, detailUpdatedAt:row.detailUpdatedAt, detailNextRefreshAt:row.detailNextRefreshAt }; }
|
|
425
|
+
function hasNoFlyMachineInstance(row){ const machines = Number(row.specs?.machines); return Number.isFinite(machines) && machines === 0; }
|
|
426
|
+
function visibleSellerRows(rows){ return hideNoInstanceApps ? rows.filter(row => row.dataSource !== "fly" || !hasNoFlyMachineInstance(row)) : rows; }
|
|
427
|
+
function renderSellerRows(rows){ sellerRowsCache = rows; const visibleRows = visibleSellerRows(rows); document.getElementById("sellerRows").innerHTML = visibleRows.map(row => sellerRow(row)).join("") || '<div class="status-line">No apps match the current display filter.</div>'; document.querySelectorAll("[data-detail]").forEach(btn => btn.onclick = () => openDetail(btn.dataset.detail)); }
|
|
410
428
|
function startSellerRefresh(){ loadSellers({ initial:true }); sellerClockTimer = setInterval(() => { updateSellerRefreshMeta(); updateSellerRowRefreshCountdowns(); }, 1000); window.addEventListener("beforeunload", () => { resetSellerDetailQueue(); clearInterval(sellerClockTimer); }); }
|
|
411
429
|
function sellerRowKey(row){ return String(row.id || row.app || row.url || ""); }
|
|
412
430
|
function resetSellerDetailQueue(){ sellerDetailQueue = []; sellerDetailQueueKeys.clear(); sellerDetailInFlight.clear(); sellerDetailTimers.forEach(timer => clearTimeout(timer)); sellerDetailTimers.clear(); }
|
|
@@ -554,7 +572,7 @@ function sellerRow(row){
|
|
|
554
572
|
const publishExtras = publishRelation === "registry_only" ? '<strong class="registry-incident" title="'+esc(statusTip)+'">严重事故</strong>' +
|
|
555
573
|
(row.alertReason ? '<span class="alert-reason">'+esc(row.alertReason)+'</span>' : '') +
|
|
556
574
|
(row.removeHint ? '<span class="remove-hint-btn" data-action="remove" data-seller-id="'+esc(row.id)+'" title="'+esc(row.removeHint)+'">'+esc(row.removeHint)+'</span>' : '')
|
|
557
|
-
:
|
|
575
|
+
: '';
|
|
558
576
|
const publishCell = '<span class="field-cell"><span class="metric-label">Pub</span>'+dsChip+publishExtras+'</span>';
|
|
559
577
|
const regionCell = '<span class="field-cell"><span class="metric-label">Region</span><strong>'+esc(regionText)+'</strong></span>';
|
|
560
578
|
const modelsCell = '<span class="field-cell"><span class="metric-label">Models</span><strong>'+esc(modelsText)+'</strong></span>';
|
|
@@ -612,15 +630,18 @@ function renderDetail(){
|
|
|
612
630
|
const fields = [["registryStatus", registryStatusDisplay(c.registryStatus || d.row.registryStatus)],["region", c.region],["upstreamUrl", c.upstreamUrl],["upstreamApiKey", c.upstreamApiKeyMasked],["upstreamStatus", fmt.normalizeStatusLabel(c.upstreamStatus)],["ttftMs", fmt.formatDuration(d.row.ttftMs)],["avgTokensPerSecond", fmt.formatSpeed(d.row.avgTokensPerSecond)],["lastTokensPerSecond", fmt.formatSpeed(d.row.lastTokensPerSecond)],["lastInferenceMs", fmt.formatDuration(d.row.lastInferenceMs)],["latencySamples", d.row.latencySamples === undefined ? "—" : esc(fmt.formatCount(d.row.latencySamples))],["upstreamBalance", c.upstreamBalance],["upstreamBalanceSource", c.upstreamBalanceSource],["upstreamBalanceFetchedAt", c.upstreamBalanceFetchedAt ? fmt.formatTimeFull(c.upstreamBalanceFetchedAt) : "—"],["upstreamBalanceError", c.upstreamBalanceError],["upstreamBalanceProbeTemplate", c.upstreamBalanceProbeTemplate],["upstreamBalanceProbeUrl", c.upstreamBalanceProbeUrl],["upstreamBalanceProbeUserId", c.upstreamBalanceProbeUserId],["upstreamBalanceProbeRechargeUrl", c.upstreamBalanceProbeRechargeUrl],["markupRatio", c.markupRatio],["discountRatio", c.discountRatio],["maxConnections", c.maxConnections],["maxQueueDepth", c.maxQueueDepth]];
|
|
613
631
|
document.getElementById("configFields").innerHTML = detailFieldsHtml(fields);
|
|
614
632
|
const billingOptions = Array.from(new Set(d.models.map(m => m.billingModel).filter(Boolean)));
|
|
615
|
-
document.getElementById("modelsTable").innerHTML = '<table><thead><tr><th>Enable</th><th>Upstream model</th><th>Billing model</th><th>Input price</th><th>Output price</th><th>TTFT</th><th>AVG speed</th><th>Samples</th></tr></thead><tbody>'+d.models.map(m => '<tr><td>'+modelEnableHtml(m)+'</td><td>'+esc(m.upstreamModel)+'</td><td>'+(editing ? selectHtml(m.upstreamModel, m.billingModel, billingOptions) : esc(m.billingModel))+'</td><td>'+esc(m.inputPrice || "—")+'</td><td>'+esc(m.outputPrice || "—")+'</td><td>'+esc(m.ttftMs === undefined ? "—" : fmt.formatDuration(m.ttftMs))+'</td><td>'+esc(m.avgTokensPerSecond === undefined ? "—" : fmt.formatSpeed(m.avgTokensPerSecond))+'</td><td>'+esc(m.latencySamples === undefined ? "—" : fmt.formatCount(m.latencySamples))+'</td></tr>').join("")+'</tbody></table>';
|
|
633
|
+
document.getElementById("modelsTable").innerHTML = '<table><thead><tr><th><input id="modelBulkToggle" class="model-toggle" type="checkbox" aria-label="Toggle all models" '+(editing ? "" : "disabled")+'> Enable</th><th>Upstream model</th><th>Billing model</th><th>Input price</th><th>Output price</th><th>TTFT</th><th>AVG speed</th><th>Samples</th></tr></thead><tbody>'+d.models.map(m => '<tr><td>'+modelEnableHtml(m)+'</td><td>'+esc(m.upstreamModel)+'</td><td>'+(editing ? selectHtml(m.upstreamModel, m.billingModel, billingOptions) : esc(m.billingModel))+'</td><td>'+esc(m.inputPrice || "—")+'</td><td>'+esc(m.outputPrice || "—")+'</td><td>'+esc(m.ttftMs === undefined ? "—" : fmt.formatDuration(m.ttftMs))+'</td><td>'+esc(m.avgTokensPerSecond === undefined ? "—" : fmt.formatSpeed(m.avgTokensPerSecond))+'</td><td>'+esc(m.latencySamples === undefined ? "—" : fmt.formatCount(m.latencySamples))+'</td></tr>').join("")+'</tbody></table>';
|
|
634
|
+
setupModelBulkToggle();
|
|
616
635
|
}
|
|
617
636
|
function showDetailStatus(message, loading){ const el = document.getElementById("detailStatus"); if (loading){ el.innerHTML = '<span class="spinner" aria-hidden="true"></span>'; el.setAttribute("role", "status"); el.setAttribute("aria-label", message || "Loading"); } else { el.textContent = message || ""; el.removeAttribute("role"); el.removeAttribute("aria-label"); } el.classList.toggle("hidden", !message && !loading); el.classList.toggle("loading", Boolean(loading)); }
|
|
618
637
|
function metricCell(value){ return value === undefined || value === null || value === "" ? missing("—") : esc(value); }
|
|
619
638
|
function detailFieldsHtml(fields){ return fields.filter(([key,value]) => !isMissingDetailField(key,value)).map(([key,value]) => { const editable = editing && ["upstreamUrl","upstreamApiKey","upstreamBalanceProbeTemplate","upstreamBalanceProbeUrl","upstreamBalanceProbeRechargeUrl","upstreamBalanceProbeUserId","markupRatio","discountRatio","maxConnections","maxQueueDepth"].includes(key); return fieldHtml(key, value, editable); }).join(""); }
|
|
620
639
|
function isMissingDetailField(key,value){ const fmt = window.__tbFmt; return value === undefined || value === null || value === "" || value === fmt.UNKNOWN_VALUE || (key === "registryStatus" && value === "unknown"); }
|
|
621
|
-
function fieldHtml(key,value, editable, options={}){ const fieldId = "field-" + String(key).replace(/[^a-z0-9_-]/gi, "-") + "-" + Math.random().toString(36).slice(2); const label = options.label || key; const labelText = label + (options.required ? ' <span class="required-star">*</span>' : ''); const display = value === undefined || value === null ? "" : value; const className = "field" + (options.full ? " full" : ""); const help = options.help ? '<p class="field-help">'+esc(options.help)+'</p>' : ""; const hiddenInput = '<input type="hidden" data-field="'+esc(key)+'" value="'+esc(display)+'">'; if (options.hidden) return hiddenInput; if (!editable || options.readonly) return '<div class="'+className+'"><label>'+labelText+'</label><div class="readonly-value" title="'+esc(display)+'" data-readonly-field="'+esc(key)+'">'+esc(display || window.__tbFmt.UNKNOWN_VALUE)+'</div>'+(options.submit ? hiddenInput : "")+help+'</div>'; if (key === "upstreamBalanceProbeTemplate") return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><select id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'">'+balanceProbeTemplates.map(t => '<option value="'+esc(t)+'" '+(t === display ? "selected" : "")+'>'+esc(t)+'</option>').join("")+'</select>'+help+'</div>'; const type = options.type || (String(key).toLowerCase().includes("key") ? "password" : "text"); const attrs = options.type === "number" ? ' type="number" step="any"' : ' type="'+esc(type)+'"'; const placeholder = options.placeholder ? ' placeholder="'+esc(options.placeholder)+'"' : ""; return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><input'+attrs+placeholder+' id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'" value="'+esc(display)+'">'+help+'</div>'; }
|
|
640
|
+
function fieldHtml(key,value, editable, options={}){ const fieldId = "field-" + String(key).replace(/[^a-z0-9_-]/gi, "-") + "-" + Math.random().toString(36).slice(2); const label = options.label || key; const labelText = label + (options.required ? ' <span class="required-star">*</span>' : ''); const display = value === undefined || value === null ? "" : value; const className = "field" + (options.full ? " full" : ""); const help = options.help ? '<p class="field-help">'+esc(options.help)+'</p>' : ""; const hiddenInput = '<input type="hidden" data-field="'+esc(key)+'" value="'+esc(display)+'">'; if (options.hidden) return hiddenInput; if (!editable || options.readonly) return '<div class="'+className+'"><label>'+labelText+'</label><div class="readonly-value" title="'+esc(display)+'" data-readonly-field="'+esc(key)+'">'+esc(display || window.__tbFmt.UNKNOWN_VALUE)+'</div>'+(options.submit ? hiddenInput : "")+help+'</div>'; if (key === "upstreamBalanceProbeTemplate") return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><select id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'">'+balanceProbeTemplates.map(t => '<option value="'+esc(t)+'" '+(t === display ? "selected" : "")+'>'+esc(t)+'</option>').join("")+'</select>'+help+'</div>'; if (key === "upstreamProtocolPreset") return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><select id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'">'+["auto","chat","image"].map(t => '<option value="'+esc(t)+'" '+(t === display ? "selected" : "")+'>'+esc(t)+'</option>').join("")+'</select>'+help+'</div>'; const type = options.type || (String(key).toLowerCase().includes("key") ? "password" : "text"); const attrs = options.type === "number" ? ' type="number" step="any"' : ' type="'+esc(type)+'"'; const placeholder = options.placeholder ? ' placeholder="'+esc(options.placeholder)+'"' : ""; return '<div class="'+className+'"><label for="'+esc(fieldId)+'">'+labelText+'</label><input'+attrs+placeholder+' id="'+esc(fieldId)+'" name="'+esc(key)+'" aria-label="'+esc(label)+'" autocomplete="off" data-field="'+esc(key)+'" data-original="'+esc(display)+'" value="'+esc(display)+'">'+help+'</div>'; }
|
|
622
641
|
function selectHtml(upstreamModel, billingModel, options){ const values = options.includes(billingModel) ? options : [billingModel, ...options].filter(Boolean); return '<select name="billingModel" aria-label="billingModel" autocomplete="off" data-model="'+esc(upstreamModel)+'">'+values.map(value => '<option '+(value === billingModel ? "selected" : "")+'>'+esc(value)+'</option>').join("")+'</select>'; }
|
|
623
642
|
function modelEnableHtml(model){ return '<input class="model-toggle" type="checkbox" aria-label="Enable '+esc(model.upstreamModel)+'" data-model-enabled="'+esc(model.upstreamModel)+'" '+(model.enabled === false ? "" : "checked")+' '+(editing ? "" : "disabled")+'>'; }
|
|
643
|
+
function setupModelBulkToggle(){ const bulk = document.getElementById("modelBulkToggle"); if (!bulk) return; const rows = Array.from(document.querySelectorAll("[data-model-enabled]")); rows.forEach(input => input.onchange = updateModelBulkToggle); bulk.onchange = event => { rows.forEach(input => { if (!input.disabled) input.checked = Boolean(event.target.checked); }); updateModelBulkToggle(); }; updateModelBulkToggle(); }
|
|
644
|
+
function updateModelBulkToggle(){ const bulk = document.getElementById("modelBulkToggle"); if (!bulk) return; const rows = Array.from(document.querySelectorAll("[data-model-enabled]")).filter(input => !input.disabled); const checked = rows.filter(input => input.checked).length; const mixed = checked > 0 && checked < rows.length; bulk.checked = rows.length > 0 && checked === rows.length; bulk.indeterminate = mixed; bulk.setAttribute("aria-checked", mixed ? "mixed" : String(bulk.checked)); bulk.disabled = !editing || rows.length === 0; }
|
|
624
645
|
function registryStatusForAction(action){ if (action === "drain") return "draining"; if (action === "activate") return "active"; return "offline"; }
|
|
625
646
|
function setStatusActionBusy(busy){ document.querySelectorAll("[data-status-action]").forEach(btn => { btn.disabled = Boolean(busy); }); }
|
|
626
647
|
function setDetailSavingBusy(busy){ const edit = document.getElementById("editDetail"); edit.disabled = Boolean(busy); edit.textContent = busy ? "Saving" : (editing ? "Save changes" : "Edit config"); document.querySelectorAll("#detailGrid [data-field], #detailGrid [data-model], #detailGrid [data-model-enabled], [data-status-action], #deleteSeller").forEach(input => { input.disabled = Boolean(busy); }); }
|
|
@@ -628,17 +649,18 @@ document.getElementById("editDetail").onclick = async () => { if (!editing){ edi
|
|
|
628
649
|
function modelConfigPatchFromDetail(){ const enabledByModel = {}; document.querySelectorAll("[data-model-enabled]").forEach(input => enabledByModel[input.dataset.modelEnabled] = Boolean(input.checked)); return (currentDetail?.models || []).map(model => ({ ...(model.configModel || { id:model.upstreamModel }), id:model.upstreamModel, enabled: enabledByModel[model.upstreamModel] !== false })); }
|
|
629
650
|
document.getElementById("deleteSeller").onclick = async () => { if (!currentDetail) return; if (deleteReady && !confirm("Destroy deployment for "+currentDetail.row.name+"?")) return; const result = await api("/api/sellers/"+encodeURIComponent(currentDetail.row.id)+"/deployment", { method:"DELETE", body: JSON.stringify({ confirm: deleteReady }) }); showDetailStatus(result.stdout || (deleteReady ? "Deployment destroy requested." : "Dry-run ready. Click delete again to confirm destroy."), false); deleteReady = !deleteReady; document.getElementById("deleteSeller").title = deleteReady ? "Confirm destroy deployment" : "Delete deployment"; };
|
|
630
651
|
document.getElementById("closeDetail").onclick = () => { deleteReady = false; document.getElementById("detailModal").classList.remove("open"); };
|
|
652
|
+
document.getElementById("hideNoInstanceApps").onchange = event => { hideNoInstanceApps = Boolean(event.target.checked); renderSellerRows(sellerRowsCache); };
|
|
631
653
|
document.querySelectorAll("[data-status-action]").forEach(btn => btn.onclick = async () => { if (!currentDetail) return; const action = btn.dataset.statusAction; const id = currentDetail.row.id; const status = registryStatusForAction(action); try { setStatusActionBusy(true); showDetailStatus("Updating registry status", true); const result = await api("/api/sellers/"+encodeURIComponent(id)+"/"+action, { method:"POST" }); if (!result.ok) throw new Error(result.stderr || "Status update failed."); patchSellerRegistryStatus(id, status); deleteReady = false; currentDetail = null; document.getElementById("detailModal").classList.remove("open"); } catch (err) { showDetailStatus(err.message || "Status update failed.", false); } finally { setStatusActionBusy(false); } });
|
|
632
654
|
document.getElementById("createSeller").onclick = () => { buildCreateForm(); document.getElementById("createModal").classList.add("open"); };
|
|
633
655
|
document.getElementById("closeCreate").onclick = () => document.getElementById("createModal").classList.remove("open");
|
|
634
656
|
function buildCreateForm(){ clearInterval(createJobTimer); createJobTimer = null; currentCreateJob = null; expandedProgressSteps = new Set(); createAppSuffix = randomAppSuffix(); document.getElementById("submitCreate").disabled = false; document.getElementById("submitCreate").textContent = "Create seller"; document.getElementById("createStatus").textContent = ""; document.getElementById("createStatus").classList.add("hidden"); document.getElementById("createStatus").classList.remove("loading"); document.getElementById("createProgress").classList.add("hidden"); document.getElementById("createProgress").innerHTML = ""; setCreateFormDisabled(false); const defaults = createDefaults(); document.getElementById("createFields").innerHTML = createFormHtml(defaults); setupCreateFormBehavior(); setupPaymentTabs(); }
|
|
635
|
-
function createFormHtml(defaults){ return [createSectionHtml("基础信息设置", ["sellerName","app","region","image","flyConfig"], defaults), createSectionHtml("上游设置", ["upstreamWebsite","upstreamUrl","upstreamApiKey","upstreamBalanceProbeTemplate","upstreamBalanceProbeUrl","upstreamBalanceProbeUserId","upstreamBalanceProbeRechargeUrl"], defaults), createSectionHtml("性能与安全", ["maxConnections","maxQueueDepth","markupRatio","discountRatio"], defaults), paymentSectionHtml(defaults)].join(""); }
|
|
657
|
+
function createFormHtml(defaults){ return [createSectionHtml("基础信息设置", ["sellerName","app","region","image","flyConfig"], defaults), createSectionHtml("上游设置", ["upstreamWebsite","upstreamUrl","upstreamApiKey","upstreamProtocolPreset","upstreamBalanceProbeTemplate","upstreamBalanceProbeUrl","upstreamBalanceProbeUserId","upstreamBalanceProbeRechargeUrl"], defaults), createSectionHtml("性能与安全", ["maxConnections","maxQueueDepth","markupRatio","discountRatio"], defaults), paymentSectionHtml(defaults)].join(""); }
|
|
636
658
|
function createSectionHtml(title, fields, defaults){ return '<section class="create-section"><h2>'+esc(title)+'</h2><div class="section-grid">'+fields.map(key => fieldHtml(key, defaults[key], createFieldEditable(key), createFieldOptions(key))).join("")+'</div></section>'; }
|
|
637
659
|
function paymentSectionHtml(defaults){ const enabled = new Set(defaults.paymentMethods || []); const editableFields = [["clawtipPayTo","payTo"],["clawtipSm4KeyBase64","sm4KeyBase64"]]; const fixedFields = ["clawtipSkillSlug","clawtipSkillId","clawtipDescription","clawtipResourceUrl","clawtipActivationFeeFen","clawtipMicrosPerFen"]; return '<section class="create-section"><h2>支付设置</h2><div class="payment-tabs">'+paymentMethods.map((method,index) => '<button type="button" class="payment-tab '+(index === 0 ? "active" : "")+'" data-payment-tab="'+esc(method)+'">'+esc(method)+'</button>').join("")+'</div><div class="payment-panel" data-payment-panel="clawtip" data-enabled="'+String(enabled.has("clawtip"))+'"><div class="payment-head"><h3>ClawTip</h3>'+paymentToggleHtml("clawtip", enabled.has("clawtip"))+'</div><div class="section-grid">'+editableFields.map(([key,label]) => fieldHtml(key, defaults[key], true, { ...createFieldOptions(key), label })).join("")+fixedFields.map(key => fieldHtml(key, defaults[key], false, { hidden:true })).join("")+'<div class="field full"><label>自动生成参数</label><div class="generated-summary" data-generated-summary="clawtip"></div></div></div></div><div class="payment-panel hidden" data-payment-panel="mock" data-enabled="'+String(enabled.has("mock"))+'"><div class="payment-head"><h3>Mock</h3>'+paymentToggleHtml("mock", enabled.has("mock"))+'</div><div class="section-grid">'+fieldHtml("参数规则", "启用即可使用 mock 支付方式,无需额外参数", false, { full:true })+'</div></div></section>'; }
|
|
638
660
|
function paymentToggleHtml(method, enabled){ return '<button type="button" class="pill-switch '+(enabled ? "enabled" : "")+'" data-payment-toggle="'+esc(method)+'" aria-pressed="'+String(enabled)+'">'+(enabled ? "已启用" : "未启用")+'</button>'; }
|
|
639
661
|
function numericFieldOptions(key){ return ["maxConnections","maxQueueDepth","markupRatio","discountRatio","clawtipActivationFeeFen","clawtipMicrosPerFen"].includes(key) ? { type:"number" } : {}; }
|
|
640
662
|
function createFieldEditable(key){ return !["app","image","flyConfig"].includes(key); }
|
|
641
|
-
function createFieldOptions(key){ const labels = { sellerName:"Seller name", app:"Fly app name", region:"Fly region", image:"Seller image", flyConfig:"Fly config file", upstreamWebsite:"Upstream website", upstreamUrl:"OpenAI-compatible base URL", upstreamApiKey:"Upstream API key", upstreamBalanceProbeTemplate:"Balance probe template", upstreamBalanceProbeUrl:"Balance probe URL (auto from template)", upstreamBalanceProbeUserId:"Balance probe user ID", upstreamBalanceProbeRechargeUrl:"Recharge URL", maxConnections:"Max connections", maxQueueDepth:"Max queue depth", markupRatio:"Markup ratio", discountRatio:"Discount ratio", clawtipPayTo:"payTo", clawtipSm4KeyBase64:"sm4KeyBase64" }; const help = { sellerName:"Use a domain-style slug, for example openrouter-ai or moonshot-cn. The Fly app will be generated as tbs-<seller-name>-<random>.", app:"Generated from seller name. Example: tbs-openrouter-ai-k7p9x.", region:"Fly.io region, for example sin, nrt, hkg, lax.", image:"Uses the published seller image by default.", flyConfig:"Uses the standard tb-seller Fly.io config.", upstreamWebsite:"Customer-facing upstream website. Example: https://moonshot.cn", upstreamUrl:"OpenAI-compatible API base URL. Examples: https://api.moonshot.cn/v1 or https://openrouter.ai/api/v1", upstreamApiKey:"Upstream provider API key. This is submitted to the seller config and never shown in the seller list.", upstreamBalanceProbeTemplate:"Choose the parser used for balance lookup. usage_generic calls /v1/usage with the upstream API key.", upstreamBalanceProbeUrl:"Auto-filled and disabled for known templates. NewAPI generic keeps this editable. Example: https://code.shoestravel.xin/api/user/self", upstreamBalanceProbeUserId:"Only needed by NewAPI-style balance endpoints that require a user id.", upstreamBalanceProbeRechargeUrl:"Where operators recharge this upstream account. Example: https://code.shoestravel.xin/topup or https://openrouter.ai/settings/credits", maxConnections:"Seller concurrency limit. Each connection can serve one in-flight request.", maxQueueDepth:"Number of queued requests allowed when all connections are busy.", markupRatio:"Multiplier applied to upstream prices before discount. 1.0 = passthrough.", discountRatio:"Configured
|
|
663
|
+
function createFieldOptions(key){ const labels = { sellerName:"Seller name", app:"Fly app name", region:"Fly region", image:"Seller image", flyConfig:"Fly config file", upstreamWebsite:"Upstream website", upstreamUrl:"OpenAI-compatible base URL", upstreamApiKey:"Upstream API key", upstreamProtocolPreset:"Protocol preset", upstreamBalanceProbeTemplate:"Balance probe template", upstreamBalanceProbeUrl:"Balance probe URL (auto from template)", upstreamBalanceProbeUserId:"Balance probe user ID", upstreamBalanceProbeRechargeUrl:"Recharge URL", maxConnections:"Max connections", maxQueueDepth:"Max queue depth", markupRatio:"Markup ratio", discountRatio:"Discount ratio", clawtipPayTo:"payTo", clawtipSm4KeyBase64:"sm4KeyBase64" }; const help = { sellerName:"Use a domain-style slug, for example openrouter-ai or moonshot-cn. The Fly app will be generated as tbs-<seller-name>-<random>.", app:"Generated from seller name. Example: tbs-openrouter-ai-k7p9x.", region:"Fly.io region, for example sin, nrt, hkg, lax.", image:"Uses the published seller image by default.", flyConfig:"Uses the standard tb-seller Fly.io config.", upstreamWebsite:"Customer-facing upstream website. Example: https://moonshot.cn", upstreamUrl:"OpenAI-compatible API base URL. Examples: https://api.moonshot.cn/v1 or https://openrouter.ai/api/v1", upstreamApiKey:"Upstream provider API key. This is submitted to the seller config and never shown in the seller list.", upstreamProtocolPreset:"Use image for nodes that should only publish /v1/images/generations.", upstreamBalanceProbeTemplate:"Choose the parser used for balance lookup. usage_generic calls /v1/usage with the upstream API key.", upstreamBalanceProbeUrl:"Auto-filled and disabled for known templates. NewAPI generic keeps this editable. Example: https://code.shoestravel.xin/api/user/self", upstreamBalanceProbeUserId:"Only needed by NewAPI-style balance endpoints that require a user id.", upstreamBalanceProbeRechargeUrl:"Where operators recharge this upstream account. Example: https://code.shoestravel.xin/topup or https://openrouter.ai/settings/credits", maxConnections:"Seller concurrency limit. Each connection can serve one in-flight request.", maxQueueDepth:"Number of queued requests allowed when all connections are busy.", markupRatio:"Multiplier applied to upstream prices before discount. 1.0 = passthrough.", discountRatio:"Configured ratio applied after markup. UI displays this raw value, for example 1, 0.5, or 0.01.", clawtipPayTo:"Alipay payTo target for ClawTip payments.", clawtipSm4KeyBase64:"Base64 SM4 symmetric key used to encrypt ClawTip payment requests." }; const options = { ...numericFieldOptions(key) }; if (labels[key]) options.label = labels[key]; if (help[key]) options.help = help[key]; if (["app","image","flyConfig"].includes(key)) options.submit = true; return options; }
|
|
642
664
|
function randomAppSuffix(){ return Math.random().toString(36).replace(/[^a-z0-9]/g, "").slice(2, 7) || "x7p9k"; }
|
|
643
665
|
function setupCreateFormBehavior(){ ["sellerName","upstreamUrl","upstreamBalanceProbeTemplate"].forEach(key => { const input = document.querySelector('#createFields [data-field="'+key+'"]'); if (!input) return; input.addEventListener("input", updateGeneratedCreateFields); input.addEventListener("change", updateGeneratedCreateFields); }); updateGeneratedCreateFields(); }
|
|
644
666
|
function updateGeneratedCreateFields(){ const sellerName = createFieldString("sellerName") || "seller"; const app = appNameFromSellerName(sellerName); const template = createFieldString("upstreamBalanceProbeTemplate") || "auto"; const upstreamUrl = createFieldString("upstreamUrl") || ""; const balanceInput = document.querySelector('#createFields [data-field="upstreamBalanceProbeUrl"]'); setCreateFieldValue("app", app); if (balanceInput){ const editableBalanceUrl = template === "newapi_generic"; balanceInput.disabled = !editableBalanceUrl; balanceInput.readOnly = !editableBalanceUrl; if (editableBalanceUrl && balanceInput.dataset.autoGenerated === "true") { setCreateFieldValue("upstreamBalanceProbeUrl", ""); balanceInput.dataset.autoGenerated = "false"; } if (!editableBalanceUrl) { setCreateFieldValue("upstreamBalanceProbeUrl", balanceProbeUrlForTemplate(template, upstreamUrl)); balanceInput.dataset.autoGenerated = "true"; } } setCreateFieldValue("clawtipSkillSlug", app); setCreateFieldValue("clawtipSkillId", "si-" + app); setCreateFieldValue("clawtipDescription", "TokenBuddy Seller " + sellerSlugFromName(sellerName)); setCreateFieldValue("clawtipResourceUrl", "https://" + app + ".fly.dev"); setCreateFieldValue("clawtipActivationFeeFen", "1"); setCreateFieldValue("clawtipMicrosPerFen", "10000"); const summary = document.querySelector('[data-generated-summary="clawtip"]'); if (summary) summary.textContent = "skillSlug=" + app + " · skillId=si-" + app + " · resourceUrl=https://" + app + ".fly.dev · activationFeeFen=1"; }
|
|
@@ -654,13 +676,19 @@ function selectPaymentTab(method){ document.querySelectorAll("[data-payment-tab]
|
|
|
654
676
|
function togglePaymentMethod(method){ const panel = document.querySelector('[data-payment-panel="'+method+'"]'); if (!panel) return; panel.dataset.enabled = String(panel.dataset.enabled !== "true"); updatePaymentPanels(); }
|
|
655
677
|
function enabledPaymentMethods(){ return Array.from(document.querySelectorAll("[data-payment-panel]")).filter(panel => panel.dataset.enabled === "true").map(panel => panel.dataset.paymentPanel); }
|
|
656
678
|
function updatePaymentPanels(){ document.querySelectorAll("[data-payment-panel]").forEach(panel => { const enabled = panel.dataset.enabled === "true"; const toggle = panel.querySelector("[data-payment-toggle]"); if (toggle){ toggle.classList.toggle("enabled", enabled); toggle.setAttribute("aria-pressed", String(enabled)); toggle.textContent = enabled ? "已启用" : "未启用"; } panel.querySelectorAll("[data-field]").forEach(input => { input.disabled = !enabled; }); }); }
|
|
657
|
-
function createDefaults(){ const regions = sellerRowsCache.map(row => String(row.region || "").toLowerCase()).filter(Boolean); const region = regions[0] || "sin"; const existing = new Set(sellerRowsCache.flatMap(row => [row.id, row.name, row.app].filter(Boolean))); let index = sellerRowsCache.length + 1; let sellerName = "openrouter-ai"; while (existing.has(sellerName)) sellerName = "openrouter-ai-" + index++; const app = appNameFromSellerName(sellerName); return { sellerName, app, region, image:"registry.fly.io/tb-seller:latest", upstreamWebsite:"https://openrouter.ai", upstreamUrl:"https://openrouter.ai/api/v1", upstreamApiKey:"", upstreamBalanceProbeTemplate:"openrouter", upstreamBalanceProbeUrl:"https://openrouter.ai/api/v1/credits", upstreamBalanceProbeUserId:"", upstreamBalanceProbeRechargeUrl:"https://openrouter.ai/settings/credits", maxConnections:8, maxQueueDepth:4, markupRatio:1.2, discountRatio:1, paymentMethods:["clawtip"], clawtipPayTo:"", clawtipSm4KeyBase64:"", clawtipSkillSlug:app, clawtipSkillId:"si-"+app, clawtipDescription:"TokenBuddy Seller "+sellerName, clawtipResourceUrl:"https://"+app+".fly.dev", clawtipActivationFeeFen:1, clawtipMicrosPerFen:10000, flyConfig:"
|
|
679
|
+
function createDefaults(){ const regions = sellerRowsCache.map(row => String(row.region || "").toLowerCase()).filter(Boolean); const region = regions[0] || "sin"; const existing = new Set(sellerRowsCache.flatMap(row => [row.id, row.name, row.app].filter(Boolean))); let index = sellerRowsCache.length + 1; let sellerName = "openrouter-ai"; while (existing.has(sellerName)) sellerName = "openrouter-ai-" + index++; const app = appNameFromSellerName(sellerName); return { sellerName, app, region, image:"registry.fly.io/tb-seller:latest", upstreamWebsite:"https://openrouter.ai", upstreamUrl:"https://openrouter.ai/api/v1", upstreamApiKey:"", upstreamProtocolPreset:"auto", upstreamBalanceProbeTemplate:"openrouter", upstreamBalanceProbeUrl:"https://openrouter.ai/api/v1/credits", upstreamBalanceProbeUserId:"", upstreamBalanceProbeRechargeUrl:"https://openrouter.ai/settings/credits", maxConnections:8, maxQueueDepth:4, markupRatio:1.2, discountRatio:1, paymentMethods:["clawtip"], clawtipPayTo:"", clawtipSm4KeyBase64:"", clawtipSkillSlug:app, clawtipSkillId:"si-"+app, clawtipDescription:"TokenBuddy Seller "+sellerName, clawtipResourceUrl:"https://"+app+".fly.dev", clawtipActivationFeeFen:1, clawtipMicrosPerFen:10000, flyConfig:"" }; }
|
|
658
680
|
document.getElementById("submitCreate").onclick = async () => { const body = { paymentMethods: enabledPaymentMethods() }; document.querySelectorAll("#createFields [data-field]").forEach(input => body[input.dataset.field] = fieldValue(input)); if (!confirm("Create seller deployment "+body.sellerName+" on Fly.io?")) return; try { setCreateFormDisabled(true); document.getElementById("submitCreate").disabled = true; document.getElementById("submitCreate").textContent = "Creating"; document.getElementById("createStatus").textContent = ""; document.getElementById("createStatus").classList.add("hidden"); document.getElementById("createStatus").classList.remove("loading"); document.getElementById("createProgress").classList.remove("hidden"); document.getElementById("createProgress").innerHTML = '<div class="progress-step running"><div class="progress-title"><span class="spinner" aria-hidden="true"></span><strong>Starting</strong></div><div class="progress-meta"><span>Preparing create workflow.</span></div></div>'; const response = await api("/api/sellers", { method:"POST", body: JSON.stringify(body) }); renderCreateJob(response.job); pollCreateJob(response.jobId); } catch (err) { setCreateFormDisabled(false); document.getElementById("submitCreate").disabled = false; document.getElementById("submitCreate").textContent = "Create seller"; document.getElementById("createStatus").classList.remove("hidden"); document.getElementById("createStatus").textContent = err.message; document.getElementById("createStatus").classList.remove("loading"); } };
|
|
659
681
|
function pollCreateJob(jobId){ clearInterval(createJobTimer); createJobTimer = setInterval(async () => { try { const job = await api("/api/jobs/"+encodeURIComponent(jobId)); renderCreateJob(job); if (job.status !== "running"){ clearInterval(createJobTimer); createJobTimer = null; if (job.status === "succeeded") loadSellers(); } } catch (err) { clearInterval(createJobTimer); createJobTimer = null; document.getElementById("createStatus").classList.remove("hidden"); document.getElementById("createStatus").textContent = err.message; document.getElementById("createStatus").classList.remove("loading"); } }, 1200); }
|
|
660
682
|
function renderCreateJob(job){ if (!job) return; currentCreateJob = job; const done = job.status !== "running"; const events = job.events || []; const progress = document.getElementById("createProgress"); const status = document.getElementById("createStatus"); progress.classList.remove("hidden"); status.classList.toggle("hidden", !done); status.classList.remove("loading"); if (done){ status.textContent = job.status === "succeeded" ? "Created and added to bootstrap registry." : (job.error || "Create seller failed."); status.removeAttribute("role"); } else { status.textContent = ""; } progress.innerHTML = events.map(event => progressStep(event)).join("") || '<div class="progress-step running"><div class="progress-title"><span class="spinner" aria-hidden="true"></span><strong>Starting</strong></div><div class="progress-meta"><span>Preparing create workflow.</span></div></div>'; if (done && job.status === "failed"){ setCreateFormDisabled(false); document.getElementById("submitCreate").disabled = false; document.getElementById("submitCreate").textContent = "Retry create"; } else if (done) { document.getElementById("submitCreate").disabled = true; document.getElementById("submitCreate").textContent = "Created"; } }
|
|
661
|
-
function
|
|
683
|
+
function progressLogText(event){ const result = event.result || {}; return [result.command ? "$ " + result.command.join(" ") : "", result.stdout || "", result.stderr || ""].filter(Boolean).join("\\n"); }
|
|
684
|
+
function progressCopyIcon(){ return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'; }
|
|
685
|
+
function progressStep(event){ const log = progressLogText(event); const expanded = expandedProgressSteps.has(event.stepId); const copied = copiedProgressStepId === event.stepId; const spinner = event.status === "running" ? '<span class="spinner" aria-hidden="true"></span>' : ""; return '<div role="button" tabindex="0" class="progress-step '+esc(event.status)+'" data-progress-step="'+esc(event.stepId)+'" aria-expanded="'+String(expanded)+'"><div class="progress-title">'+spinner+'<strong>'+esc(event.title)+'</strong></div><div class="progress-meta"><span>'+esc(event.message || event.status)+'</span>'+(log ? '<span class="progress-toggle">'+(expanded ? "Hide details" : "Show details")+'</span>' : '')+'</div>'+(log && expanded ? '<div class="progress-log-wrap"><button type="button" class="progress-copy" data-copy-progress-log="'+esc(event.stepId)+'" title="Copy output" aria-label="Copy output">'+progressCopyIcon()+'<span>'+(copied ? "Copied" : "Copy")+'</span></button><pre class="progress-log">'+esc(log)+'</pre></div>' : '')+'</div>'; }
|
|
686
|
+
async function copyProgressLog(stepId){ const event = (currentCreateJob?.events || []).find(item => item.stepId === stepId); if (!event) return; const text = progressLogText(event); try { if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text); else fallbackCopyText(text); copiedProgressStepId = stepId; if (currentCreateJob) renderCreateJob(currentCreateJob); setTimeout(() => { if (copiedProgressStepId === stepId) { copiedProgressStepId = ""; if (currentCreateJob) renderCreateJob(currentCreateJob); } }, 1400); } catch { fallbackCopyText(text); copiedProgressStepId = stepId; if (currentCreateJob) renderCreateJob(currentCreateJob); } }
|
|
687
|
+
function fallbackCopyText(text){ const area = document.createElement("textarea"); area.value = text; area.setAttribute("readonly", ""); area.style.position = "fixed"; area.style.opacity = "0"; document.body.appendChild(area); area.select(); document.execCommand("copy"); area.remove(); }
|
|
662
688
|
function setCreateFormDisabled(disabled){ document.querySelectorAll("#createFields [data-field], #createFields [data-payment-tab], #createFields [data-payment-toggle]").forEach(input => { input.disabled = Boolean(disabled); }); if (!disabled) updatePaymentPanels(); }
|
|
663
|
-
|
|
689
|
+
function toggleProgressStep(id){ if (expandedProgressSteps.has(id)) expandedProgressSteps.delete(id); else expandedProgressSteps.add(id); if (currentCreateJob) renderCreateJob(currentCreateJob); }
|
|
690
|
+
document.getElementById("createProgress").onclick = event => { const copy = event.target.closest("[data-copy-progress-log]"); if (copy){ event.stopPropagation(); copyProgressLog(copy.dataset.copyProgressLog); return; } const step = event.target.closest("[data-progress-step]"); if (!step) return; toggleProgressStep(step.dataset.progressStep); };
|
|
691
|
+
document.getElementById("createProgress").onkeydown = event => { if (event.key !== "Enter" && event.key !== " ") return; const step = event.target.closest("[data-progress-step]"); if (!step || event.target.closest("[data-copy-progress-log]")) return; event.preventDefault(); toggleProgressStep(step.dataset.progressStep); };
|
|
664
692
|
document.getElementById("closeBootstrapConfig").onclick = () => document.getElementById("bootstrapModal").classList.remove("open");
|
|
665
693
|
function fieldValue(input){ return numeric(input.value); }
|
|
666
694
|
function numeric(value){ const n = Number(value); return value !== "" && Number.isFinite(n) ? n : value; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const TB_ADMIN_WORKDIR_ENV = "TB_ADMIN_WORKDIR";
|
|
2
|
+
export declare const TOKENBUDDY_ADMIN_CONFIG_ENV = "TOKENBUDDY_ADMIN_CONFIG";
|
|
3
|
+
export declare const DEFAULT_ADMIN_WORKDIR_RELATIVE: string;
|
|
4
|
+
export interface AdminWorkdirOptions {
|
|
5
|
+
cliWorkdir?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
homeDir?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AdminConfigPathInput {
|
|
10
|
+
workdir?: string;
|
|
11
|
+
cliConfig?: string;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
homeDir?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function defaultAdminWorkdir(homeDir?: string): string;
|
|
16
|
+
export declare function resolveAdminWorkdir(options?: AdminWorkdirOptions): string;
|
|
17
|
+
export declare function resolveWorkdirPath(workdir: string, input: string): string;
|
|
18
|
+
export declare function defaultAdminConfigPath(workdir: string): string;
|
|
19
|
+
export declare function resolveAdminConfigPath(input?: AdminConfigPathInput): string;
|
|
20
|
+
export declare function defaultProviderPath(workdir: string, provider: "fly.io"): string;
|
|
21
|
+
//# sourceMappingURL=workdir.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
export const TB_ADMIN_WORKDIR_ENV = "TB_ADMIN_WORKDIR";
|
|
4
|
+
export const TOKENBUDDY_ADMIN_CONFIG_ENV = "TOKENBUDDY_ADMIN_CONFIG";
|
|
5
|
+
export const DEFAULT_ADMIN_WORKDIR_RELATIVE = path.join(".config", "tokenbuddy");
|
|
6
|
+
function expandHome(input, homeDir) {
|
|
7
|
+
if (input === "~") {
|
|
8
|
+
return homeDir;
|
|
9
|
+
}
|
|
10
|
+
if (input.startsWith("~/")) {
|
|
11
|
+
return path.join(homeDir, input.slice(2));
|
|
12
|
+
}
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
export function defaultAdminWorkdir(homeDir = os.homedir()) {
|
|
16
|
+
return path.join(homeDir, DEFAULT_ADMIN_WORKDIR_RELATIVE);
|
|
17
|
+
}
|
|
18
|
+
export function resolveAdminWorkdir(options = {}) {
|
|
19
|
+
const env = options.env || process.env;
|
|
20
|
+
const homeDir = options.homeDir || os.homedir();
|
|
21
|
+
const raw = options.cliWorkdir || env[TB_ADMIN_WORKDIR_ENV] || defaultAdminWorkdir(homeDir);
|
|
22
|
+
const expanded = expandHome(raw, homeDir);
|
|
23
|
+
return path.resolve(expanded);
|
|
24
|
+
}
|
|
25
|
+
export function resolveWorkdirPath(workdir, input) {
|
|
26
|
+
const expanded = expandHome(input, os.homedir());
|
|
27
|
+
if (path.isAbsolute(expanded)) {
|
|
28
|
+
return path.resolve(expanded);
|
|
29
|
+
}
|
|
30
|
+
return path.resolve(workdir, expanded);
|
|
31
|
+
}
|
|
32
|
+
export function defaultAdminConfigPath(workdir) {
|
|
33
|
+
return path.join(workdir, "admin.toml");
|
|
34
|
+
}
|
|
35
|
+
export function resolveAdminConfigPath(input = {}) {
|
|
36
|
+
const env = input.env || process.env;
|
|
37
|
+
const workdir = input.workdir || resolveAdminWorkdir({
|
|
38
|
+
env,
|
|
39
|
+
homeDir: input.homeDir
|
|
40
|
+
});
|
|
41
|
+
const configuredPath = input.cliConfig || env[TOKENBUDDY_ADMIN_CONFIG_ENV];
|
|
42
|
+
if (configuredPath) {
|
|
43
|
+
return resolveWorkdirPath(workdir, configuredPath);
|
|
44
|
+
}
|
|
45
|
+
return defaultAdminConfigPath(workdir);
|
|
46
|
+
}
|
|
47
|
+
export function defaultProviderPath(workdir, provider) {
|
|
48
|
+
return path.join(workdir, "providers", `${provider}.toml`);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=workdir.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenbuddy/tb-admin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "Remote admin CLI for TokenBuddy seller apps",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
"tb-admin": "./bin/tb-admin.js"
|
|
10
10
|
},
|
|
11
11
|
"private": false,
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/*.js",
|
|
14
|
+
"dist/src/**/*.js",
|
|
15
|
+
"dist/src/**/*.d.ts",
|
|
16
|
+
"templates/"
|
|
17
|
+
],
|
|
12
18
|
"scripts": {
|
|
13
19
|
"build": "tsc"
|
|
14
20
|
},
|
|
15
21
|
"dependencies": {
|
|
16
22
|
"@iarna/toml": "^2.2.5",
|
|
17
|
-
"@tokenbuddy/contracts": "^1.0.
|
|
23
|
+
"@tokenbuddy/contracts": "^1.0.37",
|
|
18
24
|
"@types/js-yaml": "^4.0.9",
|
|
19
25
|
"cli-table3": "^0.6.4",
|
|
20
26
|
"commander": "^12.0.0",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
default_profile = "bootstrap"
|
|
2
|
+
|
|
3
|
+
[profiles.bootstrap]
|
|
4
|
+
url = "https://tb-registry.fly.dev"
|
|
5
|
+
token = "<operator-secret>"
|
|
6
|
+
|
|
7
|
+
[profiles.seller-example]
|
|
8
|
+
url = "https://tbs-example.fly.dev"
|
|
9
|
+
token = "<operator-secret>"
|
|
10
|
+
|
|
11
|
+
[seller_providers.fly]
|
|
12
|
+
flyctl_path = "flyctl"
|
|
13
|
+
default_region = "sin"
|
|
14
|
+
default_image = "registry.fly.io/tb-seller:<version>"
|
|
15
|
+
default_config = "deploy-secrets/seller-configs/seller.example.yaml"
|
|
16
|
+
operator_secret = "<operator-secret>"
|
|
17
|
+
volume_name = "tb_seller_data"
|
|
18
|
+
volume_size_gb = 1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Bootstrap Runtime Configs
|
|
2
|
+
|
|
3
|
+
Store real bootstrap registry runtime files in this directory inside your admin
|
|
4
|
+
workdir.
|
|
5
|
+
|
|
6
|
+
Suggested copies:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cp tb-registry.example.yaml tb-registry.yaml
|
|
10
|
+
cp registry.example.json registry.json
|
|
11
|
+
cp admin-web.example.env admin-web.env
|
|
12
|
+
cp cloudflare-r2.example.env cloudflare-r2.env
|
|
13
|
+
cp registry-signing-key.example.json registry-signing-key.json
|
|
14
|
+
chmod 600 tb-registry.yaml registry.json admin-web.env cloudflare-r2.env registry-signing-key.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Never commit files from this directory. They may contain registry signing keys,
|
|
18
|
+
R2 credentials, operator secrets, and admin web secrets.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
TB_REGISTRY_R2_ACCESS_KEY_ID=<r2-access-key-id>
|
|
2
|
+
TB_REGISTRY_R2_SECRET_ACCESS_KEY=<r2-secret-access-key>
|
|
3
|
+
TB_REGISTRY_R2_BUCKET=tokenbuddy-registry
|
|
4
|
+
TB_REGISTRY_R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
|
5
|
+
TB_REGISTRY_R2_REGION=auto
|
|
6
|
+
TB_REGISTRY_PUBLIC_BASE_URL=https://registry.tokenbuddy.ai
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"sellers": [
|
|
4
|
+
{
|
|
5
|
+
"id": "tbs-example",
|
|
6
|
+
"name": "Example TokenBuddy Seller",
|
|
7
|
+
"baseUrl": "https://tbs-example.fly.dev",
|
|
8
|
+
"status": "active",
|
|
9
|
+
"paymentMethods": ["clawtip"],
|
|
10
|
+
"supportedProtocols": ["chat.completions"],
|
|
11
|
+
"models": []
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
bind:
|
|
2
|
+
host: 0.0.0.0
|
|
3
|
+
port: 8080
|
|
4
|
+
clawtip:
|
|
5
|
+
payTo: <pay-to-redacted>
|
|
6
|
+
sm4KeyBase64: <sm4-key-redacted>
|
|
7
|
+
skillSlug: tb-registry
|
|
8
|
+
skillId: si-tb-registry
|
|
9
|
+
description: TokenBuddy ClawTip wallet activation
|
|
10
|
+
resourceUrl: https://tb-registry.fly.dev
|
|
11
|
+
activationFeeFen: 1
|
|
12
|
+
microsPerFen: 1000000
|
|
13
|
+
sellerRegistryPath: /home/bootstrap/data/sellers.json
|
|
14
|
+
allowLocalSellerUrls: false
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Seller Runtime Configs
|
|
2
|
+
|
|
3
|
+
Store real seller runtime YAML files in this directory inside your admin workdir.
|
|
4
|
+
|
|
5
|
+
Start by copying `seller.example.yaml` to a seller-specific file, for example:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp seller.example.yaml tbs-example.yaml
|
|
9
|
+
chmod 600 tbs-example.yaml
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Never commit files from this directory. They may contain upstream API keys,
|
|
13
|
+
operator secrets, and payment provider credentials.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
sellerId: tbs-example
|
|
2
|
+
manifestVersion: manifest.v1
|
|
3
|
+
allowMock: true
|
|
4
|
+
publicMockPayments: false
|
|
5
|
+
|
|
6
|
+
upstreamUrl: https://api.example.com
|
|
7
|
+
upstreamApiKey: <upstream-api-key>
|
|
8
|
+
upstreamCapabilities:
|
|
9
|
+
chatCompletions: supported
|
|
10
|
+
responses: supported
|
|
11
|
+
messages: supported
|
|
12
|
+
imagesGenerations: unsupported
|
|
13
|
+
|
|
14
|
+
enableProtocolAdapters: false
|
|
15
|
+
maxConnections: 8
|
|
16
|
+
maxQueueDepth: 4
|
|
17
|
+
queueTimeoutSecs: 60
|
|
18
|
+
markupRatio: 1.0
|
|
19
|
+
discountRatio: 1.0
|
|
20
|
+
|
|
21
|
+
operatorSecret: <operator-secret>
|
|
22
|
+
|
|
23
|
+
models: []
|
|
24
|
+
modelAliases: {}
|
|
25
|
+
|
|
26
|
+
# Optional ClawTip payment config.
|
|
27
|
+
# clawtip:
|
|
28
|
+
# payTo: <pay-to-redacted>
|
|
29
|
+
# sm4KeyBase64: <sm4-key-redacted>
|
|
30
|
+
# skillSlug: tokenbuddy-seller
|
|
31
|
+
# skillId: si-tokenbuddy-seller
|
|
32
|
+
# description: TokenBuddy Seller
|
|
33
|
+
# resourceUrl: https://tbs-example.fly.dev
|
|
34
|
+
# activationFeeFen: 1
|
|
35
|
+
# microsPerFen: 1000000
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# TokenBuddy admin deploy environment.
|
|
2
|
+
# Copy to env/deploy.env and fill real values in your admin workdir only.
|
|
3
|
+
|
|
4
|
+
RELEASE_VERSION="REPLACE_ME"
|
|
5
|
+
NPM_TOKEN=""
|
|
6
|
+
FLY_API_TOKEN=""
|
|
7
|
+
FLY_ACCESS_TOKEN=""
|
|
8
|
+
OPERATOR_SECRET=""
|
|
9
|
+
|
|
10
|
+
# Optional defaults used by deploy wrappers.
|
|
11
|
+
TB_ADMIN_PROVIDER=fly.io
|
|
12
|
+
TB_ADMIN_ARTIFACTS_DIR=artifacts
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
app = "tb-registry"
|
|
2
|
+
primary_region = "sin"
|
|
3
|
+
|
|
4
|
+
# Deploys must pass --image registry.fly.io/tb-registry:<v>.
|
|
5
|
+
# Do not add [build] here; release image builds live outside this template.
|
|
6
|
+
|
|
7
|
+
[env]
|
|
8
|
+
TOKENBUDDY_REGISTRY_CONFIG = "/home/bootstrap/data/tb-registry.yaml"
|
|
9
|
+
|
|
10
|
+
[mounts]
|
|
11
|
+
source = "tb_registry_data"
|
|
12
|
+
destination = "/home/bootstrap/data"
|
|
13
|
+
|
|
14
|
+
[http_service]
|
|
15
|
+
internal_port = 8080
|
|
16
|
+
force_https = true
|
|
17
|
+
auto_stop_machines = "stop"
|
|
18
|
+
auto_start_machines = true
|
|
19
|
+
min_machines_running = 0
|
|
20
|
+
|
|
21
|
+
[[http_service.checks]]
|
|
22
|
+
interval = "30s"
|
|
23
|
+
timeout = "5s"
|
|
24
|
+
grace_period = "10s"
|
|
25
|
+
method = "GET"
|
|
26
|
+
path = "/healthz"
|
|
27
|
+
|
|
28
|
+
[[vm]]
|
|
29
|
+
size = "shared-cpu-1x"
|
|
30
|
+
memory = "256mb"
|
|
31
|
+
cpus = 1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
app = "tbs-<name>"
|
|
2
|
+
primary_region = "sin"
|
|
3
|
+
|
|
4
|
+
# Deploys must pass --image registry.fly.io/tb-seller:<v>.
|
|
5
|
+
# Do not add [build] here; release image builds live outside this template.
|
|
6
|
+
|
|
7
|
+
[env]
|
|
8
|
+
PORT = "8000"
|
|
9
|
+
TOKENBUDDY_SELLER_CONFIG_PATH = "/data/seller-config.yaml"
|
|
10
|
+
|
|
11
|
+
[http_service]
|
|
12
|
+
internal_port = 8000
|
|
13
|
+
force_https = true
|
|
14
|
+
auto_stop_machines = false
|
|
15
|
+
auto_start_machines = true
|
|
16
|
+
min_machines_running = 1
|
|
17
|
+
|
|
18
|
+
[mounts]
|
|
19
|
+
source = "tb_seller_data"
|
|
20
|
+
destination = "/data"
|
|
21
|
+
|
|
22
|
+
[[vm]]
|
|
23
|
+
memory = "512mb"
|
|
24
|
+
cpu_kind = "shared"
|
|
25
|
+
cpus = 1
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
provider = "fly.io"
|
|
2
|
+
flyctl_path = "flyctl"
|
|
3
|
+
default_region = "sin"
|
|
4
|
+
seller_fly_config = "fly/fly.tb-seller.toml"
|
|
5
|
+
registry_fly_config = "fly/fly.tb-registry.toml"
|
|
6
|
+
deploy_env = "env/deploy.env"
|
|
7
|
+
artifacts_dir = "artifacts"
|
|
8
|
+
|
|
9
|
+
# Use FLY_API_TOKEN in the environment for non-interactive auth.
|
|
10
|
+
# Do not store real Fly.io tokens in this example file.
|