opencode-go-usage-tui 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -6
- package/README_EN.md +38 -6
- package/dist/tui.js +897 -57
- package/package.json +1 -1
- package/src/i18n.ts +88 -0
- package/src/index.tsx +420 -44
- package/src/pricing.ts +348 -0
package/dist/tui.js
CHANGED
|
@@ -9,8 +9,8 @@ import { insertNode as _$insertNode } from "@opentui/solid";
|
|
|
9
9
|
import { insert as _$insert } from "@opentui/solid";
|
|
10
10
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
11
11
|
import { createSignal, createEffect, onMount, onCleanup, Show } from "solid-js";
|
|
12
|
-
import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "node:fs";
|
|
13
|
-
import { join, dirname } from "node:path";
|
|
12
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, renameSync, existsSync as existsSync2 } from "node:fs";
|
|
13
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
14
14
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
15
15
|
|
|
16
16
|
// src/i18n.ts
|
|
@@ -49,7 +49,51 @@ var ZH_T = {
|
|
|
49
49
|
onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
|
|
50
50
|
onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
|
|
51
51
|
onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
|
|
52
|
-
setupCanceled: "\u5DF2\u53D6\u6D88\u914D\u7F6E\uFF0C\u8F93\u5165 /go-config \u53EF\u91CD\u65B0\u8BBE\u7F6E"
|
|
52
|
+
setupCanceled: "\u5DF2\u53D6\u6D88\u914D\u7F6E\uFF0C\u8F93\u5165 /go-config \u53EF\u91CD\u65B0\u8BBE\u7F6E",
|
|
53
|
+
// ── 模型价格表 ──
|
|
54
|
+
priceTitle: "\u6A21\u578B\u4EF7\u683C",
|
|
55
|
+
priceLoading: "\u6293\u53D6\u4EF7\u683C\u4E2D...",
|
|
56
|
+
priceError: "\u4EF7\u683C\u6293\u53D6\u5931\u8D25",
|
|
57
|
+
priceUpdated: "\u66F4\u65B0\u65F6\u95F4",
|
|
58
|
+
priceChanged: "\u4EF7\u683C\u6709\u53D8\u5316",
|
|
59
|
+
priceNoChange: "\u65E0\u53D8\u5316",
|
|
60
|
+
priceChangesSummary: "{t} \u9879\u53D8\u5316\uFF1A{a} \u65B0\u589E / {r} \u4E0B\u67B6 / {p} \u4EF7\u683C / {l} \u9650\u989D",
|
|
61
|
+
priceVariantTag: "{n}\u4E2A\u6863\u4F4D",
|
|
62
|
+
priceInput: "\u8F93\u5165",
|
|
63
|
+
priceOutput: "\u8F93\u51FA",
|
|
64
|
+
priceCacheRead: "\u7F13\u5B58\u8BFB",
|
|
65
|
+
priceCacheWrite: "\u7F13\u5B58\u5199",
|
|
66
|
+
priceUsage: "\u6708\u9650\u989D",
|
|
67
|
+
priceReq5h: "5h\u8BF7\u6C42",
|
|
68
|
+
priceReqWeek: "\u5468\u8BF7\u6C42",
|
|
69
|
+
priceReqMonth: "\u6708\u8BF7\u6C42",
|
|
70
|
+
priceModelCol: "\u6A21\u578B",
|
|
71
|
+
priceReqs: "\u8BF7\u6C42\u6570(5h/\u5468/\u6708)",
|
|
72
|
+
priceSdk: "SDK",
|
|
73
|
+
priceDiffUp: "\u2191{v}",
|
|
74
|
+
priceDiffDown: "\u2193{v}",
|
|
75
|
+
priceDiffSame: "=",
|
|
76
|
+
priceChartTitle: "\u4EF7\u683C\u53D8\u5316\u8BB0\u5F55",
|
|
77
|
+
priceChartEmpty: "\u6682\u65E0\u53D8\u5316\u8BB0\u5F55",
|
|
78
|
+
priceChartAdded: "\u65B0\u589E: {n}",
|
|
79
|
+
priceChartChanged: "\u53D8\u5316: {n}",
|
|
80
|
+
priceChartRemoved: "\u79FB\u9664: {n}",
|
|
81
|
+
priceCollapse: "\u4EF7\u683C\u8868",
|
|
82
|
+
priceExpand: "\u4EF7\u683C\u8868",
|
|
83
|
+
settingsTitle: "Go \u7528\u91CF\u8BBE\u7F6E",
|
|
84
|
+
settingsDesc: "\u5173\u6CE8\u6A21\u578B\u3001\u5237\u65B0\u95F4\u9694\u8BBE\u7F6E",
|
|
85
|
+
settingsFocus: "\u5173\u6CE8\u7684\u6A21\u578B",
|
|
86
|
+
settingsRefresh: "\u5237\u65B0\u95F4\u9694 (\u79D2)",
|
|
87
|
+
settingsRefreshUsage: "\u7528\u91CF\u5237\u65B0\u95F4\u9694 (\u79D2)",
|
|
88
|
+
settingsRefreshPricing: "\u4EF7\u683C\u5237\u65B0\u95F4\u9694 (\u79D2)",
|
|
89
|
+
settingsSaved: "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
|
|
90
|
+
settingsCanceled: "\u5DF2\u53D6\u6D88\u8BBE\u7F6E",
|
|
91
|
+
focusAll: "\u5168\u90E8\u9009\u62E9",
|
|
92
|
+
focusNone: "\u6E05\u7A7A\u9009\u62E9",
|
|
93
|
+
focusModelsHint: "\u9009\u62E9\u8981\u5173\u6CE8\u7684\u6A21\u578B\uFF08\u7559\u7A7A\u663E\u793A\u5168\u90E8\uFF09",
|
|
94
|
+
focusDone: "\u5B8C\u6210",
|
|
95
|
+
focusToggle: "\u5207\u6362\u9009\u4E2D",
|
|
96
|
+
focusSelected: "\u5DF2\u9009 {n}"
|
|
53
97
|
};
|
|
54
98
|
var EN_T = {
|
|
55
99
|
panelTitle: "Go Usage",
|
|
@@ -86,7 +130,51 @@ var EN_T = {
|
|
|
86
130
|
onboardLangDesc: "Choose display language / \u9009\u62E9\u663E\u793A\u8BED\u8A00",
|
|
87
131
|
onboardConfigTitle: "Welcome to Go Usage / \u6B22\u8FCE\u4F7F\u7528 Go \u7528\u91CF",
|
|
88
132
|
onboardConfigDesc: "First-time setup / \u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\u8D26\u53F7",
|
|
89
|
-
setupCanceled: "Setup canceled, run /go-config to reconfigure"
|
|
133
|
+
setupCanceled: "Setup canceled, run /go-config to reconfigure",
|
|
134
|
+
// ── 模型价格表 ──
|
|
135
|
+
priceTitle: "Model Pricing",
|
|
136
|
+
priceLoading: "Fetching pricing...",
|
|
137
|
+
priceError: "Pricing fetch failed",
|
|
138
|
+
priceUpdated: "Pricing updated",
|
|
139
|
+
priceChanged: "Pricing changed",
|
|
140
|
+
priceNoChange: "No changes",
|
|
141
|
+
priceChangesSummary: "{t} change(s): {a} added / {r} removed / {p} price / {l} limit",
|
|
142
|
+
priceVariantTag: "{n} tiers",
|
|
143
|
+
priceInput: "In",
|
|
144
|
+
priceOutput: "Out",
|
|
145
|
+
priceCacheRead: "Cache In",
|
|
146
|
+
priceCacheWrite: "Cache Wr",
|
|
147
|
+
priceUsage: "Usage",
|
|
148
|
+
priceReq5h: "5h req",
|
|
149
|
+
priceReqWeek: "Wk req",
|
|
150
|
+
priceReqMonth: "Mo req",
|
|
151
|
+
priceModelCol: "Model",
|
|
152
|
+
priceReqs: "Requests (5h/wk/mo)",
|
|
153
|
+
priceSdk: "SDK",
|
|
154
|
+
priceDiffUp: "\u2191{v}",
|
|
155
|
+
priceDiffDown: "\u2193{v}",
|
|
156
|
+
priceDiffSame: "=",
|
|
157
|
+
priceChartTitle: "Price change history",
|
|
158
|
+
priceChartEmpty: "No change history yet",
|
|
159
|
+
priceChartAdded: "Added: {n}",
|
|
160
|
+
priceChartChanged: "Changed: {n}",
|
|
161
|
+
priceChartRemoved: "Removed: {n}",
|
|
162
|
+
priceCollapse: "Pricing",
|
|
163
|
+
priceExpand: "Pricing",
|
|
164
|
+
settingsTitle: "Go Usage Settings",
|
|
165
|
+
settingsDesc: "Focused models, refresh interval settings",
|
|
166
|
+
settingsFocus: "Focused models",
|
|
167
|
+
settingsRefresh: "Refresh interval (sec)",
|
|
168
|
+
settingsRefreshUsage: "Usage refresh interval (sec)",
|
|
169
|
+
settingsRefreshPricing: "Pricing refresh interval (sec)",
|
|
170
|
+
settingsSaved: "Settings saved",
|
|
171
|
+
settingsCanceled: "Settings canceled",
|
|
172
|
+
focusAll: "Select all",
|
|
173
|
+
focusNone: "Clear selection",
|
|
174
|
+
focusModelsHint: "Select models to focus (empty shows all)",
|
|
175
|
+
focusDone: "Done",
|
|
176
|
+
focusToggle: "Toggle select",
|
|
177
|
+
focusSelected: "{n} selected"
|
|
90
178
|
};
|
|
91
179
|
var LANGS = { zh: ZH_T, en: EN_T };
|
|
92
180
|
var LANG_META = [
|
|
@@ -113,27 +201,285 @@ function detectLang() {
|
|
|
113
201
|
}
|
|
114
202
|
}
|
|
115
203
|
|
|
204
|
+
// src/pricing.ts
|
|
205
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
206
|
+
import { join, dirname } from "node:path";
|
|
207
|
+
var DOCS_URL = "https://opencode.ai/docs/go";
|
|
208
|
+
var PRICING_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
|
|
209
|
+
var PRICING_FILE = join(PRICING_DIR, "go-pricing-data.json");
|
|
210
|
+
function cleanText(html) {
|
|
211
|
+
return html.replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/ /g, " ").replace(/'|'/g, "'").trim();
|
|
212
|
+
}
|
|
213
|
+
function parsePrice(str) {
|
|
214
|
+
const s = (str || "").replace(/[$\s]/g, "");
|
|
215
|
+
if (!s || s === "-" || s === "null" || s === "\u2014" || s === "\u2013") return null;
|
|
216
|
+
const num = parseFloat(s.replace(/,/g, ""));
|
|
217
|
+
return isNaN(num) ? null : num;
|
|
218
|
+
}
|
|
219
|
+
function parseCount(str) {
|
|
220
|
+
const s = (str || "").replace(/[,\s]/g, "");
|
|
221
|
+
if (!s || s === "-" || s === "\u2014" || s === "\u2013") return 0;
|
|
222
|
+
const m = s.match(/^([\d.]+)k$/i);
|
|
223
|
+
if (m) return Math.round(parseFloat(m[1]) * 1e3);
|
|
224
|
+
const num = parseInt(s, 10);
|
|
225
|
+
return isNaN(num) ? 0 : num;
|
|
226
|
+
}
|
|
227
|
+
function normalizeName(name) {
|
|
228
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
229
|
+
}
|
|
230
|
+
function generateId(name) {
|
|
231
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
232
|
+
}
|
|
233
|
+
function extractTables(html) {
|
|
234
|
+
const tables = [];
|
|
235
|
+
const re = /<table[^>]*>([\s\S]*?)<\/table>/gi;
|
|
236
|
+
let m;
|
|
237
|
+
while ((m = re.exec(html)) !== null) tables.push(m[1]);
|
|
238
|
+
return tables;
|
|
239
|
+
}
|
|
240
|
+
function extractRows(tableHtml) {
|
|
241
|
+
const rows = [];
|
|
242
|
+
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
243
|
+
let rm;
|
|
244
|
+
while ((rm = rowRe.exec(tableHtml)) !== null) {
|
|
245
|
+
const cells = [];
|
|
246
|
+
const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
|
|
247
|
+
let cm;
|
|
248
|
+
while ((cm = cellRe.exec(rm[1])) !== null) cells.push(cleanText(cm[1]));
|
|
249
|
+
if (cells.length > 0) rows.push(cells);
|
|
250
|
+
}
|
|
251
|
+
return rows;
|
|
252
|
+
}
|
|
253
|
+
function extractLinks(html) {
|
|
254
|
+
const links = [];
|
|
255
|
+
const re = /<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
256
|
+
let m;
|
|
257
|
+
while ((m = re.exec(html)) !== null) {
|
|
258
|
+
const href = m[1];
|
|
259
|
+
if (href.startsWith("http") || href.startsWith("/")) links.push(href);
|
|
260
|
+
}
|
|
261
|
+
return links;
|
|
262
|
+
}
|
|
263
|
+
async function fetchPricing() {
|
|
264
|
+
const res = await fetch(DOCS_URL, {
|
|
265
|
+
headers: {
|
|
266
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
267
|
+
"accept": "text/html"
|
|
268
|
+
},
|
|
269
|
+
signal: AbortSignal.timeout(15e3)
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
272
|
+
const html = await res.text();
|
|
273
|
+
const tables = extractTables(html);
|
|
274
|
+
if (tables.length < 4) throw new Error("Required tables not found");
|
|
275
|
+
const priceTable = tables[1];
|
|
276
|
+
const limitTable = tables[0];
|
|
277
|
+
const endpointTable = tables[2];
|
|
278
|
+
const retentionTable = tables[3];
|
|
279
|
+
const priceRows = extractRows(priceTable);
|
|
280
|
+
const limitRows = extractRows(limitTable);
|
|
281
|
+
const endpointRows = extractRows(endpointTable);
|
|
282
|
+
const retentionRows = extractRows(retentionTable);
|
|
283
|
+
const limitMap = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const row of limitRows.slice(1)) {
|
|
285
|
+
if (row.length >= 4) limitMap.set(normalizeName(row[0]), row);
|
|
286
|
+
}
|
|
287
|
+
const endpointMap = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const row of endpointRows.slice(1)) {
|
|
289
|
+
if (row.length >= 4) endpointMap.set(normalizeName(row[0]), row);
|
|
290
|
+
}
|
|
291
|
+
const retentionMap = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const row of retentionRows.slice(1)) {
|
|
293
|
+
if (row.length >= 3) retentionMap.set(normalizeName(row[0]), row);
|
|
294
|
+
}
|
|
295
|
+
const priceGroupMap = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const row of priceRows.slice(1)) {
|
|
297
|
+
if (row.length < 5) continue;
|
|
298
|
+
const rawName = row[0];
|
|
299
|
+
const m = rawName.match(/^(.*?)\s*[((]\s*([^(()]*?)\s*[))]\s*$/);
|
|
300
|
+
const baseName = m ? m[1].trim() : rawName.trim();
|
|
301
|
+
const variant = m ? m[2].trim() : "";
|
|
302
|
+
const key = normalizeName(baseName);
|
|
303
|
+
if (!priceGroupMap.has(key)) priceGroupMap.set(key, []);
|
|
304
|
+
priceGroupMap.get(key).push({ baseName, variant, row });
|
|
305
|
+
}
|
|
306
|
+
const models = [];
|
|
307
|
+
for (const row of limitRows.slice(1)) {
|
|
308
|
+
if (row.length < 4) continue;
|
|
309
|
+
const name = row[0];
|
|
310
|
+
const key = normalizeName(name);
|
|
311
|
+
const id = generateId(name);
|
|
312
|
+
const group = priceGroupMap.get(key);
|
|
313
|
+
const endpointRow = endpointMap.get(key);
|
|
314
|
+
const retentionRow = retentionMap.get(key);
|
|
315
|
+
const variants = (group ?? []).map((g) => ({
|
|
316
|
+
label: g.variant || "default",
|
|
317
|
+
pricing: {
|
|
318
|
+
input: parsePrice(g.row[1]),
|
|
319
|
+
output: parsePrice(g.row[2]),
|
|
320
|
+
cachedRead: parsePrice(g.row[3]),
|
|
321
|
+
cachedWrite: g.row.length > 4 ? parsePrice(g.row[4]) : null
|
|
322
|
+
}
|
|
323
|
+
}));
|
|
324
|
+
const allInputs = variants.map((v) => v.pricing.input).filter((v) => v !== null);
|
|
325
|
+
const allOutputs = variants.map((v) => v.pricing.output).filter((v) => v !== null);
|
|
326
|
+
const allReads = variants.map((v) => v.pricing.cachedRead).filter((v) => v !== null);
|
|
327
|
+
const allWrites = variants.map((v) => v.pricing.cachedWrite).filter((v) => v !== null);
|
|
328
|
+
models.push({
|
|
329
|
+
id,
|
|
330
|
+
name,
|
|
331
|
+
variants,
|
|
332
|
+
pricing: {
|
|
333
|
+
input: variants.length === 1 ? variants[0].pricing.input : allInputs.length ? Math.min(...allInputs) : null,
|
|
334
|
+
output: variants.length === 1 ? variants[0].pricing.output : allOutputs.length ? Math.min(...allOutputs) : null,
|
|
335
|
+
cachedRead: variants.length === 1 ? variants[0].pricing.cachedRead : allReads.length ? Math.min(...allReads) : null,
|
|
336
|
+
cachedWrite: variants.length === 1 ? variants[0].pricing.cachedWrite : allWrites.length ? Math.min(...allWrites) : null
|
|
337
|
+
},
|
|
338
|
+
limits: {
|
|
339
|
+
fiveHour: parseCount(row[1]),
|
|
340
|
+
weekly: parseCount(row[2]),
|
|
341
|
+
monthly: parseCount(row[3])
|
|
342
|
+
},
|
|
343
|
+
usageLimit: group && group[0].row.length > 5 ? parsePrice(group[0].row[5]) : null,
|
|
344
|
+
endpoint: endpointRow ? extractLinks(endpointRow.join(" "))[0] || "" : "",
|
|
345
|
+
sdk: endpointRow ? endpointRow[3] || "" : "",
|
|
346
|
+
training: retentionRow ? retentionRow[1] || "" : "",
|
|
347
|
+
retention: retentionRow ? retentionRow[2] || "" : ""
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return { fetchTime: (/* @__PURE__ */ new Date()).toISOString(), models };
|
|
351
|
+
}
|
|
352
|
+
function comparePricing(oldData, newData) {
|
|
353
|
+
const changes = [];
|
|
354
|
+
if (!oldData || oldData.models.length === 0) {
|
|
355
|
+
return {
|
|
356
|
+
hasChanges: true,
|
|
357
|
+
changes: newData.models.map((m) => ({ type: "added", modelId: m.id, modelName: m.name, newValue: m })),
|
|
358
|
+
summary: { total: newData.models.length, added: newData.models.length, removed: 0, priceChanges: 0, limitChanges: 0 }
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const oldIndex = new Map(oldData.models.map((m) => [m.id, m]));
|
|
362
|
+
for (const newModel of newData.models) {
|
|
363
|
+
const oldModel = oldIndex.get(newModel.id);
|
|
364
|
+
if (!oldModel) {
|
|
365
|
+
changes.push({ type: "added", modelId: newModel.id, modelName: newModel.name, newValue: newModel });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const priceChanges = [];
|
|
369
|
+
const limitChanges = [];
|
|
370
|
+
for (const field of ["input", "output", "cachedRead", "cachedWrite"]) {
|
|
371
|
+
if (oldModel.pricing[field] !== newModel.pricing[field]) {
|
|
372
|
+
priceChanges.push({
|
|
373
|
+
field: `pricing.${field}`,
|
|
374
|
+
oldValue: oldModel.pricing[field],
|
|
375
|
+
newValue: newModel.pricing[field],
|
|
376
|
+
changeType: changeType(oldModel.pricing[field], newModel.pricing[field])
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (priceChanges.length > 0) {
|
|
381
|
+
changes.push({ type: "pricing", modelId: newModel.id, modelName: newModel.name, changes: priceChanges });
|
|
382
|
+
}
|
|
383
|
+
for (const field of ["fiveHour", "weekly", "monthly"]) {
|
|
384
|
+
if (oldModel.limits[field] !== newModel.limits[field]) {
|
|
385
|
+
limitChanges.push({
|
|
386
|
+
field: `limits.${field}`,
|
|
387
|
+
oldValue: oldModel.limits[field],
|
|
388
|
+
newValue: newModel.limits[field],
|
|
389
|
+
changeType: changeType(oldModel.limits[field], newModel.limits[field])
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (limitChanges.length > 0) {
|
|
394
|
+
changes.push({ type: "limits", modelId: newModel.id, modelName: newModel.name, changes: limitChanges });
|
|
395
|
+
}
|
|
396
|
+
if (oldModel.usageLimit !== newModel.usageLimit) {
|
|
397
|
+
changes.push({
|
|
398
|
+
type: "usageLimit",
|
|
399
|
+
modelId: newModel.id,
|
|
400
|
+
modelName: newModel.name,
|
|
401
|
+
changes: [{
|
|
402
|
+
field: "usageLimit",
|
|
403
|
+
oldValue: oldModel.usageLimit,
|
|
404
|
+
newValue: newModel.usageLimit,
|
|
405
|
+
changeType: changeType(oldModel.usageLimit, newModel.usageLimit)
|
|
406
|
+
}]
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
oldIndex.delete(newModel.id);
|
|
410
|
+
}
|
|
411
|
+
for (const [id, oldModel] of oldIndex) {
|
|
412
|
+
changes.push({ type: "removed", modelId: id, modelName: oldModel.name, oldValue: oldModel });
|
|
413
|
+
}
|
|
414
|
+
const summary = { total: changes.length, added: 0, removed: 0, priceChanges: 0, limitChanges: 0 };
|
|
415
|
+
for (const c of changes) {
|
|
416
|
+
if (c.type === "added") summary.added++;
|
|
417
|
+
else if (c.type === "removed") summary.removed++;
|
|
418
|
+
else if (c.type === "pricing") summary.priceChanges++;
|
|
419
|
+
else if (c.type === "limits" || c.type === "usageLimit") summary.limitChanges++;
|
|
420
|
+
}
|
|
421
|
+
return { hasChanges: changes.length > 0, changes, summary };
|
|
422
|
+
}
|
|
423
|
+
function changeType(oldVal, newVal) {
|
|
424
|
+
if (oldVal === null && newVal !== null) return "added";
|
|
425
|
+
if (oldVal !== null && newVal === null) return "removed";
|
|
426
|
+
if (newVal !== null && oldVal !== null && newVal > oldVal) return "increase";
|
|
427
|
+
if (newVal !== null && oldVal !== null && newVal < oldVal) return "decrease";
|
|
428
|
+
return "unchanged";
|
|
429
|
+
}
|
|
430
|
+
function loadPricingStore() {
|
|
431
|
+
try {
|
|
432
|
+
const data = JSON.parse(readFileSync(PRICING_FILE, "utf8"));
|
|
433
|
+
if (data && Array.isArray(data.current?.models)) return data;
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
return { lastFetch: "", current: { fetchTime: "", models: [] }, previous: null, history: [] };
|
|
437
|
+
}
|
|
438
|
+
function savePricingStore(store) {
|
|
439
|
+
try {
|
|
440
|
+
mkdirSync(dirname(PRICING_FILE), { recursive: true });
|
|
441
|
+
writeFileSync(PRICING_FILE, JSON.stringify(store, null, 2), "utf8");
|
|
442
|
+
} catch {
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function updatePricingStore(newData) {
|
|
446
|
+
const store = loadPricingStore();
|
|
447
|
+
const compare = comparePricing(store.current.models.length > 0 ? store.current : null, newData);
|
|
448
|
+
const prev = store.current;
|
|
449
|
+
const newStore = {
|
|
450
|
+
lastFetch: newData.fetchTime,
|
|
451
|
+
current: newData,
|
|
452
|
+
previous: prev.models.length > 0 ? prev : null,
|
|
453
|
+
history: [
|
|
454
|
+
{ fetchTime: newData.fetchTime, changes: compare.changes, summary: compare.summary },
|
|
455
|
+
...store.history
|
|
456
|
+
].slice(0, 10)
|
|
457
|
+
};
|
|
458
|
+
savePricingStore(newStore);
|
|
459
|
+
return newStore;
|
|
460
|
+
}
|
|
461
|
+
|
|
116
462
|
// src/index.tsx
|
|
117
|
-
var VERSION = "1.
|
|
463
|
+
var VERSION = "1.3.0";
|
|
118
464
|
var CHECK_INTERVAL = Number(process?.env?.OPENCODE_GO_CHECK_INTERVAL ?? 6e4);
|
|
119
465
|
var WARN_THRESHOLD = Number(process?.env?.OPENCODE_GO_WARN_THRESHOLD ?? 0.8);
|
|
120
466
|
var CONFIG_DIR = process?.env?.OPENCODE_CONFIG_DIR || (process?.env?.USERPROFILE ? `${process.env.USERPROFILE}\\.config\\opencode` : "") || process?.env?.HOME + "/.config/opencode";
|
|
121
|
-
var CONFIG_FILE =
|
|
122
|
-
var COOKIE_ENC_FILE =
|
|
123
|
-
var KEY_FILE =
|
|
467
|
+
var CONFIG_FILE = join2(CONFIG_DIR, "go-usage-config.json");
|
|
468
|
+
var COOKIE_ENC_FILE = join2(CONFIG_DIR, "go-auth-cookie.enc");
|
|
469
|
+
var KEY_FILE = join2(CONFIG_DIR, ".encryption-key");
|
|
124
470
|
var COOKIE_PLAIN_FILE = `${CONFIG_DIR}\\go-auth-cookie.txt`;
|
|
125
471
|
var KV_PREFIX = "go-usage";
|
|
126
472
|
function loadEncryptionKey() {
|
|
127
473
|
try {
|
|
128
|
-
const hex2 =
|
|
474
|
+
const hex2 = readFileSync2(KEY_FILE, "utf8").trim();
|
|
129
475
|
if (hex2) return Buffer.from(hex2, "hex");
|
|
130
476
|
} catch {
|
|
131
477
|
}
|
|
132
478
|
const key = randomBytes(32);
|
|
133
|
-
|
|
479
|
+
mkdirSync2(dirname2(KEY_FILE), {
|
|
134
480
|
recursive: true
|
|
135
481
|
});
|
|
136
|
-
|
|
482
|
+
writeFileSync2(KEY_FILE, key.toString("hex"), {
|
|
137
483
|
encoding: "utf8",
|
|
138
484
|
mode: 384
|
|
139
485
|
});
|
|
@@ -164,7 +510,7 @@ function decryptText(encJson) {
|
|
|
164
510
|
}
|
|
165
511
|
function readJsonFile(file) {
|
|
166
512
|
try {
|
|
167
|
-
return JSON.parse(
|
|
513
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
168
514
|
} catch {
|
|
169
515
|
return null;
|
|
170
516
|
}
|
|
@@ -175,16 +521,26 @@ function loadConfig() {
|
|
|
175
521
|
if (env.OPENCODE_GO_AUTH_COOKIE) {
|
|
176
522
|
return {
|
|
177
523
|
workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
|
|
178
|
-
authCookie: env.OPENCODE_GO_AUTH_COOKIE
|
|
524
|
+
authCookie: env.OPENCODE_GO_AUTH_COOKIE,
|
|
525
|
+
usagePanel: fileCfg.ui?.usage_panel ?? true,
|
|
526
|
+
pricePanel: fileCfg.ui?.price_panel ?? true,
|
|
527
|
+
focusModels: fileCfg.focus_models ?? [],
|
|
528
|
+
refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
|
|
529
|
+
pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
|
|
179
530
|
};
|
|
180
531
|
}
|
|
181
532
|
try {
|
|
182
|
-
if (
|
|
183
|
-
const cookie = decryptText(
|
|
533
|
+
if (existsSync2(COOKIE_ENC_FILE)) {
|
|
534
|
+
const cookie = decryptText(readFileSync2(COOKIE_ENC_FILE, "utf8"));
|
|
184
535
|
if (cookie) {
|
|
185
536
|
return {
|
|
186
537
|
workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
|
|
187
|
-
authCookie: cookie
|
|
538
|
+
authCookie: cookie,
|
|
539
|
+
usagePanel: fileCfg.ui?.usage_panel ?? true,
|
|
540
|
+
pricePanel: fileCfg.ui?.price_panel ?? true,
|
|
541
|
+
focusModels: fileCfg.focus_models ?? [],
|
|
542
|
+
refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
|
|
543
|
+
pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
|
|
188
544
|
};
|
|
189
545
|
}
|
|
190
546
|
}
|
|
@@ -192,38 +548,48 @@ function loadConfig() {
|
|
|
192
548
|
}
|
|
193
549
|
const legacyCookie = fileCfg.auth_cookie || fileCfg.cookie || (() => {
|
|
194
550
|
try {
|
|
195
|
-
return
|
|
551
|
+
return readFileSync2(COOKIE_PLAIN_FILE, "utf8").trim();
|
|
196
552
|
} catch {
|
|
197
553
|
return "";
|
|
198
554
|
}
|
|
199
555
|
})();
|
|
200
556
|
if (legacyCookie) {
|
|
201
557
|
const enc = encryptText(legacyCookie);
|
|
202
|
-
|
|
558
|
+
mkdirSync2(CONFIG_DIR, {
|
|
203
559
|
recursive: true
|
|
204
560
|
});
|
|
205
|
-
|
|
561
|
+
writeFileSync2(COOKIE_ENC_FILE, enc, "utf8");
|
|
206
562
|
const cleanCfg = {
|
|
207
563
|
...fileCfg
|
|
208
564
|
};
|
|
209
565
|
delete cleanCfg.cookie;
|
|
210
566
|
delete cleanCfg.auth_cookie;
|
|
211
567
|
try {
|
|
212
|
-
|
|
568
|
+
writeFileSync2(CONFIG_FILE, JSON.stringify(cleanCfg, null, 2), "utf8");
|
|
213
569
|
} catch {
|
|
214
570
|
}
|
|
215
571
|
try {
|
|
216
|
-
if (
|
|
572
|
+
if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
|
|
217
573
|
} catch {
|
|
218
574
|
}
|
|
219
575
|
return {
|
|
220
576
|
workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
|
|
221
|
-
authCookie: legacyCookie
|
|
577
|
+
authCookie: legacyCookie,
|
|
578
|
+
usagePanel: fileCfg.ui?.usage_panel ?? true,
|
|
579
|
+
pricePanel: fileCfg.ui?.price_panel ?? true,
|
|
580
|
+
focusModels: fileCfg.focus_models ?? [],
|
|
581
|
+
refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
|
|
582
|
+
pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
|
|
222
583
|
};
|
|
223
584
|
}
|
|
224
585
|
return {
|
|
225
586
|
workspaceId: env.OPENCODE_GO_WORKSPACE_ID || fileCfg.workspace_id || "",
|
|
226
|
-
authCookie: ""
|
|
587
|
+
authCookie: "",
|
|
588
|
+
usagePanel: fileCfg.ui?.usage_panel ?? true,
|
|
589
|
+
pricePanel: fileCfg.ui?.price_panel ?? true,
|
|
590
|
+
focusModels: fileCfg.focus_models ?? [],
|
|
591
|
+
refreshIntervalMs: (fileCfg.refresh_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3)) * 1e3,
|
|
592
|
+
pricingIntervalMs: (fileCfg.pricing_interval_sec ?? Math.round(CHECK_INTERVAL / 1e3 * 30)) * 1e3
|
|
227
593
|
};
|
|
228
594
|
}
|
|
229
595
|
function saveConfig(patch) {
|
|
@@ -231,34 +597,44 @@ function saveConfig(patch) {
|
|
|
231
597
|
if (patch.workspace_id !== void 0) fileCfg.workspace_id = patch.workspace_id;
|
|
232
598
|
if (patch.cookie !== void 0) {
|
|
233
599
|
if (patch.cookie) {
|
|
234
|
-
|
|
600
|
+
mkdirSync2(CONFIG_DIR, {
|
|
235
601
|
recursive: true
|
|
236
602
|
});
|
|
237
|
-
|
|
603
|
+
writeFileSync2(COOKIE_ENC_FILE, encryptText(patch.cookie), "utf8");
|
|
238
604
|
delete fileCfg.cookie;
|
|
239
605
|
delete fileCfg.auth_cookie;
|
|
240
606
|
try {
|
|
241
|
-
if (
|
|
607
|
+
if (existsSync2(COOKIE_PLAIN_FILE)) renameSync(COOKIE_PLAIN_FILE, COOKIE_PLAIN_FILE + ".bak");
|
|
242
608
|
} catch {
|
|
243
609
|
}
|
|
244
610
|
} else {
|
|
245
611
|
try {
|
|
246
|
-
if (
|
|
612
|
+
if (existsSync2(COOKIE_ENC_FILE)) renameSync(COOKIE_ENC_FILE, COOKIE_ENC_FILE + ".bak");
|
|
247
613
|
} catch {
|
|
248
614
|
}
|
|
249
615
|
}
|
|
250
616
|
}
|
|
617
|
+
if (patch.ui !== void 0) {
|
|
618
|
+
fileCfg.ui = {
|
|
619
|
+
...fileCfg.ui ?? {},
|
|
620
|
+
...patch.ui
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (patch.focus_models !== void 0) fileCfg.focus_models = patch.focus_models;
|
|
624
|
+
if (patch.refresh_interval_sec !== void 0) fileCfg.refresh_interval_sec = patch.refresh_interval_sec;
|
|
625
|
+
if (patch.pricing_interval_sec !== void 0) fileCfg.pricing_interval_sec = patch.pricing_interval_sec;
|
|
251
626
|
try {
|
|
252
|
-
|
|
627
|
+
mkdirSync2(dirname2(CONFIG_FILE), {
|
|
253
628
|
recursive: true
|
|
254
629
|
});
|
|
255
|
-
|
|
630
|
+
writeFileSync2(CONFIG_FILE, JSON.stringify(fileCfg, null, 2), "utf8");
|
|
256
631
|
} catch {
|
|
257
632
|
}
|
|
258
633
|
}
|
|
259
634
|
var [configTick, setConfigTick] = createSignal(0);
|
|
260
635
|
var [langCode, setLangCode] = createSignal(detectLang());
|
|
261
636
|
var t = createT(() => langCode());
|
|
637
|
+
var [sharedPricing, setSharedPricing] = createSignal(null);
|
|
262
638
|
function kvTryGet(api, key) {
|
|
263
639
|
try {
|
|
264
640
|
return api.kv.get(key);
|
|
@@ -272,7 +648,8 @@ function kvTrySet(api, key, val) {
|
|
|
272
648
|
} catch {
|
|
273
649
|
}
|
|
274
650
|
}
|
|
275
|
-
|
|
651
|
+
var REQUEST_TIMEOUT = 1e4;
|
|
652
|
+
async function fetchUsage(workspaceId, authCookie, signal) {
|
|
276
653
|
if (!workspaceId || !authCookie) return {
|
|
277
654
|
error: "not_configured"
|
|
278
655
|
};
|
|
@@ -299,7 +676,8 @@ async function fetchUsage(workspaceId, authCookie) {
|
|
|
299
676
|
},
|
|
300
677
|
f: 31,
|
|
301
678
|
m: []
|
|
302
|
-
})
|
|
679
|
+
}),
|
|
680
|
+
signal
|
|
303
681
|
});
|
|
304
682
|
if (!res.ok) {
|
|
305
683
|
if (res.status === 302) return {
|
|
@@ -389,36 +767,70 @@ function GoUsagePanel(props) {
|
|
|
389
767
|
const [configured, setConfigured] = createSignal(true);
|
|
390
768
|
const [lastUpdated, setLastUpdated] = createSignal("");
|
|
391
769
|
const [panelWidth, setPanelWidth] = createSignal(24);
|
|
770
|
+
const [cfg, setCfg] = createSignal(loadConfig());
|
|
771
|
+
createEffect(() => {
|
|
772
|
+
void configTick();
|
|
773
|
+
setCfg(loadConfig());
|
|
774
|
+
});
|
|
392
775
|
let boxEl;
|
|
776
|
+
let abortController = null;
|
|
777
|
+
let timeoutId = null;
|
|
778
|
+
let isRefreshing = false;
|
|
779
|
+
let resizeTimeout2 = null;
|
|
780
|
+
function cleanupPendingRequest() {
|
|
781
|
+
if (timeoutId) {
|
|
782
|
+
clearTimeout(timeoutId);
|
|
783
|
+
timeoutId = null;
|
|
784
|
+
}
|
|
785
|
+
if (abortController) {
|
|
786
|
+
abortController.abort();
|
|
787
|
+
abortController = null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
393
790
|
async function refresh() {
|
|
394
|
-
|
|
395
|
-
|
|
791
|
+
if (isRefreshing) return;
|
|
792
|
+
const cfg2 = loadConfig();
|
|
793
|
+
if (!cfg2.workspaceId || !cfg2.authCookie) {
|
|
396
794
|
setConfigured(false);
|
|
397
795
|
setUsage(null);
|
|
398
796
|
setError("");
|
|
399
797
|
return;
|
|
400
798
|
}
|
|
401
799
|
setConfigured(true);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
800
|
+
cleanupPendingRequest();
|
|
801
|
+
abortController = new AbortController();
|
|
802
|
+
const signal = abortController.signal;
|
|
803
|
+
timeoutId = setTimeout(() => abortController?.abort(), REQUEST_TIMEOUT);
|
|
804
|
+
isRefreshing = true;
|
|
805
|
+
try {
|
|
806
|
+
const u = await fetchUsage(cfg2.workspaceId, cfg2.authCookie, signal);
|
|
807
|
+
if (signal.aborted) return;
|
|
808
|
+
if (u === null) {
|
|
809
|
+
setError(t("queryFailed"));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (u.error === "cookie_expired") {
|
|
813
|
+
setError(t("cookieExpired"));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (u.error === "not_configured") {
|
|
817
|
+
setConfigured(false);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
setUsage(u);
|
|
821
|
+
setLastUpdated((/* @__PURE__ */ new Date()).toLocaleTimeString());
|
|
822
|
+
setError("");
|
|
823
|
+
} finally {
|
|
824
|
+
if (timeoutId) {
|
|
825
|
+
clearTimeout(timeoutId);
|
|
826
|
+
timeoutId = null;
|
|
827
|
+
}
|
|
828
|
+
isRefreshing = false;
|
|
414
829
|
}
|
|
415
|
-
setUsage(u);
|
|
416
|
-
setLastUpdated((/* @__PURE__ */ new Date()).toLocaleTimeString());
|
|
417
|
-
setError("");
|
|
418
830
|
}
|
|
419
831
|
onMount(() => {
|
|
420
832
|
refresh();
|
|
421
|
-
const timer = setInterval(refresh,
|
|
833
|
+
const timer = setInterval(refresh, loadConfig().refreshIntervalMs);
|
|
422
834
|
const stopWatch = createEffect(() => {
|
|
423
835
|
void configTick();
|
|
424
836
|
refresh();
|
|
@@ -426,6 +838,11 @@ function GoUsagePanel(props) {
|
|
|
426
838
|
onCleanup(() => {
|
|
427
839
|
clearInterval(timer);
|
|
428
840
|
stopWatch();
|
|
841
|
+
cleanupPendingRequest();
|
|
842
|
+
if (resizeTimeout2) {
|
|
843
|
+
clearTimeout(resizeTimeout2);
|
|
844
|
+
resizeTimeout2 = null;
|
|
845
|
+
}
|
|
429
846
|
});
|
|
430
847
|
});
|
|
431
848
|
const [pal, setPal] = createSignal({
|
|
@@ -505,8 +922,11 @@ function GoUsagePanel(props) {
|
|
|
505
922
|
_$setProp(_el$1, "flexDirection", "column");
|
|
506
923
|
_$setProp(_el$1, "gap", 0);
|
|
507
924
|
_$setProp(_el$1, "onSizeChange", () => {
|
|
508
|
-
|
|
509
|
-
|
|
925
|
+
if (resizeTimeout2) clearTimeout(resizeTimeout2);
|
|
926
|
+
resizeTimeout2 = setTimeout(() => {
|
|
927
|
+
const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
|
|
928
|
+
setPanelWidth(w);
|
|
929
|
+
}, 100);
|
|
510
930
|
});
|
|
511
931
|
_$insertNode(_el$10, _el$11);
|
|
512
932
|
_$insertNode(_el$10, _el$12);
|
|
@@ -647,6 +1067,250 @@ function GoUsagePanel(props) {
|
|
|
647
1067
|
return _el$1;
|
|
648
1068
|
})();
|
|
649
1069
|
}
|
|
1070
|
+
function PricePanel(props) {
|
|
1071
|
+
const [open, setOpen] = createSignal(true);
|
|
1072
|
+
const [pricingStore, setPricingStore] = createSignal(null);
|
|
1073
|
+
const [priceError, setPriceError] = createSignal("");
|
|
1074
|
+
const [priceLoading, setPriceLoading] = createSignal(false);
|
|
1075
|
+
const [panelWidth, setPanelWidth] = createSignal(24);
|
|
1076
|
+
const [cfg, setCfg] = createSignal(loadConfig());
|
|
1077
|
+
createEffect(() => {
|
|
1078
|
+
void configTick();
|
|
1079
|
+
setCfg(loadConfig());
|
|
1080
|
+
});
|
|
1081
|
+
let boxEl;
|
|
1082
|
+
let priceController = null;
|
|
1083
|
+
async function refreshPricing() {
|
|
1084
|
+
if (priceLoading()) return;
|
|
1085
|
+
setPriceLoading(true);
|
|
1086
|
+
setPriceError("");
|
|
1087
|
+
priceController?.abort();
|
|
1088
|
+
priceController = new AbortController();
|
|
1089
|
+
try {
|
|
1090
|
+
const data = await fetchPricing();
|
|
1091
|
+
const store = updatePricingStore(data);
|
|
1092
|
+
setSharedPricing(data);
|
|
1093
|
+
setPricingStore(store);
|
|
1094
|
+
const prev = store.previous;
|
|
1095
|
+
if (prev && prev.fetchTime !== data.fetchTime) {
|
|
1096
|
+
const diff = comparePricing(prev, data);
|
|
1097
|
+
if (diff.hasChanges) {
|
|
1098
|
+
props.api.toast({
|
|
1099
|
+
variant: "info",
|
|
1100
|
+
title: t("priceChanged"),
|
|
1101
|
+
message: t("priceChangesSummary", {
|
|
1102
|
+
t: diff.summary.total,
|
|
1103
|
+
a: diff.summary.added,
|
|
1104
|
+
r: diff.summary.removed,
|
|
1105
|
+
p: diff.summary.priceChanges,
|
|
1106
|
+
l: diff.summary.limitChanges
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
} catch {
|
|
1112
|
+
setPriceError(t("priceError"));
|
|
1113
|
+
} finally {
|
|
1114
|
+
setPriceLoading(false);
|
|
1115
|
+
priceController = null;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function formatCount(n) {
|
|
1119
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
|
|
1120
|
+
return String(n);
|
|
1121
|
+
}
|
|
1122
|
+
function visualPadEnd(s, w) {
|
|
1123
|
+
const cur = visualWidth(s);
|
|
1124
|
+
if (cur >= w) return s;
|
|
1125
|
+
return s + " ".repeat(w - cur);
|
|
1126
|
+
}
|
|
1127
|
+
function abbreviateName(name, maxLen) {
|
|
1128
|
+
if (visualWidth(name) <= maxLen) return name;
|
|
1129
|
+
let out = "";
|
|
1130
|
+
let w = 0;
|
|
1131
|
+
for (const c of name) {
|
|
1132
|
+
const cw = visualWidth(c);
|
|
1133
|
+
if (w + cw > maxLen - 1) break;
|
|
1134
|
+
out += c;
|
|
1135
|
+
w += cw;
|
|
1136
|
+
}
|
|
1137
|
+
return out + "\u2026";
|
|
1138
|
+
}
|
|
1139
|
+
function justify(label, value) {
|
|
1140
|
+
const gauge = panelWidth() - 4;
|
|
1141
|
+
const used = visualWidth(label) + visualWidth(value);
|
|
1142
|
+
const gap = Math.max(1, gauge - used);
|
|
1143
|
+
return label + " ".repeat(gap) + value;
|
|
1144
|
+
}
|
|
1145
|
+
function renderLimitRow(m) {
|
|
1146
|
+
const name = abbreviateName(m.name, 18);
|
|
1147
|
+
const req = `${formatCount(m.limits.fiveHour)}/${formatCount(m.limits.weekly)}/${formatCount(m.limits.monthly)}`;
|
|
1148
|
+
return (() => {
|
|
1149
|
+
var _el$30 = _$createElement("text"), _el$31 = _$createElement("span");
|
|
1150
|
+
_$insertNode(_el$30, _el$31);
|
|
1151
|
+
_$insert(_el$31, () => justify(name, req));
|
|
1152
|
+
_$effect((_$p) => _$setProp(_el$31, "style", {
|
|
1153
|
+
fg: colors().text
|
|
1154
|
+
}, _$p));
|
|
1155
|
+
return _el$30;
|
|
1156
|
+
})();
|
|
1157
|
+
}
|
|
1158
|
+
const [pal, setPal] = createSignal({
|
|
1159
|
+
...FALLBACK
|
|
1160
|
+
});
|
|
1161
|
+
createEffect(() => {
|
|
1162
|
+
const th = props.theme;
|
|
1163
|
+
const p = {
|
|
1164
|
+
...FALLBACK
|
|
1165
|
+
};
|
|
1166
|
+
for (const k of Object.keys(FALLBACK)) {
|
|
1167
|
+
const h = hex(th?.[k]);
|
|
1168
|
+
if (h) p[k] = h;
|
|
1169
|
+
}
|
|
1170
|
+
setPal(p);
|
|
1171
|
+
});
|
|
1172
|
+
const colors = () => pal();
|
|
1173
|
+
const sep = () => "\u2500".repeat(Math.max(1, panelWidth() - 4));
|
|
1174
|
+
setTimeout(() => {
|
|
1175
|
+
const store = loadPricingStore();
|
|
1176
|
+
if (store.current.models.length > 0) {
|
|
1177
|
+
setSharedPricing(store.current);
|
|
1178
|
+
setPricingStore(store);
|
|
1179
|
+
}
|
|
1180
|
+
refreshPricing();
|
|
1181
|
+
setInterval(() => refreshPricing(), loadConfig().pricingIntervalMs);
|
|
1182
|
+
}, 2e3);
|
|
1183
|
+
return (() => {
|
|
1184
|
+
var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$34 = _$createElement("span"), _el$35 = _$createElement("span"), _el$36 = _$createElement("b"), _el$40 = _$createElement("span");
|
|
1185
|
+
_$insertNode(_el$32, _el$33);
|
|
1186
|
+
var _ref$2 = boxEl;
|
|
1187
|
+
typeof _ref$2 === "function" ? _$use(_ref$2, _el$32) : boxEl = _el$32;
|
|
1188
|
+
_$setProp(_el$32, "border", true);
|
|
1189
|
+
_$setProp(_el$32, "paddingLeft", 1);
|
|
1190
|
+
_$setProp(_el$32, "paddingRight", 1);
|
|
1191
|
+
_$setProp(_el$32, "flexDirection", "column");
|
|
1192
|
+
_$setProp(_el$32, "gap", 0);
|
|
1193
|
+
_$setProp(_el$32, "onSizeChange", () => {
|
|
1194
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
1195
|
+
resizeTimeout = setTimeout(() => {
|
|
1196
|
+
const w = boxEl ? Math.max(20, boxEl.width ?? 24) : 24;
|
|
1197
|
+
setPanelWidth(w);
|
|
1198
|
+
}, 100);
|
|
1199
|
+
});
|
|
1200
|
+
_$insertNode(_el$33, _el$34);
|
|
1201
|
+
_$insertNode(_el$33, _el$35);
|
|
1202
|
+
_$insertNode(_el$33, _el$40);
|
|
1203
|
+
_$setProp(_el$33, "onMouseUp", () => setOpen((o) => !o));
|
|
1204
|
+
_$insert(_el$34, () => open() ? "\u25BC " : "\u25B6 ");
|
|
1205
|
+
_$insertNode(_el$35, _el$36);
|
|
1206
|
+
_$insert(_el$36, () => t("priceTitle"));
|
|
1207
|
+
_$insert(_el$33, _$createComponent(Show, {
|
|
1208
|
+
get when() {
|
|
1209
|
+
return sharedPricing();
|
|
1210
|
+
},
|
|
1211
|
+
get children() {
|
|
1212
|
+
var _el$37 = _$createElement("span"), _el$38 = _$createTextNode(` (`), _el$39 = _$createTextNode(`)`);
|
|
1213
|
+
_$insertNode(_el$37, _el$38);
|
|
1214
|
+
_$insertNode(_el$37, _el$39);
|
|
1215
|
+
_$insert(_el$37, () => sharedPricing().models.length, _el$39);
|
|
1216
|
+
_$effect((_$p) => _$setProp(_el$37, "style", {
|
|
1217
|
+
fg: colors().muted
|
|
1218
|
+
}, _$p));
|
|
1219
|
+
return _el$37;
|
|
1220
|
+
}
|
|
1221
|
+
}), _el$40);
|
|
1222
|
+
_$insert(_el$40, () => sep().slice(visualWidth((open() ? "\u25BC " : "\u25B6 ") + t("priceTitle") + (sharedPricing() ? ` (${sharedPricing().models.length})` : ""))));
|
|
1223
|
+
_$insert(_el$32, _$createComponent(Show, {
|
|
1224
|
+
get when() {
|
|
1225
|
+
return open();
|
|
1226
|
+
},
|
|
1227
|
+
get children() {
|
|
1228
|
+
return [(() => {
|
|
1229
|
+
var _el$41 = _$createElement("text");
|
|
1230
|
+
_$insert(_el$41, () => justify(t("priceModelCol"), t("priceReqs")));
|
|
1231
|
+
_$effect((_$p) => _$setProp(_el$41, "fg", colors().muted, _$p));
|
|
1232
|
+
return _el$41;
|
|
1233
|
+
})(), (() => {
|
|
1234
|
+
var _el$42 = _$createElement("text");
|
|
1235
|
+
_$insert(_el$42, sep);
|
|
1236
|
+
_$effect((_$p) => _$setProp(_el$42, "fg", colors().muted, _$p));
|
|
1237
|
+
return _el$42;
|
|
1238
|
+
})(), _$createComponent(Show, {
|
|
1239
|
+
get when() {
|
|
1240
|
+
return priceLoading() || !sharedPricing();
|
|
1241
|
+
},
|
|
1242
|
+
get fallback() {
|
|
1243
|
+
return _$createComponent(Show, {
|
|
1244
|
+
get when() {
|
|
1245
|
+
return sharedPricing();
|
|
1246
|
+
},
|
|
1247
|
+
get fallback() {
|
|
1248
|
+
return (() => {
|
|
1249
|
+
var _el$46 = _$createElement("text");
|
|
1250
|
+
_$insert(_el$46, () => t("priceLoading"));
|
|
1251
|
+
_$effect((_$p) => _$setProp(_el$46, "fg", colors().muted, _$p));
|
|
1252
|
+
return _el$46;
|
|
1253
|
+
})();
|
|
1254
|
+
},
|
|
1255
|
+
get children() {
|
|
1256
|
+
return [_$memo(() => sharedPricing().models.filter((m) => {
|
|
1257
|
+
return cfg().focusModels.length === 0 || cfg().focusModels.includes(m.id);
|
|
1258
|
+
}).map((m) => renderLimitRow(m))), (() => {
|
|
1259
|
+
var _el$44 = _$createElement("text");
|
|
1260
|
+
_$insert(_el$44, () => justify(t("priceUpdated"), new Date(sharedPricing().fetchTime).toLocaleTimeString()));
|
|
1261
|
+
_$effect((_$p) => _$setProp(_el$44, "fg", colors().muted, _$p));
|
|
1262
|
+
return _el$44;
|
|
1263
|
+
})(), _$createComponent(Show, {
|
|
1264
|
+
get when() {
|
|
1265
|
+
return _$memo(() => !!pricingStore())() && pricingStore().history.length > 0;
|
|
1266
|
+
},
|
|
1267
|
+
get children() {
|
|
1268
|
+
var _el$45 = _$createElement("text");
|
|
1269
|
+
_$insert(_el$45, () => t("priceChangesSummary", {
|
|
1270
|
+
t: pricingStore().history[0].summary.total,
|
|
1271
|
+
a: pricingStore().history[0].summary.added,
|
|
1272
|
+
r: pricingStore().history[0].summary.removed,
|
|
1273
|
+
p: pricingStore().history[0].summary.priceChanges,
|
|
1274
|
+
l: pricingStore().history[0].summary.limitChanges
|
|
1275
|
+
}));
|
|
1276
|
+
_$effect((_$p) => _$setProp(_el$45, "fg", colors().warning, _$p));
|
|
1277
|
+
return _el$45;
|
|
1278
|
+
}
|
|
1279
|
+
})];
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
get children() {
|
|
1284
|
+
var _el$43 = _$createElement("text");
|
|
1285
|
+
_$insert(_el$43, () => t("priceLoading"));
|
|
1286
|
+
_$effect((_$p) => _$setProp(_el$43, "fg", colors().muted, _$p));
|
|
1287
|
+
return _el$43;
|
|
1288
|
+
}
|
|
1289
|
+
})];
|
|
1290
|
+
}
|
|
1291
|
+
}), null);
|
|
1292
|
+
_$effect((_p$) => {
|
|
1293
|
+
var _v$1 = colors().border, _v$10 = {
|
|
1294
|
+
fg: colors().muted
|
|
1295
|
+
}, _v$11 = {
|
|
1296
|
+
fg: colors().primary
|
|
1297
|
+
}, _v$12 = {
|
|
1298
|
+
fg: colors().muted
|
|
1299
|
+
};
|
|
1300
|
+
_v$1 !== _p$.e && (_p$.e = _$setProp(_el$32, "borderColor", _v$1, _p$.e));
|
|
1301
|
+
_v$10 !== _p$.t && (_p$.t = _$setProp(_el$34, "style", _v$10, _p$.t));
|
|
1302
|
+
_v$11 !== _p$.a && (_p$.a = _$setProp(_el$35, "style", _v$11, _p$.a));
|
|
1303
|
+
_v$12 !== _p$.o && (_p$.o = _$setProp(_el$40, "style", _v$12, _p$.o));
|
|
1304
|
+
return _p$;
|
|
1305
|
+
}, {
|
|
1306
|
+
e: void 0,
|
|
1307
|
+
t: void 0,
|
|
1308
|
+
a: void 0,
|
|
1309
|
+
o: void 0
|
|
1310
|
+
});
|
|
1311
|
+
return _el$32;
|
|
1312
|
+
})();
|
|
1313
|
+
}
|
|
650
1314
|
function createSidebarSlot(api) {
|
|
651
1315
|
return {
|
|
652
1316
|
order: 60,
|
|
@@ -662,15 +1326,30 @@ function createSidebarSlot(api) {
|
|
|
662
1326
|
}
|
|
663
1327
|
};
|
|
664
1328
|
}
|
|
1329
|
+
function createPricingSlot(api) {
|
|
1330
|
+
return {
|
|
1331
|
+
order: 61,
|
|
1332
|
+
slots: {
|
|
1333
|
+
sidebar_content(ctx, _input) {
|
|
1334
|
+
return _$createComponent(PricePanel, {
|
|
1335
|
+
get theme() {
|
|
1336
|
+
return ctx.theme.current;
|
|
1337
|
+
},
|
|
1338
|
+
api
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
665
1344
|
function runConfigDialog(api, dialog) {
|
|
666
1345
|
dialog?.replace(() => _$createComponent(api.ui.DialogPrompt, {
|
|
667
1346
|
get title() {
|
|
668
1347
|
return t("wsPromptTitle");
|
|
669
1348
|
},
|
|
670
1349
|
description: () => (() => {
|
|
671
|
-
var _el$
|
|
672
|
-
_$insert(_el$
|
|
673
|
-
return _el$
|
|
1350
|
+
var _el$47 = _$createElement("text");
|
|
1351
|
+
_$insert(_el$47, () => t("wsPromptDesc"));
|
|
1352
|
+
return _el$47;
|
|
674
1353
|
})(),
|
|
675
1354
|
get placeholder() {
|
|
676
1355
|
return t("wsPlaceholder");
|
|
@@ -689,9 +1368,9 @@ function runConfigDialog(api, dialog) {
|
|
|
689
1368
|
return t("cookiePromptTitle");
|
|
690
1369
|
},
|
|
691
1370
|
description: () => (() => {
|
|
692
|
-
var _el$
|
|
693
|
-
_$insert(_el$
|
|
694
|
-
return _el$
|
|
1371
|
+
var _el$48 = _$createElement("text");
|
|
1372
|
+
_$insert(_el$48, () => t("cookiePromptDesc"));
|
|
1373
|
+
return _el$48;
|
|
695
1374
|
})(),
|
|
696
1375
|
get placeholder() {
|
|
697
1376
|
return t("cookiePlaceholder");
|
|
@@ -746,8 +1425,161 @@ function runLangDialog(api, dialog) {
|
|
|
746
1425
|
onCancel: () => dialog?.clear()
|
|
747
1426
|
}));
|
|
748
1427
|
}
|
|
1428
|
+
function runSettingsDialog(api, dialog) {
|
|
1429
|
+
let lastSelected;
|
|
1430
|
+
const renderSettingsDialog = (dialog2) => {
|
|
1431
|
+
const cfg = loadConfig();
|
|
1432
|
+
const options = [{
|
|
1433
|
+
title: `${t("settingsFocus")} (${cfg.focusModels.length} \u4E2A)`,
|
|
1434
|
+
value: "focus-models"
|
|
1435
|
+
}, {
|
|
1436
|
+
title: `${t("settingsRefreshUsage")} (${Math.round(cfg.refreshIntervalMs / 1e3)}s)`,
|
|
1437
|
+
value: "refresh-usage"
|
|
1438
|
+
}, {
|
|
1439
|
+
title: `${t("settingsRefreshPricing")} (${Math.round(cfg.pricingIntervalMs / 1e3)}s)`,
|
|
1440
|
+
value: "refresh-pricing"
|
|
1441
|
+
}];
|
|
1442
|
+
dialog2?.replace(() => _$createComponent(api.ui.DialogSelect, {
|
|
1443
|
+
get title() {
|
|
1444
|
+
return t("settingsTitle");
|
|
1445
|
+
},
|
|
1446
|
+
options,
|
|
1447
|
+
onSelect: (opt) => {
|
|
1448
|
+
const value = opt.value;
|
|
1449
|
+
if (value === "focus-models") {
|
|
1450
|
+
const renderFocusDialog = (dialog3) => {
|
|
1451
|
+
const cur = loadConfig().focusModels;
|
|
1452
|
+
const allModels = sharedPricing()?.models ?? [];
|
|
1453
|
+
const modelOptions = allModels.map((m) => ({
|
|
1454
|
+
title: `${cur.includes(m.id) ? "\u2611" : "\u2610"} ${m.name} (${m.pricing.input ?? "-"}/${m.pricing.output ?? "-"})`,
|
|
1455
|
+
value: m.id
|
|
1456
|
+
}));
|
|
1457
|
+
const focusOptions = [...modelOptions, {
|
|
1458
|
+
title: `${t("focusAll")} (${cur.length} \u4E2A\u5DF2\u9009)`,
|
|
1459
|
+
value: "select-all"
|
|
1460
|
+
}, {
|
|
1461
|
+
title: t("focusNone"),
|
|
1462
|
+
value: "clear-all"
|
|
1463
|
+
}, {
|
|
1464
|
+
title: `${t("focusDone")} (${t("focusSelected", {
|
|
1465
|
+
n: cur.length
|
|
1466
|
+
})})`,
|
|
1467
|
+
value: "done"
|
|
1468
|
+
}];
|
|
1469
|
+
dialog3?.replace(() => _$createComponent(api.ui.DialogSelect, {
|
|
1470
|
+
get title() {
|
|
1471
|
+
return t("settingsFocus");
|
|
1472
|
+
},
|
|
1473
|
+
options: focusOptions,
|
|
1474
|
+
current: lastSelected,
|
|
1475
|
+
onSelect: (opt2) => {
|
|
1476
|
+
const v2 = opt2.value;
|
|
1477
|
+
if (v2 === "done") {
|
|
1478
|
+
dialog3?.clear();
|
|
1479
|
+
renderSettingsDialog(dialog2);
|
|
1480
|
+
} else if (v2 === "select-all") {
|
|
1481
|
+
const all = allModels.map((m) => m.id);
|
|
1482
|
+
saveConfig({
|
|
1483
|
+
focus_models: all
|
|
1484
|
+
});
|
|
1485
|
+
setConfigTick((v) => v + 1);
|
|
1486
|
+
api.ui.toast({
|
|
1487
|
+
variant: "success",
|
|
1488
|
+
message: t("settingsSaved")
|
|
1489
|
+
});
|
|
1490
|
+
renderFocusDialog(dialog3);
|
|
1491
|
+
} else if (v2 === "clear-all") {
|
|
1492
|
+
saveConfig({
|
|
1493
|
+
focus_models: []
|
|
1494
|
+
});
|
|
1495
|
+
setConfigTick((v) => v + 1);
|
|
1496
|
+
api.ui.toast({
|
|
1497
|
+
variant: "success",
|
|
1498
|
+
message: t("settingsSaved")
|
|
1499
|
+
});
|
|
1500
|
+
renderFocusDialog(dialog3);
|
|
1501
|
+
} else {
|
|
1502
|
+
const next = cur.includes(v2) ? cur.filter((id) => id !== v2) : [...cur, v2];
|
|
1503
|
+
saveConfig({
|
|
1504
|
+
focus_models: next
|
|
1505
|
+
});
|
|
1506
|
+
setConfigTick((v) => v + 1);
|
|
1507
|
+
lastSelected = v2;
|
|
1508
|
+
renderFocusDialog(dialog3);
|
|
1509
|
+
}
|
|
1510
|
+
},
|
|
1511
|
+
onCancel: () => dialog3?.clear()
|
|
1512
|
+
}));
|
|
1513
|
+
};
|
|
1514
|
+
renderFocusDialog(dialog2);
|
|
1515
|
+
} else if (value === "refresh-usage") {
|
|
1516
|
+
dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
|
|
1517
|
+
get title() {
|
|
1518
|
+
return t("settingsRefreshUsage");
|
|
1519
|
+
},
|
|
1520
|
+
description: () => (() => {
|
|
1521
|
+
var _el$49 = _$createElement("text");
|
|
1522
|
+
_$insertNode(_el$49, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 60\uFF09`));
|
|
1523
|
+
return _el$49;
|
|
1524
|
+
})(),
|
|
1525
|
+
placeholder: "60",
|
|
1526
|
+
onConfirm: (val) => {
|
|
1527
|
+
const sec = Number(val.trim());
|
|
1528
|
+
if (!sec || sec <= 0) {
|
|
1529
|
+
dialog2?.clear();
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
saveConfig({
|
|
1533
|
+
refresh_interval_sec: sec
|
|
1534
|
+
});
|
|
1535
|
+
setConfigTick((v) => v + 1);
|
|
1536
|
+
api.ui.toast({
|
|
1537
|
+
variant: "success",
|
|
1538
|
+
message: t("settingsSaved")
|
|
1539
|
+
});
|
|
1540
|
+
renderSettingsDialog(dialog2);
|
|
1541
|
+
},
|
|
1542
|
+
onCancel: () => dialog2?.clear()
|
|
1543
|
+
}));
|
|
1544
|
+
} else if (value === "refresh-pricing") {
|
|
1545
|
+
dialog2?.replace(() => _$createComponent(api.ui.DialogPrompt, {
|
|
1546
|
+
get title() {
|
|
1547
|
+
return t("settingsRefreshPricing");
|
|
1548
|
+
},
|
|
1549
|
+
description: () => (() => {
|
|
1550
|
+
var _el$51 = _$createElement("text");
|
|
1551
|
+
_$insertNode(_el$51, _$createTextNode(`\u8F93\u5165\u79D2\u6570\uFF08\u9ED8\u8BA4 1800\uFF09`));
|
|
1552
|
+
return _el$51;
|
|
1553
|
+
})(),
|
|
1554
|
+
placeholder: "1800",
|
|
1555
|
+
onConfirm: (val) => {
|
|
1556
|
+
const sec = Number(val.trim());
|
|
1557
|
+
if (!sec || sec <= 0) {
|
|
1558
|
+
dialog2?.clear();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
saveConfig({
|
|
1562
|
+
pricing_interval_sec: sec
|
|
1563
|
+
});
|
|
1564
|
+
setConfigTick((v) => v + 1);
|
|
1565
|
+
api.ui.toast({
|
|
1566
|
+
variant: "success",
|
|
1567
|
+
message: t("settingsSaved")
|
|
1568
|
+
});
|
|
1569
|
+
renderSettingsDialog(dialog2);
|
|
1570
|
+
},
|
|
1571
|
+
onCancel: () => dialog2?.clear()
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
onCancel: () => dialog2?.clear()
|
|
1576
|
+
}));
|
|
1577
|
+
};
|
|
1578
|
+
renderSettingsDialog(dialog);
|
|
1579
|
+
}
|
|
749
1580
|
var tui = async (api) => {
|
|
750
1581
|
api.slots.register(createSidebarSlot(api));
|
|
1582
|
+
api.slots.register(createPricingSlot(api));
|
|
751
1583
|
const savedLang = kvTryGet(api, `${KV_PREFIX}.lang`);
|
|
752
1584
|
if (savedLang === "zh" || savedLang === "en") setLangCode(savedLang);
|
|
753
1585
|
else setLangCode(detectLang());
|
|
@@ -777,6 +1609,14 @@ var tui = async (api) => {
|
|
|
777
1609
|
name: "go-lang"
|
|
778
1610
|
},
|
|
779
1611
|
onSelect: (dialog) => runLangDialog(api, dialog)
|
|
1612
|
+
}, {
|
|
1613
|
+
title: t("settingsTitle"),
|
|
1614
|
+
value: "go-usage.settings",
|
|
1615
|
+
description: t("settingsDesc"),
|
|
1616
|
+
slash: {
|
|
1617
|
+
name: "go-settings"
|
|
1618
|
+
},
|
|
1619
|
+
onSelect: (dialog) => runSettingsDialog(api, dialog)
|
|
780
1620
|
}]);
|
|
781
1621
|
};
|
|
782
1622
|
var mod = {
|