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