@wrongstack/webui 0.41.0 → 0.54.1
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/assets/index-5ECutVTP.css +1 -0
- package/dist/index.css +3 -0
- package/dist/index.css.map +1 -1
- package/dist/index.html +2 -2
- package/dist/server/entry.js +368 -305
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.js +368 -305
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -5
- package/dist/assets/index-Ec0lUwfB.css +0 -1
- /package/dist/assets/{index-26E8h-cq.js → index-BRHGqfHg.js} +0 -0
package/dist/server/entry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/server/index.ts
|
|
3
|
-
import * as
|
|
3
|
+
import * as fs2 from "fs/promises";
|
|
4
4
|
import * as path2 from "path";
|
|
5
5
|
|
|
6
6
|
// src/server/http-server.ts
|
|
@@ -84,6 +84,54 @@ function createHttpServer(opts) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// src/server/file-picker.ts
|
|
88
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
89
|
+
".git",
|
|
90
|
+
"node_modules",
|
|
91
|
+
"dist",
|
|
92
|
+
"build",
|
|
93
|
+
".next",
|
|
94
|
+
".turbo",
|
|
95
|
+
".cache",
|
|
96
|
+
"target",
|
|
97
|
+
"coverage",
|
|
98
|
+
".nyc_output",
|
|
99
|
+
"out",
|
|
100
|
+
".pnpm-store",
|
|
101
|
+
".parcel-cache"
|
|
102
|
+
]);
|
|
103
|
+
var KEEP_DOTFILES = /* @__PURE__ */ new Set([
|
|
104
|
+
".wrongstack",
|
|
105
|
+
".env.example",
|
|
106
|
+
".gitignore",
|
|
107
|
+
".eslintrc",
|
|
108
|
+
".prettierrc"
|
|
109
|
+
]);
|
|
110
|
+
function isHiddenEntry(name) {
|
|
111
|
+
return name.startsWith(".") && !KEEP_DOTFILES.has(name);
|
|
112
|
+
}
|
|
113
|
+
function rankFiles(paths, query, limit) {
|
|
114
|
+
const q = query.toLowerCase();
|
|
115
|
+
const scored = [];
|
|
116
|
+
for (const p of paths) {
|
|
117
|
+
if (!q) {
|
|
118
|
+
scored.push({ path: p, score: 0 });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const lower = p.toLowerCase();
|
|
122
|
+
const base = lower.split("/").pop() ?? lower;
|
|
123
|
+
let score = 0;
|
|
124
|
+
if (base === q) score = 100;
|
|
125
|
+
else if (base.startsWith(q)) score = 60;
|
|
126
|
+
else if (lower.includes(q)) score = 20;
|
|
127
|
+
else continue;
|
|
128
|
+
score -= p.split("/").length;
|
|
129
|
+
scored.push({ path: p, score });
|
|
130
|
+
}
|
|
131
|
+
scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
132
|
+
return scored.slice(0, limit).map((s) => s.path);
|
|
133
|
+
}
|
|
134
|
+
|
|
87
135
|
// src/server/index.ts
|
|
88
136
|
import {
|
|
89
137
|
Agent,
|
|
@@ -120,7 +168,7 @@ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/sec
|
|
|
120
168
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
121
169
|
import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
|
|
122
170
|
import { WebSocket, WebSocketServer } from "ws";
|
|
123
|
-
import { randomBytes
|
|
171
|
+
import { randomBytes } from "crypto";
|
|
124
172
|
|
|
125
173
|
// ../runtime/src/container.ts
|
|
126
174
|
import {
|
|
@@ -194,44 +242,14 @@ function createDefaultContainer(opts) {
|
|
|
194
242
|
}
|
|
195
243
|
|
|
196
244
|
// src/server/boot.ts
|
|
197
|
-
import * as fs2 from "fs/promises";
|
|
198
|
-
import * as os from "os";
|
|
199
245
|
import {
|
|
200
|
-
|
|
201
|
-
DefaultLogger,
|
|
202
|
-
DefaultPathResolver,
|
|
203
|
-
DefaultSecretVault,
|
|
204
|
-
migratePlaintextSecrets,
|
|
205
|
-
resolveWstackPaths,
|
|
206
|
-
writeErr
|
|
246
|
+
bootConfig as coreBootConfig
|
|
207
247
|
} from "@wrongstack/core";
|
|
208
248
|
async function bootConfig() {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
const projectRoot = pathResolver.projectRoot;
|
|
212
|
-
const userHome = os.homedir();
|
|
213
|
-
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
214
|
-
await fs2.mkdir(wpaths.globalRoot, { recursive: true });
|
|
215
|
-
await fs2.mkdir(wpaths.projectDir, { recursive: true });
|
|
216
|
-
await fs2.mkdir(wpaths.projectSessions, { recursive: true });
|
|
217
|
-
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
218
|
-
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
219
|
-
try {
|
|
220
|
-
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
221
|
-
if (migrated > 0) {
|
|
222
|
-
writeErr(`[WebUI] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
223
|
-
`);
|
|
224
|
-
}
|
|
225
|
-
} catch {
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
|
|
229
|
-
const config = await configLoader.load({ cliFlags: {} });
|
|
230
|
-
const logger = new DefaultLogger({
|
|
231
|
-
level: config.log?.level ?? "info",
|
|
232
|
-
file: wpaths.logFile
|
|
249
|
+
const { config, vault, globalConfigPath, projectRoot, wpaths, logger } = await coreBootConfig({
|
|
250
|
+
appLabel: "WebUI"
|
|
233
251
|
});
|
|
234
|
-
return { config, vault, globalConfigPath
|
|
252
|
+
return { config, vault, globalConfigPath, projectRoot, wpaths, logger };
|
|
235
253
|
}
|
|
236
254
|
function patchConfig(config, updates) {
|
|
237
255
|
return Object.freeze({ ...config, ...updates });
|
|
@@ -1362,7 +1380,255 @@ var WorktreeWebSocketHandler = class {
|
|
|
1362
1380
|
}
|
|
1363
1381
|
};
|
|
1364
1382
|
|
|
1383
|
+
// src/server/ws-auth.ts
|
|
1384
|
+
import { Buffer } from "buffer";
|
|
1385
|
+
import { timingSafeEqual } from "crypto";
|
|
1386
|
+
function isLoopbackHostname(hostname) {
|
|
1387
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1388
|
+
}
|
|
1389
|
+
function isLoopbackBind(wsHost2) {
|
|
1390
|
+
return wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
|
|
1391
|
+
}
|
|
1392
|
+
function tokenMatches(provided, expected) {
|
|
1393
|
+
if (!provided) return false;
|
|
1394
|
+
const a = Buffer.from(provided);
|
|
1395
|
+
const b = Buffer.from(expected);
|
|
1396
|
+
if (a.length !== b.length) return false;
|
|
1397
|
+
return timingSafeEqual(a, b);
|
|
1398
|
+
}
|
|
1399
|
+
function extractToken(url) {
|
|
1400
|
+
const match = url.match(/[?&]token=([^&]+)/);
|
|
1401
|
+
return match ? match[1] : void 0;
|
|
1402
|
+
}
|
|
1403
|
+
function hostHeaderOk(input) {
|
|
1404
|
+
if (!isLoopbackBind(input.wsHost)) return true;
|
|
1405
|
+
const hostHeader = (input.hostHeader ?? "").trim();
|
|
1406
|
+
if (!hostHeader) return false;
|
|
1407
|
+
let hostname;
|
|
1408
|
+
try {
|
|
1409
|
+
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
return isLoopbackHostname(hostname);
|
|
1414
|
+
}
|
|
1415
|
+
function verifyClient(input) {
|
|
1416
|
+
const { origin, url, hostHeader, remoteAddress, wsHost: wsHost2, expectedToken } = input;
|
|
1417
|
+
const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
|
|
1418
|
+
if (!hostHeaderOk({ hostHeader, wsHost: wsHost2 })) return false;
|
|
1419
|
+
if (!origin) {
|
|
1420
|
+
const remoteIp = remoteAddress ?? "";
|
|
1421
|
+
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1422
|
+
if (!isRemoteLoopback && wsHost2 === "0.0.0.0") return false;
|
|
1423
|
+
return tokenOk || isLoopbackBind(wsHost2);
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
const { hostname } = new URL(origin);
|
|
1427
|
+
if (isLoopbackHostname(hostname)) return true;
|
|
1428
|
+
return tokenOk;
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/server/lifecycle.ts
|
|
1435
|
+
function createShutdown(res) {
|
|
1436
|
+
const log = res.log ?? ((m) => console.log(m));
|
|
1437
|
+
const exit = res.exit ?? ((code) => process.exit(code));
|
|
1438
|
+
let shuttingDown = false;
|
|
1439
|
+
return async () => {
|
|
1440
|
+
if (shuttingDown) return;
|
|
1441
|
+
shuttingDown = true;
|
|
1442
|
+
log("[WebUI] Shutting down...");
|
|
1443
|
+
try {
|
|
1444
|
+
await res.flushSession();
|
|
1445
|
+
} catch (e) {
|
|
1446
|
+
log(`[WebUI] Error closing session: ${e instanceof Error ? e.message : String(e)}`);
|
|
1447
|
+
}
|
|
1448
|
+
for (const ws of res.clients()) ws.close();
|
|
1449
|
+
for (const server of res.servers) server?.close();
|
|
1450
|
+
exit(0);
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function registerShutdownHandlers(res) {
|
|
1454
|
+
const shutdown = createShutdown(res);
|
|
1455
|
+
process.on("SIGINT", shutdown);
|
|
1456
|
+
process.on("SIGTERM", shutdown);
|
|
1457
|
+
return () => {
|
|
1458
|
+
process.off("SIGINT", shutdown);
|
|
1459
|
+
process.off("SIGTERM", shutdown);
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// src/server/usage-cost.ts
|
|
1464
|
+
function getCostRates(model) {
|
|
1465
|
+
const cost = model?.cost;
|
|
1466
|
+
return {
|
|
1467
|
+
input: cost?.input ?? 0,
|
|
1468
|
+
output: cost?.output ?? 0,
|
|
1469
|
+
cacheRead: cost?.cache_read ?? 0
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function computeUsageCost(usage, rates) {
|
|
1473
|
+
return (usage.input * rates.input + usage.output * rates.output + (usage.cacheRead ?? 0) * rates.cacheRead) / 1e6;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/server/provider-keys.ts
|
|
1477
|
+
function normalizeKeys(cfg) {
|
|
1478
|
+
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
1479
|
+
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
1480
|
+
}
|
|
1481
|
+
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
1482
|
+
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
1483
|
+
}
|
|
1484
|
+
return [];
|
|
1485
|
+
}
|
|
1486
|
+
function writeKeysBack(cfg, keys) {
|
|
1487
|
+
if (keys.length === 0) {
|
|
1488
|
+
delete cfg.apiKeys;
|
|
1489
|
+
delete cfg.apiKey;
|
|
1490
|
+
delete cfg.activeKey;
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
cfg.apiKeys = keys;
|
|
1494
|
+
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
1495
|
+
cfg.apiKey = active.apiKey;
|
|
1496
|
+
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
1497
|
+
cfg.activeKey = active.label;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function maskedKey(key) {
|
|
1501
|
+
if (!key) return "\u2014";
|
|
1502
|
+
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
1503
|
+
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
1504
|
+
}
|
|
1505
|
+
function upsertKey(providers, providerId, label, apiKey, nowIso) {
|
|
1506
|
+
const existing = providers[providerId] ?? { type: providerId };
|
|
1507
|
+
const keys = normalizeKeys(existing);
|
|
1508
|
+
const idx = keys.findIndex((k) => k.label === label);
|
|
1509
|
+
if (idx >= 0) {
|
|
1510
|
+
keys[idx] = { ...keys[idx], apiKey, createdAt: nowIso };
|
|
1511
|
+
} else {
|
|
1512
|
+
keys.push({ label, apiKey, createdAt: nowIso });
|
|
1513
|
+
}
|
|
1514
|
+
writeKeysBack(existing, keys);
|
|
1515
|
+
if (!existing.activeKey) existing.activeKey = label;
|
|
1516
|
+
providers[providerId] = existing;
|
|
1517
|
+
return { ok: true, message: `Key "${label}" saved for ${providerId}` };
|
|
1518
|
+
}
|
|
1519
|
+
function deleteKey(providers, providerId, label) {
|
|
1520
|
+
const existing = providers[providerId];
|
|
1521
|
+
if (!existing) {
|
|
1522
|
+
return { ok: false, message: `Provider "${providerId}" not found` };
|
|
1523
|
+
}
|
|
1524
|
+
const keys = normalizeKeys(existing).filter((k) => k.label !== label);
|
|
1525
|
+
if (keys.length === 0) {
|
|
1526
|
+
delete providers[providerId];
|
|
1527
|
+
} else {
|
|
1528
|
+
writeKeysBack(existing, keys);
|
|
1529
|
+
if (existing.activeKey === label) existing.activeKey = keys[0].label;
|
|
1530
|
+
providers[providerId] = existing;
|
|
1531
|
+
}
|
|
1532
|
+
return { ok: true, message: `Key "${label}" deleted from ${providerId}` };
|
|
1533
|
+
}
|
|
1534
|
+
function setActiveKey(providers, providerId, label) {
|
|
1535
|
+
const existing = providers[providerId];
|
|
1536
|
+
if (!existing) {
|
|
1537
|
+
return { ok: false, message: `Provider "${providerId}" not found` };
|
|
1538
|
+
}
|
|
1539
|
+
existing.activeKey = label;
|
|
1540
|
+
writeKeysBack(existing, normalizeKeys(existing));
|
|
1541
|
+
providers[providerId] = existing;
|
|
1542
|
+
return { ok: true, message: `Active key for ${providerId} set to "${label}"` };
|
|
1543
|
+
}
|
|
1544
|
+
function addProvider(providers, payload, nowIso) {
|
|
1545
|
+
if (providers[payload.id]) {
|
|
1546
|
+
return {
|
|
1547
|
+
ok: false,
|
|
1548
|
+
message: `Provider "${payload.id}" already exists. Use key.add to add a key.`
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
const newProv = {
|
|
1552
|
+
type: payload.id,
|
|
1553
|
+
family: payload.family,
|
|
1554
|
+
baseUrl: payload.baseUrl
|
|
1555
|
+
};
|
|
1556
|
+
if (payload.apiKey) {
|
|
1557
|
+
newProv.apiKeys = [{ label: "default", apiKey: payload.apiKey, createdAt: nowIso }];
|
|
1558
|
+
newProv.activeKey = "default";
|
|
1559
|
+
}
|
|
1560
|
+
providers[payload.id] = newProv;
|
|
1561
|
+
return { ok: true, message: `Provider "${payload.id}" added` };
|
|
1562
|
+
}
|
|
1563
|
+
function removeProvider(providers, providerId) {
|
|
1564
|
+
if (!providers[providerId]) {
|
|
1565
|
+
return { ok: false, message: `Provider "${providerId}" not found` };
|
|
1566
|
+
}
|
|
1567
|
+
delete providers[providerId];
|
|
1568
|
+
return { ok: true, message: `Provider "${providerId}" removed` };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/server/token-estimator.ts
|
|
1572
|
+
function estimateTokens(s) {
|
|
1573
|
+
return Math.ceil(s.length / 4);
|
|
1574
|
+
}
|
|
1575
|
+
function stringifyContent(c) {
|
|
1576
|
+
if (typeof c === "string") return c;
|
|
1577
|
+
try {
|
|
1578
|
+
return JSON.stringify(c);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return String(c);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
function messageTokens(content) {
|
|
1584
|
+
if (typeof content === "string") return estimateTokens(content);
|
|
1585
|
+
if (!Array.isArray(content)) return 0;
|
|
1586
|
+
let tk = 0;
|
|
1587
|
+
for (const b of content) {
|
|
1588
|
+
if (b.type === "text") tk += estimateTokens(b.text ?? "");
|
|
1589
|
+
else if (b.type === "tool_use") tk += estimateTokens(stringifyContent(b.input));
|
|
1590
|
+
else if (b.type === "tool_result") tk += estimateTokens(stringifyContent(b.content));
|
|
1591
|
+
else tk += estimateTokens(stringifyContent(b));
|
|
1592
|
+
}
|
|
1593
|
+
return tk;
|
|
1594
|
+
}
|
|
1595
|
+
function messagePreview(content) {
|
|
1596
|
+
if (typeof content === "string") return content.slice(0, 60);
|
|
1597
|
+
if (!Array.isArray(content)) return "";
|
|
1598
|
+
return content.map(
|
|
1599
|
+
(b) => b.type === "text" ? (b.text ?? "").slice(0, 40) : b.type === "tool_use" ? `[tool_use: ${b.name}]` : b.type === "tool_result" ? "[tool_result]" : `[${b.type}]`
|
|
1600
|
+
).join(" ").slice(0, 60);
|
|
1601
|
+
}
|
|
1602
|
+
function estimateContextBreakdown(input) {
|
|
1603
|
+
const sysTokens = input.systemPrompt.reduce((acc, b) => acc + estimateTokens(b.text ?? ""), 0);
|
|
1604
|
+
const toolBreakdown = input.tools.map((t) => {
|
|
1605
|
+
const schema = t.inputSchema ?? {};
|
|
1606
|
+
const desc = t.description ?? "";
|
|
1607
|
+
return {
|
|
1608
|
+
name: t.name,
|
|
1609
|
+
tokens: estimateTokens(t.name) + estimateTokens(desc) + estimateTokens(stringifyContent(schema))
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
const toolTokens = toolBreakdown.reduce((a, b) => a + b.tokens, 0);
|
|
1613
|
+
const messageBreakdown = input.messages.map((m, i) => ({
|
|
1614
|
+
index: i,
|
|
1615
|
+
role: m.role,
|
|
1616
|
+
tokens: messageTokens(m.content),
|
|
1617
|
+
preview: messagePreview(m.content)
|
|
1618
|
+
}));
|
|
1619
|
+
const msgTokens = messageBreakdown.reduce((a, b) => a + b.tokens, 0);
|
|
1620
|
+
return {
|
|
1621
|
+
total: sysTokens + toolTokens + msgTokens,
|
|
1622
|
+
systemPrompt: sysTokens,
|
|
1623
|
+
tools: { total: toolTokens, count: input.tools.length, breakdown: toolBreakdown },
|
|
1624
|
+
messages: { total: msgTokens, count: input.messages.length, breakdown: messageBreakdown }
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1365
1628
|
// src/server/index.ts
|
|
1629
|
+
function errMessage(err) {
|
|
1630
|
+
return err instanceof Error ? err.message : String(err);
|
|
1631
|
+
}
|
|
1366
1632
|
async function startWebUI(opts = {}) {
|
|
1367
1633
|
const wsPort2 = opts.wsPort ?? 3457;
|
|
1368
1634
|
const wsHost2 = opts.wsHost ?? "127.0.0.1";
|
|
@@ -1597,10 +1863,10 @@ async function startWebUI(opts = {}) {
|
|
|
1597
1863
|
try {
|
|
1598
1864
|
const m = await modelsRegistry.getModel(config.provider, config.model);
|
|
1599
1865
|
maxContext = m?.capabilities?.maxContext ?? 0;
|
|
1600
|
-
const
|
|
1601
|
-
inputCost =
|
|
1602
|
-
outputCost =
|
|
1603
|
-
cacheReadCost =
|
|
1866
|
+
const rates = getCostRates(m);
|
|
1867
|
+
inputCost = rates.input;
|
|
1868
|
+
outputCost = rates.output;
|
|
1869
|
+
cacheReadCost = rates.cacheRead;
|
|
1604
1870
|
} catch {
|
|
1605
1871
|
}
|
|
1606
1872
|
return {
|
|
@@ -1620,59 +1886,25 @@ async function startWebUI(opts = {}) {
|
|
|
1620
1886
|
}
|
|
1621
1887
|
const wsToken = randomBytes(16).toString("hex");
|
|
1622
1888
|
console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
};
|
|
1631
|
-
const hostHeaderOk = (req) => {
|
|
1632
|
-
const boundToLoopback = wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
|
|
1633
|
-
if (!boundToLoopback) return true;
|
|
1634
|
-
const hostHeader = (req.headers.host ?? "").trim();
|
|
1635
|
-
if (!hostHeader) return false;
|
|
1636
|
-
let hostname;
|
|
1637
|
-
try {
|
|
1638
|
-
hostname = new URL(`http://${hostHeader}`).hostname;
|
|
1639
|
-
} catch {
|
|
1640
|
-
return false;
|
|
1641
|
-
}
|
|
1642
|
-
return isLoopback(hostname);
|
|
1643
|
-
};
|
|
1644
|
-
const verifyClient = (info) => {
|
|
1645
|
-
const origin = info.origin;
|
|
1646
|
-
const url = info.req.url ?? "";
|
|
1647
|
-
const tokenMatch = url.match(/[?&]token=([^&]+)/);
|
|
1648
|
-
const providedToken = tokenMatch ? tokenMatch[1] : void 0;
|
|
1649
|
-
const tokenOk = tokenMatches(providedToken);
|
|
1650
|
-
if (!hostHeaderOk(info.req)) return false;
|
|
1651
|
-
if (!origin) {
|
|
1652
|
-
const remoteIp = info.req.socket.remoteAddress ?? "";
|
|
1653
|
-
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
1654
|
-
if (!isRemoteLoopback && wsHost2 === "0.0.0.0") return false;
|
|
1655
|
-
return tokenOk || wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
|
|
1656
|
-
}
|
|
1657
|
-
try {
|
|
1658
|
-
const { hostname } = new URL(origin);
|
|
1659
|
-
if (isLoopback(hostname)) return true;
|
|
1660
|
-
return tokenOk;
|
|
1661
|
-
} catch {
|
|
1662
|
-
return false;
|
|
1663
|
-
}
|
|
1664
|
-
};
|
|
1889
|
+
const verifyClient2 = (info) => verifyClient({
|
|
1890
|
+
origin: info.origin,
|
|
1891
|
+
url: info.req.url ?? "",
|
|
1892
|
+
hostHeader: info.req.headers.host,
|
|
1893
|
+
remoteAddress: info.req.socket.remoteAddress,
|
|
1894
|
+
wsHost: wsHost2,
|
|
1895
|
+
expectedToken: wsToken
|
|
1896
|
+
});
|
|
1665
1897
|
const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
|
|
1666
1898
|
const wssPrimary = new WebSocketServer({
|
|
1667
1899
|
port: wsPort2,
|
|
1668
1900
|
host: wsHost2,
|
|
1669
|
-
verifyClient,
|
|
1901
|
+
verifyClient: verifyClient2,
|
|
1670
1902
|
maxPayload: WS_MAX_PAYLOAD
|
|
1671
1903
|
});
|
|
1672
1904
|
const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({
|
|
1673
1905
|
port: wsPort2,
|
|
1674
1906
|
host: "::1",
|
|
1675
|
-
verifyClient,
|
|
1907
|
+
verifyClient: verifyClient2,
|
|
1676
1908
|
maxPayload: WS_MAX_PAYLOAD
|
|
1677
1909
|
}) : null;
|
|
1678
1910
|
const clients = /* @__PURE__ */ new Map();
|
|
@@ -1924,7 +2156,7 @@ async function startWebUI(opts = {}) {
|
|
|
1924
2156
|
type: "error",
|
|
1925
2157
|
payload: {
|
|
1926
2158
|
phase: "agent.run",
|
|
1927
|
-
message:
|
|
2159
|
+
message: errMessage(err)
|
|
1928
2160
|
}
|
|
1929
2161
|
});
|
|
1930
2162
|
} finally {
|
|
@@ -1981,62 +2213,17 @@ async function startWebUI(opts = {}) {
|
|
|
1981
2213
|
break;
|
|
1982
2214
|
}
|
|
1983
2215
|
case "context.debug": {
|
|
1984
|
-
const
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
return JSON.stringify(c);
|
|
1989
|
-
} catch {
|
|
1990
|
-
return String(c);
|
|
1991
|
-
}
|
|
1992
|
-
};
|
|
1993
|
-
const sysTokens = context.systemPrompt.reduce((acc, b) => acc + estimate(b.text ?? ""), 0);
|
|
1994
|
-
const tools = toolRegistry.list();
|
|
1995
|
-
const toolBreakdown = tools.map((t) => {
|
|
1996
|
-
const schema = t.inputSchema ?? {};
|
|
1997
|
-
const desc = t.description ?? "";
|
|
1998
|
-
return {
|
|
1999
|
-
name: t.name,
|
|
2000
|
-
tokens: estimate(t.name) + estimate(desc) + estimate(stringifyContent(schema))
|
|
2001
|
-
};
|
|
2216
|
+
const breakdown = estimateContextBreakdown({
|
|
2217
|
+
systemPrompt: context.systemPrompt,
|
|
2218
|
+
tools: toolRegistry.list(),
|
|
2219
|
+
messages: context.messages
|
|
2002
2220
|
});
|
|
2003
|
-
const toolTokens = toolBreakdown.reduce((a, b) => a + b.tokens, 0);
|
|
2004
|
-
const messageBreakdown = context.messages.map((m, i) => {
|
|
2005
|
-
let tk = 0;
|
|
2006
|
-
if (typeof m.content === "string") {
|
|
2007
|
-
tk = estimate(m.content);
|
|
2008
|
-
} else if (Array.isArray(m.content)) {
|
|
2009
|
-
for (const b of m.content) {
|
|
2010
|
-
if (b.type === "text") tk += estimate(b.text ?? "");
|
|
2011
|
-
else if (b.type === "tool_use") tk += estimate(stringifyContent(b.input));
|
|
2012
|
-
else if (b.type === "tool_result") tk += estimate(stringifyContent(b.content));
|
|
2013
|
-
else tk += estimate(stringifyContent(b));
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
return {
|
|
2017
|
-
index: i,
|
|
2018
|
-
role: m.role,
|
|
2019
|
-
tokens: tk,
|
|
2020
|
-
preview: typeof m.content === "string" ? m.content.slice(0, 60) : Array.isArray(m.content) ? m.content.map(
|
|
2021
|
-
(b) => b.type === "text" ? (b.text ?? "").slice(0, 40) : b.type === "tool_use" ? `[tool_use: ${b.name}]` : b.type === "tool_result" ? `[tool_result]` : `[${b.type}]`
|
|
2022
|
-
).join(" ").slice(0, 60) : ""
|
|
2023
|
-
};
|
|
2024
|
-
});
|
|
2025
|
-
const msgTokens = messageBreakdown.reduce((a, b) => a + b.tokens, 0);
|
|
2026
|
-
const total = sysTokens + toolTokens + msgTokens;
|
|
2027
2221
|
send(ws, {
|
|
2028
2222
|
type: "context.debug",
|
|
2029
2223
|
payload: {
|
|
2030
|
-
|
|
2224
|
+
...breakdown,
|
|
2031
2225
|
mode: context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
2032
|
-
policy: context.meta["contextWindowPolicy"]
|
|
2033
|
-
systemPrompt: sysTokens,
|
|
2034
|
-
tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
|
|
2035
|
-
messages: {
|
|
2036
|
-
total: msgTokens,
|
|
2037
|
-
count: context.messages.length,
|
|
2038
|
-
breakdown: messageBreakdown
|
|
2039
|
-
}
|
|
2226
|
+
policy: context.meta["contextWindowPolicy"]
|
|
2040
2227
|
}
|
|
2041
2228
|
});
|
|
2042
2229
|
break;
|
|
@@ -2061,7 +2248,7 @@ async function startWebUI(opts = {}) {
|
|
|
2061
2248
|
`Compacted: ${report.before} \u2192 ${report.after} tokens (saved ~${Math.max(0, report.before - report.after)})`
|
|
2062
2249
|
);
|
|
2063
2250
|
} catch (err) {
|
|
2064
|
-
sendResult(ws, false,
|
|
2251
|
+
sendResult(ws, false, errMessage(err));
|
|
2065
2252
|
}
|
|
2066
2253
|
break;
|
|
2067
2254
|
}
|
|
@@ -2201,7 +2388,7 @@ async function startWebUI(opts = {}) {
|
|
|
2201
2388
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2202
2389
|
try {
|
|
2203
2390
|
configWriteLock = configWriteLock.then(async () => {
|
|
2204
|
-
const raw = await
|
|
2391
|
+
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2205
2392
|
const parsed = JSON.parse(raw);
|
|
2206
2393
|
parsed.provider = newProvider;
|
|
2207
2394
|
parsed.model = newModel;
|
|
@@ -2220,7 +2407,7 @@ async function startWebUI(opts = {}) {
|
|
|
2220
2407
|
type: "key.operation_result",
|
|
2221
2408
|
payload: {
|
|
2222
2409
|
success: false,
|
|
2223
|
-
message: `Switch failed: ${
|
|
2410
|
+
message: `Switch failed: ${errMessage(err)}`
|
|
2224
2411
|
}
|
|
2225
2412
|
});
|
|
2226
2413
|
break;
|
|
@@ -2275,7 +2462,7 @@ async function startWebUI(opts = {}) {
|
|
|
2275
2462
|
} catch (err) {
|
|
2276
2463
|
send(ws, {
|
|
2277
2464
|
type: "sessions.list",
|
|
2278
|
-
payload: { sessions: [], error:
|
|
2465
|
+
payload: { sessions: [], error: errMessage(err) }
|
|
2279
2466
|
});
|
|
2280
2467
|
}
|
|
2281
2468
|
break;
|
|
@@ -2290,7 +2477,7 @@ async function startWebUI(opts = {}) {
|
|
|
2290
2477
|
await sessionStore.delete(id);
|
|
2291
2478
|
sendResult(ws, true, `Session ${id} deleted`);
|
|
2292
2479
|
} catch (err) {
|
|
2293
|
-
sendResult(ws, false,
|
|
2480
|
+
sendResult(ws, false, errMessage(err));
|
|
2294
2481
|
}
|
|
2295
2482
|
break;
|
|
2296
2483
|
}
|
|
@@ -2325,7 +2512,7 @@ async function startWebUI(opts = {}) {
|
|
|
2325
2512
|
});
|
|
2326
2513
|
sendResult(ws, true, `Resumed session ${id}`);
|
|
2327
2514
|
} catch (err) {
|
|
2328
|
-
sendResult(ws, false,
|
|
2515
|
+
sendResult(ws, false, errMessage(err));
|
|
2329
2516
|
}
|
|
2330
2517
|
break;
|
|
2331
2518
|
}
|
|
@@ -2353,7 +2540,7 @@ async function startWebUI(opts = {}) {
|
|
|
2353
2540
|
} catch (err) {
|
|
2354
2541
|
send(ws, {
|
|
2355
2542
|
type: "memory.list",
|
|
2356
|
-
payload: { text: "", error:
|
|
2543
|
+
payload: { text: "", error: errMessage(err) }
|
|
2357
2544
|
});
|
|
2358
2545
|
}
|
|
2359
2546
|
break;
|
|
@@ -2364,7 +2551,7 @@ async function startWebUI(opts = {}) {
|
|
|
2364
2551
|
await memoryStore.remember(text, scope ?? "project-memory");
|
|
2365
2552
|
sendResult(ws, true, "Saved to memory");
|
|
2366
2553
|
} catch (err) {
|
|
2367
|
-
sendResult(ws, false,
|
|
2554
|
+
sendResult(ws, false, errMessage(err));
|
|
2368
2555
|
}
|
|
2369
2556
|
break;
|
|
2370
2557
|
}
|
|
@@ -2378,7 +2565,7 @@ async function startWebUI(opts = {}) {
|
|
|
2378
2565
|
removed > 0 ? `Removed ${removed} entr${removed === 1 ? "y" : "ies"}` : "No matching entries"
|
|
2379
2566
|
);
|
|
2380
2567
|
} catch (err) {
|
|
2381
|
-
sendResult(ws, false,
|
|
2568
|
+
sendResult(ws, false, errMessage(err));
|
|
2382
2569
|
}
|
|
2383
2570
|
break;
|
|
2384
2571
|
}
|
|
@@ -2412,7 +2599,7 @@ async function startWebUI(opts = {}) {
|
|
|
2412
2599
|
payload: {
|
|
2413
2600
|
skills: [],
|
|
2414
2601
|
enabled: true,
|
|
2415
|
-
error:
|
|
2602
|
+
error: errMessage(err)
|
|
2416
2603
|
}
|
|
2417
2604
|
});
|
|
2418
2605
|
}
|
|
@@ -2506,44 +2693,25 @@ async function startWebUI(opts = {}) {
|
|
|
2506
2693
|
payload: { plan }
|
|
2507
2694
|
});
|
|
2508
2695
|
} catch (err) {
|
|
2509
|
-
sendResult(ws, false,
|
|
2696
|
+
sendResult(ws, false, errMessage(err));
|
|
2510
2697
|
}
|
|
2511
2698
|
break;
|
|
2512
2699
|
}
|
|
2513
2700
|
case "files.list": {
|
|
2514
2701
|
const payload = msg.payload ?? {};
|
|
2515
|
-
const query = (payload.query ?? "").toLowerCase();
|
|
2516
2702
|
const limit = payload.limit ?? 50;
|
|
2517
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2518
|
-
".git",
|
|
2519
|
-
"node_modules",
|
|
2520
|
-
"dist",
|
|
2521
|
-
"build",
|
|
2522
|
-
".next",
|
|
2523
|
-
".turbo",
|
|
2524
|
-
".cache",
|
|
2525
|
-
"target",
|
|
2526
|
-
"coverage",
|
|
2527
|
-
".nyc_output",
|
|
2528
|
-
"out",
|
|
2529
|
-
".pnpm-store",
|
|
2530
|
-
".parcel-cache"
|
|
2531
|
-
]);
|
|
2532
2703
|
const results = [];
|
|
2533
2704
|
async function walk(dir, rel, depth) {
|
|
2534
2705
|
if (depth > 8 || results.length >= 600) return;
|
|
2535
2706
|
let entries = [];
|
|
2536
2707
|
try {
|
|
2537
|
-
entries = await
|
|
2708
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
2538
2709
|
} catch {
|
|
2539
2710
|
return;
|
|
2540
2711
|
}
|
|
2541
2712
|
for (const e of entries) {
|
|
2542
2713
|
if (results.length >= 600) return;
|
|
2543
|
-
if (
|
|
2544
|
-
if (e.name !== ".gitignore" && e.name !== ".eslintrc" && e.name !== ".prettierrc")
|
|
2545
|
-
continue;
|
|
2546
|
-
}
|
|
2714
|
+
if (isHiddenEntry(e.name)) continue;
|
|
2547
2715
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2548
2716
|
if (e.isDirectory()) {
|
|
2549
2717
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
@@ -2554,26 +2722,9 @@ async function startWebUI(opts = {}) {
|
|
|
2554
2722
|
}
|
|
2555
2723
|
}
|
|
2556
2724
|
await walk(projectRoot, "", 0);
|
|
2557
|
-
const scored = [];
|
|
2558
|
-
for (const p of results) {
|
|
2559
|
-
if (!query) {
|
|
2560
|
-
scored.push({ path: p, score: 0 });
|
|
2561
|
-
continue;
|
|
2562
|
-
}
|
|
2563
|
-
const lower = p.toLowerCase();
|
|
2564
|
-
const base = lower.split("/").pop() ?? lower;
|
|
2565
|
-
let score = 0;
|
|
2566
|
-
if (base === query) score = 100;
|
|
2567
|
-
else if (base.startsWith(query)) score = 60;
|
|
2568
|
-
else if (lower.includes(query)) score = 20;
|
|
2569
|
-
else continue;
|
|
2570
|
-
score -= p.split("/").length;
|
|
2571
|
-
scored.push({ path: p, score });
|
|
2572
|
-
}
|
|
2573
|
-
scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
2574
2725
|
send(ws, {
|
|
2575
2726
|
type: "files.list",
|
|
2576
|
-
payload: { files:
|
|
2727
|
+
payload: { files: rankFiles(results, payload.query ?? "", limit) }
|
|
2577
2728
|
});
|
|
2578
2729
|
break;
|
|
2579
2730
|
}
|
|
@@ -2599,7 +2750,7 @@ async function startWebUI(opts = {}) {
|
|
|
2599
2750
|
payload: {
|
|
2600
2751
|
modes: [],
|
|
2601
2752
|
activeId: "default",
|
|
2602
|
-
error:
|
|
2753
|
+
error: errMessage(err)
|
|
2603
2754
|
}
|
|
2604
2755
|
});
|
|
2605
2756
|
}
|
|
@@ -2638,7 +2789,7 @@ async function startWebUI(opts = {}) {
|
|
|
2638
2789
|
payload: { ...await sessionStartPayload() }
|
|
2639
2790
|
});
|
|
2640
2791
|
} catch (err) {
|
|
2641
|
-
sendResult(ws, false,
|
|
2792
|
+
sendResult(ws, false, errMessage(err));
|
|
2642
2793
|
}
|
|
2643
2794
|
break;
|
|
2644
2795
|
}
|
|
@@ -2646,10 +2797,7 @@ async function startWebUI(opts = {}) {
|
|
|
2646
2797
|
const usage = tokenCounter.total();
|
|
2647
2798
|
const cacheStats = tokenCounter.cacheStats();
|
|
2648
2799
|
const m = await modelsRegistry.getModel(config.provider, config.model).catch(() => null);
|
|
2649
|
-
const
|
|
2650
|
-
const outputCost = m?.cost?.output ?? 0;
|
|
2651
|
-
const cacheReadCost = m?.cost?.cache_read ?? 0;
|
|
2652
|
-
const cost = (usage.input * inputCost + usage.output * outputCost + (usage.cacheRead ?? 0) * cacheReadCost) / 1e6;
|
|
2800
|
+
const cost = computeUsageCost(usage, getCostRates(m));
|
|
2653
2801
|
send(ws, {
|
|
2654
2802
|
type: "stats.get",
|
|
2655
2803
|
payload: {
|
|
@@ -2677,7 +2825,7 @@ async function startWebUI(opts = {}) {
|
|
|
2677
2825
|
}
|
|
2678
2826
|
async function loadSavedProviders() {
|
|
2679
2827
|
try {
|
|
2680
|
-
const raw = await
|
|
2828
|
+
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2681
2829
|
const parsed = JSON.parse(raw);
|
|
2682
2830
|
if (!parsed.providers) return {};
|
|
2683
2831
|
return decryptConfigSecrets(parsed.providers, vault);
|
|
@@ -2689,7 +2837,7 @@ async function startWebUI(opts = {}) {
|
|
|
2689
2837
|
configWriteLock = configWriteLock.then(async () => {
|
|
2690
2838
|
let parsed;
|
|
2691
2839
|
try {
|
|
2692
|
-
const raw = await
|
|
2840
|
+
const raw = await fs2.readFile(globalConfigPath, "utf8");
|
|
2693
2841
|
parsed = JSON.parse(raw);
|
|
2694
2842
|
} catch {
|
|
2695
2843
|
parsed = {};
|
|
@@ -2700,134 +2848,57 @@ async function startWebUI(opts = {}) {
|
|
|
2700
2848
|
});
|
|
2701
2849
|
await configWriteLock;
|
|
2702
2850
|
}
|
|
2703
|
-
function normalizeKeys(cfg) {
|
|
2704
|
-
if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
|
|
2705
|
-
return cfg.apiKeys.map((k) => ({ ...k }));
|
|
2706
|
-
}
|
|
2707
|
-
if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
|
|
2708
|
-
return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
|
|
2709
|
-
}
|
|
2710
|
-
return [];
|
|
2711
|
-
}
|
|
2712
|
-
function writeKeysBack(cfg, keys) {
|
|
2713
|
-
if (keys.length === 0) {
|
|
2714
|
-
delete cfg.apiKeys;
|
|
2715
|
-
delete cfg.apiKey;
|
|
2716
|
-
delete cfg.activeKey;
|
|
2717
|
-
return;
|
|
2718
|
-
}
|
|
2719
|
-
cfg.apiKeys = keys;
|
|
2720
|
-
const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
|
|
2721
|
-
cfg.apiKey = active.apiKey;
|
|
2722
|
-
if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
|
|
2723
|
-
cfg.activeKey = active.label;
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
function maskedKey(key) {
|
|
2727
|
-
if (!key) return "\u2014";
|
|
2728
|
-
if (key.length <= 8) return "\u2022".repeat(key.length);
|
|
2729
|
-
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
2730
|
-
}
|
|
2731
2851
|
function sendResult(ws, success, message) {
|
|
2732
2852
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
2733
2853
|
}
|
|
2734
2854
|
async function handleKeyUpsert(ws, providerId, label, apiKey) {
|
|
2735
2855
|
try {
|
|
2736
2856
|
const providers = await loadSavedProviders();
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2741
|
-
if (idx >= 0) {
|
|
2742
|
-
keys[idx] = { ...keys[idx], apiKey, createdAt: nowIso };
|
|
2743
|
-
} else {
|
|
2744
|
-
keys.push({ label, apiKey, createdAt: nowIso });
|
|
2745
|
-
}
|
|
2746
|
-
writeKeysBack(existing, keys);
|
|
2747
|
-
if (!existing.activeKey) existing.activeKey = label;
|
|
2748
|
-
providers[providerId] = existing;
|
|
2749
|
-
await saveProviders(providers);
|
|
2750
|
-
sendResult(ws, true, `Key "${label}" saved for ${providerId}`);
|
|
2857
|
+
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
2858
|
+
if (result.ok) await saveProviders(providers);
|
|
2859
|
+
sendResult(ws, result.ok, result.message);
|
|
2751
2860
|
} catch (err) {
|
|
2752
|
-
sendResult(ws, false,
|
|
2861
|
+
sendResult(ws, false, errMessage(err));
|
|
2753
2862
|
}
|
|
2754
2863
|
}
|
|
2755
2864
|
async function handleKeyDelete(ws, providerId, label) {
|
|
2756
2865
|
try {
|
|
2757
2866
|
const providers = await loadSavedProviders();
|
|
2758
|
-
const
|
|
2759
|
-
if (
|
|
2760
|
-
|
|
2761
|
-
return;
|
|
2762
|
-
}
|
|
2763
|
-
const keys = normalizeKeys(existing).filter((k) => k.label !== label);
|
|
2764
|
-
if (keys.length === 0) {
|
|
2765
|
-
delete providers[providerId];
|
|
2766
|
-
} else {
|
|
2767
|
-
writeKeysBack(existing, keys);
|
|
2768
|
-
if (existing.activeKey === label) existing.activeKey = keys[0].label;
|
|
2769
|
-
providers[providerId] = existing;
|
|
2770
|
-
}
|
|
2771
|
-
await saveProviders(providers);
|
|
2772
|
-
sendResult(ws, true, `Key "${label}" deleted from ${providerId}`);
|
|
2867
|
+
const result = deleteKey(providers, providerId, label);
|
|
2868
|
+
if (result.ok) await saveProviders(providers);
|
|
2869
|
+
sendResult(ws, result.ok, result.message);
|
|
2773
2870
|
} catch (err) {
|
|
2774
|
-
sendResult(ws, false,
|
|
2871
|
+
sendResult(ws, false, errMessage(err));
|
|
2775
2872
|
}
|
|
2776
2873
|
}
|
|
2777
2874
|
async function handleKeySetActive(ws, providerId, label) {
|
|
2778
2875
|
try {
|
|
2779
2876
|
const providers = await loadSavedProviders();
|
|
2780
|
-
const
|
|
2781
|
-
if (
|
|
2782
|
-
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
existing.activeKey = label;
|
|
2786
|
-
writeKeysBack(existing, normalizeKeys(existing));
|
|
2787
|
-
providers[providerId] = existing;
|
|
2788
|
-
await saveProviders(providers);
|
|
2789
|
-
sendResult(ws, true, `Active key for ${providerId} set to "${label}"`);
|
|
2877
|
+
const result = setActiveKey(providers, providerId, label);
|
|
2878
|
+
if (result.ok) await saveProviders(providers);
|
|
2879
|
+
sendResult(ws, result.ok, result.message);
|
|
2790
2880
|
} catch (err) {
|
|
2791
|
-
sendResult(ws, false,
|
|
2881
|
+
sendResult(ws, false, errMessage(err));
|
|
2792
2882
|
}
|
|
2793
2883
|
}
|
|
2794
2884
|
async function handleProviderAdd(ws, payload) {
|
|
2795
2885
|
try {
|
|
2796
2886
|
const providers = await loadSavedProviders();
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
}
|
|
2801
|
-
const newProv = {
|
|
2802
|
-
type: payload.id,
|
|
2803
|
-
family: payload.family,
|
|
2804
|
-
baseUrl: payload.baseUrl
|
|
2805
|
-
};
|
|
2806
|
-
if (payload.apiKey) {
|
|
2807
|
-
newProv.apiKeys = [
|
|
2808
|
-
{ label: "default", apiKey: payload.apiKey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2809
|
-
];
|
|
2810
|
-
newProv.activeKey = "default";
|
|
2811
|
-
}
|
|
2812
|
-
providers[payload.id] = newProv;
|
|
2813
|
-
await saveProviders(providers);
|
|
2814
|
-
sendResult(ws, true, `Provider "${payload.id}" added`);
|
|
2887
|
+
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
2888
|
+
if (result.ok) await saveProviders(providers);
|
|
2889
|
+
sendResult(ws, result.ok, result.message);
|
|
2815
2890
|
} catch (err) {
|
|
2816
|
-
sendResult(ws, false,
|
|
2891
|
+
sendResult(ws, false, errMessage(err));
|
|
2817
2892
|
}
|
|
2818
2893
|
}
|
|
2819
2894
|
async function handleProviderRemove(ws, providerId) {
|
|
2820
2895
|
try {
|
|
2821
2896
|
const providers = await loadSavedProviders();
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
}
|
|
2826
|
-
delete providers[providerId];
|
|
2827
|
-
await saveProviders(providers);
|
|
2828
|
-
sendResult(ws, true, `Provider "${providerId}" removed`);
|
|
2897
|
+
const result = removeProvider(providers, providerId);
|
|
2898
|
+
if (result.ok) await saveProviders(providers);
|
|
2899
|
+
sendResult(ws, result.ok, result.message);
|
|
2829
2900
|
} catch (err) {
|
|
2830
|
-
sendResult(ws, false,
|
|
2901
|
+
sendResult(ws, false, errMessage(err));
|
|
2831
2902
|
}
|
|
2832
2903
|
}
|
|
2833
2904
|
const httpServer = createHttpServer({
|
|
@@ -2839,26 +2910,18 @@ async function startWebUI(opts = {}) {
|
|
|
2839
2910
|
httpServer.listen(httpPort, wsHost2, () => {
|
|
2840
2911
|
console.log(`[WebUI] HTTP server running on http://${wsHost2}:${httpPort}`);
|
|
2841
2912
|
});
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
try {
|
|
2913
|
+
registerShutdownHandlers({
|
|
2914
|
+
flushSession: async () => {
|
|
2845
2915
|
await session.append({
|
|
2846
2916
|
type: "session_end",
|
|
2847
2917
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2848
2918
|
usage: tokenCounter.total()
|
|
2849
2919
|
});
|
|
2850
2920
|
await session.close();
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
httpServer.close();
|
|
2856
|
-
wssPrimary.close();
|
|
2857
|
-
wssSecondary?.close();
|
|
2858
|
-
process.exit(0);
|
|
2859
|
-
};
|
|
2860
|
-
process.on("SIGINT", shutdown);
|
|
2861
|
-
process.on("SIGTERM", shutdown);
|
|
2921
|
+
},
|
|
2922
|
+
clients: () => clients.keys(),
|
|
2923
|
+
servers: [httpServer, wssPrimary, wssSecondary]
|
|
2924
|
+
});
|
|
2862
2925
|
}
|
|
2863
2926
|
|
|
2864
2927
|
// src/server/entry.ts
|