@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/src/display-format.ts
DELETED
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
// Shared display formatters for TokenBuddy UI surfaces.
|
|
2
|
-
//
|
|
3
|
-
// This module is a portable copy of `packages/tb-ui/src/lib/display-format.ts`
|
|
4
|
-
// and the canonical formatter functions declared in `DESIGN.md`
|
|
5
|
-
// ("Formatter Implementation" section). Keep both files in sync — the
|
|
6
|
-
// rule from DESIGN.md is:
|
|
7
|
-
// "Do not add new per-page formatting helpers for these domains.
|
|
8
|
-
// If a page needs a different display, add an explicit option
|
|
9
|
-
// to the shared formatter."
|
|
10
|
-
//
|
|
11
|
-
// Differences vs. the React copy:
|
|
12
|
-
// 1. No React-only imports.
|
|
13
|
-
// 2. Adds `formatBalanceAmount` and `formatSellerStatus` for the
|
|
14
|
-
// admin surface, because the admin UI surfaces live balance
|
|
15
|
-
// numbers and node-status dots that the buyer UI never shows.
|
|
16
|
-
|
|
17
|
-
export const UNKNOWN_VALUE = "—";
|
|
18
|
-
|
|
19
|
-
const PAD2 = (n: number) => (n < 10 ? `0${n}` : `${n}`);
|
|
20
|
-
|
|
21
|
-
export function formatTokenCount(
|
|
22
|
-
value: number | undefined | null,
|
|
23
|
-
options: { compact?: boolean } = {}
|
|
24
|
-
): string {
|
|
25
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
26
|
-
const numeric = value as number;
|
|
27
|
-
if (options.compact === false) return Math.round(numeric).toLocaleString("en-US");
|
|
28
|
-
if (numeric < 10_000) return Math.round(numeric).toLocaleString("en-US");
|
|
29
|
-
if (numeric < 1_000_000) return `${(numeric / 1_000).toFixed(1)}K`;
|
|
30
|
-
if (numeric < 1_000_000_000) return `${(numeric / 1_000_000).toFixed(1)}M`;
|
|
31
|
-
return `${(numeric / 1_000_000_000).toFixed(2)}B`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function formatTokenPair(
|
|
35
|
-
input: number | undefined | null,
|
|
36
|
-
output: number | undefined | null,
|
|
37
|
-
options: { compact?: boolean; separator?: string } = {}
|
|
38
|
-
): string {
|
|
39
|
-
const separator = options.separator ?? " / ";
|
|
40
|
-
return `In ${formatTokenCount(input, options)}${separator}Out ${formatTokenCount(output, options)}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function formatCount(value: number | undefined | null): string {
|
|
44
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
45
|
-
return Math.round(value as number).toLocaleString("en-US");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function formatMoney(
|
|
49
|
-
micros: number | undefined | null,
|
|
50
|
-
options: { digits?: number; ledger?: boolean; signed?: boolean } = {}
|
|
51
|
-
): string {
|
|
52
|
-
if (!Number.isFinite(micros)) return UNKNOWN_VALUE;
|
|
53
|
-
const amount = Math.abs((micros as number) / 1_000_000);
|
|
54
|
-
const digits = options.digits ?? (options.ledger && amount < 0.01 ? 6 : 4);
|
|
55
|
-
const formatted = `$${((micros as number) / 1_000_000).toFixed(digits)}`;
|
|
56
|
-
return options.signed && (micros as number) >= 0 ? `+${formatted}` : formatted;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function formatMoneyPair(
|
|
60
|
-
actualMicros: number | undefined | null,
|
|
61
|
-
referenceMicros: number | undefined | null,
|
|
62
|
-
options: { digits?: number } = {}
|
|
63
|
-
): string {
|
|
64
|
-
if (!Number.isFinite(actualMicros) && !Number.isFinite(referenceMicros)) return UNKNOWN_VALUE;
|
|
65
|
-
return `${formatMoney(actualMicros, options)} / ${formatMoney(referenceMicros, options)}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function formatDuration(valueMs: number | undefined | null): string {
|
|
69
|
-
if (!Number.isFinite(valueMs)) return UNKNOWN_VALUE;
|
|
70
|
-
const ms = Math.max(0, Math.round(valueMs as number));
|
|
71
|
-
if (ms < 1000) return `${ms}ms`;
|
|
72
|
-
return `${(ms / 1000).toFixed(2)}s`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function formatPercent(value: number | undefined | null): string {
|
|
76
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
77
|
-
return `${Math.round((value as number) * 100)}%`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function formatDiscountRatio(discountRatio: number | undefined | null): string {
|
|
81
|
-
if (!Number.isFinite(discountRatio)) return UNKNOWN_VALUE;
|
|
82
|
-
const ratio = Math.max(0, discountRatio as number);
|
|
83
|
-
if (ratio === 0) return "免费";
|
|
84
|
-
if (Math.abs(ratio - 1) < 0.0001) return "原价";
|
|
85
|
-
return `${formatSignificantDiscount(ratio * 10)}折`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function formatPriceMicrosPerMillion(value: number | undefined | null): string {
|
|
89
|
-
return formatMoney(value, { digits: 4 });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function formatPricePair(
|
|
93
|
-
inputMicros: number | undefined | null,
|
|
94
|
-
outputMicros: number | undefined | null
|
|
95
|
-
): string {
|
|
96
|
-
return formatMoneyPair(inputMicros, outputMicros, { digits: 4 });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function formatSignificantDiscount(value: number): string {
|
|
100
|
-
const rounded = Math.round(value * 10) / 10;
|
|
101
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function normalizeStatusLabel(status: string | undefined | null): string {
|
|
105
|
-
if (!status) return UNKNOWN_VALUE;
|
|
106
|
-
return status.trim().toLowerCase().replaceAll("_", " ");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export type StatusTone = "green" | "amber" | "red" | "blue" | "gray";
|
|
110
|
-
|
|
111
|
-
export function statusTone(status: string | undefined | null): StatusTone {
|
|
112
|
-
const normalized = normalizeStatusLabel(status);
|
|
113
|
-
if (
|
|
114
|
-
normalized === "ok" ||
|
|
115
|
-
normalized === "online" ||
|
|
116
|
-
normalized === "configured" ||
|
|
117
|
-
normalized === "settled" ||
|
|
118
|
-
normalized === "completed" ||
|
|
119
|
-
normalized === "success" ||
|
|
120
|
-
normalized === "active" ||
|
|
121
|
-
normalized === "healthy"
|
|
122
|
-
) {
|
|
123
|
-
return "green";
|
|
124
|
-
}
|
|
125
|
-
if (
|
|
126
|
-
normalized === "fallback" ||
|
|
127
|
-
normalized === "pending" ||
|
|
128
|
-
normalized === "degraded" ||
|
|
129
|
-
normalized === "preview" ||
|
|
130
|
-
normalized === "draining" ||
|
|
131
|
-
normalized === "busy capacity" ||
|
|
132
|
-
normalized === "auth unknown"
|
|
133
|
-
) {
|
|
134
|
-
return "amber";
|
|
135
|
-
}
|
|
136
|
-
if (
|
|
137
|
-
normalized === "failed" ||
|
|
138
|
-
normalized === "error" ||
|
|
139
|
-
normalized === "canceled" ||
|
|
140
|
-
normalized === "unhealthy" ||
|
|
141
|
-
normalized === "offline"
|
|
142
|
-
) {
|
|
143
|
-
return "red";
|
|
144
|
-
}
|
|
145
|
-
if (normalized === "running") return "blue";
|
|
146
|
-
return "gray";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function formatHash(value: string | undefined | null, length = 32): string {
|
|
150
|
-
if (!value) return UNKNOWN_VALUE;
|
|
151
|
-
return value.length > length ? `${value.slice(0, length)}...` : value;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function formatSellerId(value: string | undefined | null): string {
|
|
155
|
-
if (!value) return UNKNOWN_VALUE;
|
|
156
|
-
if (value.startsWith("tbs-") && value.length > 10) return value.slice(0, 10);
|
|
157
|
-
if (value.length <= 12) return value;
|
|
158
|
-
return value.slice(0, 12);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function formatTimeCompact(iso: string | undefined | null): string {
|
|
162
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
163
|
-
const d = new Date(iso);
|
|
164
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
165
|
-
const now = new Date();
|
|
166
|
-
const sameDay =
|
|
167
|
-
d.getFullYear() === now.getFullYear() &&
|
|
168
|
-
d.getMonth() === now.getMonth() &&
|
|
169
|
-
d.getDate() === now.getDate();
|
|
170
|
-
const time = `${PAD2(d.getHours())}:${PAD2(d.getMinutes())}`;
|
|
171
|
-
if (sameDay) return time;
|
|
172
|
-
return `${PAD2(d.getMonth() + 1)}/${PAD2(d.getDate())} ${time}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function formatTimeFull(iso: string | undefined | null): string {
|
|
176
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
177
|
-
const d = new Date(iso);
|
|
178
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
179
|
-
return (
|
|
180
|
-
`${d.getFullYear()}-${PAD2(d.getMonth() + 1)}-${PAD2(d.getDate())} ` +
|
|
181
|
-
`${PAD2(d.getHours())}:${PAD2(d.getMinutes())}:${PAD2(d.getSeconds())}`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function formatTimeLedger(iso: string | undefined | null): string {
|
|
186
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
187
|
-
const d = new Date(iso);
|
|
188
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
189
|
-
return (
|
|
190
|
-
`${d.getFullYear()}/${PAD2(d.getMonth() + 1)}/${PAD2(d.getDate())} ` +
|
|
191
|
-
`${PAD2(d.getHours())}:${PAD2(d.getMinutes())}:${PAD2(d.getSeconds())}`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function formatRouteSwitchTime(iso: string | undefined | null): string {
|
|
196
|
-
return formatTimeCompact(iso);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// --- admin-only extensions ---------------------------------------------------
|
|
200
|
-
|
|
201
|
-
export function formatBalanceAmount(
|
|
202
|
-
usdMicros: number | undefined | null,
|
|
203
|
-
currency: string | undefined | null
|
|
204
|
-
): string {
|
|
205
|
-
if (!Number.isFinite(usdMicros)) return UNKNOWN_VALUE;
|
|
206
|
-
const amount = (usdMicros as number) / 1_000_000;
|
|
207
|
-
const digits = Math.abs(amount) >= 100 ? 0 : 2;
|
|
208
|
-
const code = (currency || "USD").toUpperCase();
|
|
209
|
-
return `${code} ${amount.toFixed(digits)}`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function formatSellerCapacity(
|
|
213
|
-
used: number | undefined | null,
|
|
214
|
-
limit: number | undefined | null
|
|
215
|
-
): string {
|
|
216
|
-
if (!Number.isFinite(used) && !Number.isFinite(limit)) return UNKNOWN_VALUE;
|
|
217
|
-
const u = Number.isFinite(used) ? Math.round(used as number) : UNKNOWN_VALUE;
|
|
218
|
-
const l = Number.isFinite(limit) ? Math.round(limit as number) : UNKNOWN_VALUE;
|
|
219
|
-
return `${u} / ${l}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function formatSpeed(value: number | undefined | null): string {
|
|
223
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
224
|
-
return `${(value as number).toFixed(1)} tok/s`;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export type CanonicalStatus =
|
|
228
|
-
| "ok"
|
|
229
|
-
| "online"
|
|
230
|
-
| "configured"
|
|
231
|
-
| "pending"
|
|
232
|
-
| "degraded"
|
|
233
|
-
| "error"
|
|
234
|
-
| "unknown";
|
|
235
|
-
|
|
236
|
-
const SELLER_STATUS_MAP: Record<string, CanonicalStatus> = {
|
|
237
|
-
active: "ok",
|
|
238
|
-
healthy: "ok",
|
|
239
|
-
online: "online",
|
|
240
|
-
configured: "configured",
|
|
241
|
-
pending: "pending",
|
|
242
|
-
draining: "degraded",
|
|
243
|
-
degraded: "degraded",
|
|
244
|
-
busy_capacity: "degraded",
|
|
245
|
-
offline: "error",
|
|
246
|
-
unhealthy: "error",
|
|
247
|
-
error: "error",
|
|
248
|
-
auth_unknown: "unknown",
|
|
249
|
-
unknown: "unknown"
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
export function formatSellerStatus(status: string | undefined | null): CanonicalStatus {
|
|
253
|
-
const key = String(status || "unknown")
|
|
254
|
-
.trim()
|
|
255
|
-
.toLowerCase()
|
|
256
|
-
.replace(/-/g, "_");
|
|
257
|
-
return SELLER_STATUS_MAP[key] || "unknown";
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function sellerStatusTone(status: string | undefined | null): StatusTone {
|
|
261
|
-
return statusTone(formatSellerStatus(status));
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Inlined browser bundle — emits a self-invoking function that
|
|
265
|
-
// attaches the same formatter API to `window.__tbFmt`. The HTML
|
|
266
|
-
// served by `tb-admin ui` includes this bundle verbatim so the
|
|
267
|
-
// page can use the shared spec-compliant helpers without an
|
|
268
|
-
// extra <script src> round-trip. The JS bodies below MUST stay
|
|
269
|
-
// in lockstep with the TS implementations above.
|
|
270
|
-
export function displayFormatBundle(): string {
|
|
271
|
-
return `(() => {
|
|
272
|
-
const UNKNOWN_VALUE = ${JSON.stringify(UNKNOWN_VALUE)};
|
|
273
|
-
const PAD2 = (n) => (n < 10 ? "0" + n : "" + n);
|
|
274
|
-
function formatTokenCount(value, options) {
|
|
275
|
-
options = options || {};
|
|
276
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
277
|
-
const numeric = value;
|
|
278
|
-
if (options.compact === false) return Math.round(numeric).toLocaleString("en-US");
|
|
279
|
-
if (numeric < 10000) return Math.round(numeric).toLocaleString("en-US");
|
|
280
|
-
if (numeric < 1000000) return (numeric / 1000).toFixed(1) + "K";
|
|
281
|
-
if (numeric < 1000000000) return (numeric / 1000000).toFixed(1) + "M";
|
|
282
|
-
return (numeric / 1000000000).toFixed(2) + "B";
|
|
283
|
-
}
|
|
284
|
-
function formatTokenPair(input, output, options) {
|
|
285
|
-
options = options || {};
|
|
286
|
-
const separator = options.separator == null ? " / " : options.separator;
|
|
287
|
-
return "In " + formatTokenCount(input, options) + separator + "Out " + formatTokenCount(output, options);
|
|
288
|
-
}
|
|
289
|
-
function formatCount(value) {
|
|
290
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
291
|
-
return Math.round(value).toLocaleString("en-US");
|
|
292
|
-
}
|
|
293
|
-
function formatMoney(micros, options) {
|
|
294
|
-
options = options || {};
|
|
295
|
-
if (!Number.isFinite(micros)) return UNKNOWN_VALUE;
|
|
296
|
-
const amount = Math.abs(micros / 1000000);
|
|
297
|
-
const digits = options.digits != null ? options.digits : (options.ledger && amount < 0.01 ? 6 : 4);
|
|
298
|
-
const formatted = "$" + (micros / 1000000).toFixed(digits);
|
|
299
|
-
return options.signed && micros >= 0 ? "+" + formatted : formatted;
|
|
300
|
-
}
|
|
301
|
-
function formatMoneyPair(actualMicros, referenceMicros, options) {
|
|
302
|
-
options = options || {};
|
|
303
|
-
if (!Number.isFinite(actualMicros) && !Number.isFinite(referenceMicros)) return UNKNOWN_VALUE;
|
|
304
|
-
return formatMoney(actualMicros, options) + " / " + formatMoney(referenceMicros, options);
|
|
305
|
-
}
|
|
306
|
-
function formatDuration(valueMs) {
|
|
307
|
-
if (!Number.isFinite(valueMs)) return UNKNOWN_VALUE;
|
|
308
|
-
const ms = Math.max(0, Math.round(valueMs));
|
|
309
|
-
if (ms < 1000) return ms + "ms";
|
|
310
|
-
return (ms / 1000).toFixed(2) + "s";
|
|
311
|
-
}
|
|
312
|
-
function formatPercent(value) {
|
|
313
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
314
|
-
return Math.round(value * 100) + "%";
|
|
315
|
-
}
|
|
316
|
-
function formatDiscountRatio(discountRatio) {
|
|
317
|
-
if (!Number.isFinite(discountRatio)) return UNKNOWN_VALUE;
|
|
318
|
-
const ratio = Math.max(0, discountRatio);
|
|
319
|
-
if (ratio === 0) return "免费";
|
|
320
|
-
if (Math.abs(ratio - 1) < 0.0001) return "原价";
|
|
321
|
-
return formatSignificantDiscount(ratio * 10) + "折";
|
|
322
|
-
}
|
|
323
|
-
function formatPriceMicrosPerMillion(value) { return formatMoney(value, { digits: 4 }); }
|
|
324
|
-
function formatPricePair(inputMicros, outputMicros) { return formatMoneyPair(inputMicros, outputMicros, { digits: 4 }); }
|
|
325
|
-
function formatSignificantDiscount(value) {
|
|
326
|
-
const rounded = Math.round(value * 10) / 10;
|
|
327
|
-
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
328
|
-
}
|
|
329
|
-
function normalizeStatusLabel(status) {
|
|
330
|
-
if (!status) return UNKNOWN_VALUE;
|
|
331
|
-
return String(status).trim().toLowerCase().replaceAll("_", " ");
|
|
332
|
-
}
|
|
333
|
-
function statusTone(status) {
|
|
334
|
-
const n = normalizeStatusLabel(status);
|
|
335
|
-
if (n === "ok" || n === "online" || n === "configured" || n === "settled" || n === "completed" || n === "success" || n === "active" || n === "healthy") return "green";
|
|
336
|
-
if (n === "fallback" || n === "pending" || n === "degraded" || n === "preview" || n === "draining" || n === "busy capacity" || n === "auth unknown") return "amber";
|
|
337
|
-
if (n === "failed" || n === "error" || n === "canceled" || n === "unhealthy" || n === "offline") return "red";
|
|
338
|
-
if (n === "running") return "blue";
|
|
339
|
-
return "gray";
|
|
340
|
-
}
|
|
341
|
-
function formatHash(value, length) {
|
|
342
|
-
if (length == null) length = 32;
|
|
343
|
-
if (!value) return UNKNOWN_VALUE;
|
|
344
|
-
return value.length > length ? value.slice(0, length) + "..." : value;
|
|
345
|
-
}
|
|
346
|
-
function formatSellerId(value) {
|
|
347
|
-
if (!value) return UNKNOWN_VALUE;
|
|
348
|
-
if (value.startsWith("tbs-") && value.length > 10) return value.slice(0, 10);
|
|
349
|
-
if (value.length <= 12) return value;
|
|
350
|
-
return value.slice(0, 12);
|
|
351
|
-
}
|
|
352
|
-
function formatTimeCompact(iso) {
|
|
353
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
354
|
-
const d = new Date(iso);
|
|
355
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
356
|
-
const now = new Date();
|
|
357
|
-
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
358
|
-
const time = PAD2(d.getHours()) + ":" + PAD2(d.getMinutes());
|
|
359
|
-
if (sameDay) return time;
|
|
360
|
-
return PAD2(d.getMonth() + 1) + "/" + PAD2(d.getDate()) + " " + time;
|
|
361
|
-
}
|
|
362
|
-
function formatTimeFull(iso) {
|
|
363
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
364
|
-
const d = new Date(iso);
|
|
365
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
366
|
-
return d.getFullYear() + "-" + PAD2(d.getMonth() + 1) + "-" + PAD2(d.getDate()) + " " + PAD2(d.getHours()) + ":" + PAD2(d.getMinutes()) + ":" + PAD2(d.getSeconds());
|
|
367
|
-
}
|
|
368
|
-
function formatTimeLedger(iso) {
|
|
369
|
-
if (!iso) return UNKNOWN_VALUE;
|
|
370
|
-
const d = new Date(iso);
|
|
371
|
-
if (Number.isNaN(d.getTime())) return UNKNOWN_VALUE;
|
|
372
|
-
return d.getFullYear() + "/" + PAD2(d.getMonth() + 1) + "/" + PAD2(d.getDate()) + " " + PAD2(d.getHours()) + ":" + PAD2(d.getMinutes()) + ":" + PAD2(d.getSeconds());
|
|
373
|
-
}
|
|
374
|
-
function formatRouteSwitchTime(iso) { return formatTimeCompact(iso); }
|
|
375
|
-
function formatBalanceAmount(usdMicros, currency) {
|
|
376
|
-
if (!Number.isFinite(usdMicros)) return UNKNOWN_VALUE;
|
|
377
|
-
const amount = usdMicros / 1000000;
|
|
378
|
-
const digits = Math.abs(amount) >= 100 ? 0 : 2;
|
|
379
|
-
const code = (currency || "USD").toUpperCase();
|
|
380
|
-
return code + " " + amount.toFixed(digits);
|
|
381
|
-
}
|
|
382
|
-
function formatSellerCapacity(used, limit) {
|
|
383
|
-
if (!Number.isFinite(used) && !Number.isFinite(limit)) return UNKNOWN_VALUE;
|
|
384
|
-
const u = Number.isFinite(used) ? Math.round(used) : UNKNOWN_VALUE;
|
|
385
|
-
const l = Number.isFinite(limit) ? Math.round(limit) : UNKNOWN_VALUE;
|
|
386
|
-
return u + " / " + l;
|
|
387
|
-
}
|
|
388
|
-
function formatSpeed(value) {
|
|
389
|
-
if (!Number.isFinite(value)) return UNKNOWN_VALUE;
|
|
390
|
-
return value.toFixed(1) + " tok/s";
|
|
391
|
-
}
|
|
392
|
-
const SELLER_STATUS_MAP = {
|
|
393
|
-
active: "ok", healthy: "ok", online: "online", configured: "configured",
|
|
394
|
-
pending: "pending", draining: "degraded", degraded: "degraded",
|
|
395
|
-
busy_capacity: "degraded", offline: "error", unhealthy: "error",
|
|
396
|
-
error: "error", auth_unknown: "unknown", unknown: "unknown"
|
|
397
|
-
};
|
|
398
|
-
function formatSellerStatus(status) {
|
|
399
|
-
const key = String(status || "unknown").trim().toLowerCase().replace(/-/g, "_");
|
|
400
|
-
return SELLER_STATUS_MAP[key] || "unknown";
|
|
401
|
-
}
|
|
402
|
-
function sellerStatusTone(status) { return statusTone(formatSellerStatus(status)); }
|
|
403
|
-
window.__tbFmt = {
|
|
404
|
-
UNKNOWN_VALUE, formatTokenCount, formatTokenPair, formatCount, formatMoney, formatMoneyPair,
|
|
405
|
-
formatDuration, formatPercent, formatDiscountRatio, formatPriceMicrosPerMillion, formatPricePair,
|
|
406
|
-
normalizeStatusLabel, statusTone, formatHash, formatSellerId, formatTimeCompact, formatTimeFull,
|
|
407
|
-
formatTimeLedger, formatRouteSwitchTime, formatBalanceAmount, formatSellerCapacity, formatSpeed,
|
|
408
|
-
formatSellerStatus, sellerStatusTone
|
|
409
|
-
};
|
|
410
|
-
})();`;
|
|
411
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { ConfigManager } from "./config.js";
|
|
2
|
-
import { buildAdminCli } from "./cli.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* admin CLI 入口:构造 `ConfigManager`、绑定 commander program、同步解析 argv。
|
|
6
|
-
*/
|
|
7
|
-
export function run() {
|
|
8
|
-
const configManager = new ConfigManager();
|
|
9
|
-
const program = buildAdminCli(configManager);
|
|
10
|
-
program.parse(process.argv);
|
|
11
|
-
}
|
package/src/provider.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step 12 (provider-agnostic seller management):
|
|
3
|
-
* 抽象 seller provider 接口, 让 tb-admin CLI + UI 不再 hardcode
|
|
4
|
-
* Fly.io 调用。
|
|
5
|
-
*
|
|
6
|
-
* 设计原则 (跟用户 ZCG 在 2026-06-11 凌晨讨论):
|
|
7
|
-
* - 一个 `SellerProvider` 实现对应一种底层平台 (Fly.io / mock / ...).
|
|
8
|
-
* - 5 个核心操作: listApps / statusApp / createApp / deployApp / removeApp.
|
|
9
|
-
* - 所有方法都接受 dryRun, 都在 provider config 缺失必填字段时抛错.
|
|
10
|
-
* - 所有方法都支持 `json: true` 输出, 方便 UI / 测试 / 自动化调用.
|
|
11
|
-
* - Provider 通过 `ProviderRegistry` 注册, 默认 'fly' 实现是
|
|
12
|
-
* `FlyProvider` (从 server-cmd.ts 拆出), 'mock' 是测试实现.
|
|
13
|
-
* - 调用方 (CLI / UI / 其他) 都通过 `ProviderRegistry.get(name)` 取.
|
|
14
|
-
* - `tb-admin seller <cmd>` CLI 子命令全部加 `--provider <name>`
|
|
15
|
-
* (默认 'fly') + `--json` flag.
|
|
16
|
-
*
|
|
17
|
-
* 注意: Fly 字段 (`flyctl_path` / `default_region` / ...) 仍放在
|
|
18
|
-
* `SellerProviderConfig` 里, 不引入新的 provider 专用 config 字段
|
|
19
|
-
* (避免拆得过细). 未来加 K8s / 自建集群 provider 时, 把 `flyctl_path`
|
|
20
|
-
* 等字段移到 `FlyConfig` 子接口, 让 `SellerProviderConfig` 持有
|
|
21
|
-
* 通用字段 (default_region / default_image / volume_* 跨 provider 通用).
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import type { SellerProviderConfig } from "./config.js";
|
|
25
|
-
|
|
26
|
-
/** Dry-run / json 通用选项, 所有方法都接受 */
|
|
27
|
-
export interface ProviderCallOptions {
|
|
28
|
-
dryRun?: boolean;
|
|
29
|
-
/** 静默: 不打 [Fly.io] / [Mock] 之类的人类可读日志, 只输出 json */
|
|
30
|
-
silent?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Provider 必须能列出所有它知道的应用, 跨 provider 字段对齐 */
|
|
34
|
-
export interface SellerAppEntry {
|
|
35
|
-
/** Provider 内的应用 id (Fly 跟 K8s 各自定义) */
|
|
36
|
-
name: string;
|
|
37
|
-
/** 状态 (active / suspended / deployed / pending / unknown). 各 provider 各自定义枚举. */
|
|
38
|
-
status: string;
|
|
39
|
-
/** Provider 内部 id, 跨 provider 不必有意义. UI 用它跳转详情. */
|
|
40
|
-
providerId: string;
|
|
41
|
-
/** Region / zone / namespace. */
|
|
42
|
-
region?: string;
|
|
43
|
-
/** 上次部署时间 ISO 字符串. */
|
|
44
|
-
lastDeployAt?: string;
|
|
45
|
-
/** provider 特有的元数据 (machines count / image / 等) */
|
|
46
|
-
metadata?: Record<string, unknown>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Provider 创建入参. 字段尽量 provider-agnostic; 私有字段放 `providerOptions` */
|
|
50
|
-
export interface SellerCreateInput {
|
|
51
|
-
name: string;
|
|
52
|
-
region?: string;
|
|
53
|
-
image?: string;
|
|
54
|
-
operatorSecret: string;
|
|
55
|
-
initialConfigPath?: string;
|
|
56
|
-
volumeName?: string;
|
|
57
|
-
volumeSizeGb?: number;
|
|
58
|
-
volumeId?: string;
|
|
59
|
-
volumeSnapshotRetentionDays?: number;
|
|
60
|
-
dryRun?: boolean;
|
|
61
|
-
/** provider 私有扩展 (FlyConfig / K8sConfig / ...) */
|
|
62
|
-
providerOptions?: Record<string, unknown>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Provider deploy 入参 */
|
|
66
|
-
export interface SellerDeployInput {
|
|
67
|
-
app: string;
|
|
68
|
-
image?: string;
|
|
69
|
-
dryRun?: boolean;
|
|
70
|
-
providerOptions?: Record<string, unknown>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Provider 通用 5 操作接口 */
|
|
74
|
-
export interface SellerProvider {
|
|
75
|
-
/** provider 名 ('fly' / 'mock' / 'k8s' 等), 用于 `tb-admin --provider <name>` 选择 */
|
|
76
|
-
readonly name: string;
|
|
77
|
-
|
|
78
|
-
/** 检测 provider runtime 是否装好 (flyctl / kubectl / etc). CLI 启动时调用一次. */
|
|
79
|
-
isAvailable(): boolean;
|
|
80
|
-
|
|
81
|
-
/** List all apps this provider manages. */
|
|
82
|
-
listApps(options?: ProviderCallOptions): Promise<SellerAppEntry[]>;
|
|
83
|
-
|
|
84
|
-
/** Get one app's status (machine state / region / last deploy). */
|
|
85
|
-
statusApp(name: string, options?: ProviderCallOptions): Promise<SellerAppEntry>;
|
|
86
|
-
|
|
87
|
-
/** Create + initial deploy. `dryRun` 模式只输出计划. */
|
|
88
|
-
createApp(input: SellerCreateInput): Promise<{ ok: true; app: string; dryRun: boolean }>;
|
|
89
|
-
|
|
90
|
-
/** Update image on existing app. */
|
|
91
|
-
deployApp(input: SellerDeployInput): Promise<{ ok: true; app: string; machines: string[]; dryRun: boolean }>;
|
|
92
|
-
|
|
93
|
-
/** Destroy app. */
|
|
94
|
-
removeApp(name: string, options?: ProviderCallOptions): Promise<{ ok: true; app: string; dryRun: boolean }>;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Provider registry: CLI / UI / 测试都通过 `ProviderRegistry.get(name)`
|
|
99
|
-
* 拿 provider. 构造时塞一份 SellerProviderConfig, 跟现有
|
|
100
|
-
* `configManager.getSellerProvider(name)` 同源.
|
|
101
|
-
*/
|
|
102
|
-
export class ProviderRegistry {
|
|
103
|
-
private providers = new Map<string, SellerProvider>();
|
|
104
|
-
|
|
105
|
-
constructor(private configManager: { getSellerProvider(name?: string): SellerProviderConfig | undefined }) {}
|
|
106
|
-
|
|
107
|
-
/** 注册一个 provider. 测试场景用. */
|
|
108
|
-
register(provider: SellerProvider): void {
|
|
109
|
-
this.providers.set(provider.name, provider);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** 取 provider. 默认 'fly'. 不存在抛错 (CLI 应该 fail fast). */
|
|
113
|
-
get(name: string = "fly"): SellerProvider {
|
|
114
|
-
const registered = this.providers.get(name);
|
|
115
|
-
if (registered) {
|
|
116
|
-
return registered;
|
|
117
|
-
}
|
|
118
|
-
// 还没注册 -> 现造一个 (FlyProvider / MockProvider 各自 lazy 构造)
|
|
119
|
-
const config = this.configManager.getSellerProvider(name);
|
|
120
|
-
if (!config) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`provider '${name}' is not configured. ` +
|
|
123
|
-
`Run \`tb-admin config set <profile>\` first, or add a [seller_providers.${name}] section.`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const instance = createDefaultProvider(name, config);
|
|
127
|
-
this.providers.set(name, instance);
|
|
128
|
-
return instance;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** 列出所有已注册 provider 名. */
|
|
132
|
-
list(): string[] {
|
|
133
|
-
return Array.from(this.providers.keys());
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 默认 provider 工厂. 'fly' -> FlyProvider.
|
|
139
|
-
* 任何未知 name 抛错. 通过 import 避免循环依赖, 所以放在文件底部.
|
|
140
|
-
*/
|
|
141
|
-
export function createDefaultProvider(name: string, config: SellerProviderConfig): SellerProvider {
|
|
142
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
143
|
-
const { FlyProvider } = require("./server-cmd.js") as typeof import("./server-cmd.js");
|
|
144
|
-
if (name === "fly") {
|
|
145
|
-
// Step 14 之前 FlyProvider 暂不 implements SellerProvider (保留 1.0.31 行为 1:1).
|
|
146
|
-
// 这里通过 unknown 桥接; 真用时调用方应通过 SellerCommandRunner 而不是直接用 ProviderRegistry.
|
|
147
|
-
return new FlyProvider(config) as unknown as SellerProvider;
|
|
148
|
-
}
|
|
149
|
-
throw new Error(`unknown provider name: '${name}' (built-in: fly)`);
|
|
150
|
-
}
|