billion-context 0.1.23 → 0.1.24
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 +95 -175
- package/README.zh-CN.md +70 -125
- package/dist/index.js +311 -137
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b2) => (typeof require !== "undefined" ? require : a)[b2]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/config.ts
|
|
4
10
|
import { defaultConfig } from "acp-kernel";
|
|
@@ -99,13 +105,13 @@ function closeLogger() {
|
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// src/config.ts
|
|
102
|
-
function safeReadJson(
|
|
108
|
+
function safeReadJson(path7) {
|
|
103
109
|
try {
|
|
104
|
-
const raw = readFileSync(
|
|
110
|
+
const raw = readFileSync(path7, "utf8").replace(/^\uFEFF/, "");
|
|
105
111
|
return JSON.parse(raw);
|
|
106
112
|
} catch (e) {
|
|
107
113
|
if (e.code !== "ENOENT") {
|
|
108
|
-
log("error", `[acp-config] failed to parse ${
|
|
114
|
+
log("error", `[acp-config] failed to parse ${path7}: ${String(e)}`);
|
|
109
115
|
}
|
|
110
116
|
return void 0;
|
|
111
117
|
}
|
|
@@ -134,17 +140,41 @@ function lookupContextLimit(model) {
|
|
|
134
140
|
}
|
|
135
141
|
return void 0;
|
|
136
142
|
}
|
|
137
|
-
function resolveContextLimit(routes,
|
|
138
|
-
if (!model) return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
if (m2?.context && m2.context > 0) return m2.context;
|
|
143
|
+
function resolveContextLimit(routes, upstreamUrl, model) {
|
|
144
|
+
if (!model || !upstreamUrl) return lookupContextLimit(model);
|
|
145
|
+
let bestKey = "";
|
|
146
|
+
for (const key of Object.keys(routes)) {
|
|
147
|
+
if (upstreamUrl === key || upstreamUrl.startsWith(key + "/")) {
|
|
148
|
+
if (key.length > bestKey.length) bestKey = key;
|
|
144
149
|
}
|
|
145
150
|
}
|
|
151
|
+
if (bestKey) {
|
|
152
|
+
const m2 = routes[bestKey].models?.[model];
|
|
153
|
+
if (m2?.context && m2.context > 0) return m2.context;
|
|
154
|
+
}
|
|
146
155
|
return lookupContextLimit(model);
|
|
147
156
|
}
|
|
157
|
+
function loadRoutes(env = process.env) {
|
|
158
|
+
const fileConfig = loadConfigFile();
|
|
159
|
+
const routes = {};
|
|
160
|
+
const routesPath = env.ACP_PROVIDERS ?? fileConfig.providersPath ?? "";
|
|
161
|
+
if (routesPath) {
|
|
162
|
+
const parsed = safeReadJson(routesPath);
|
|
163
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
164
|
+
for (const [k2, v2] of Object.entries(parsed)) {
|
|
165
|
+
const route = parseRouteEntry(v2);
|
|
166
|
+
if (route) routes[normalizeUrlKey(k2)] = route;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (fileConfig.providers) {
|
|
171
|
+
for (const [k2, v2] of Object.entries(fileConfig.providers)) {
|
|
172
|
+
const route = parseRouteEntry(v2);
|
|
173
|
+
if (route && !routes[normalizeUrlKey(k2)]) routes[normalizeUrlKey(k2)] = route;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return routes;
|
|
177
|
+
}
|
|
148
178
|
function loadOptions(env = process.env) {
|
|
149
179
|
const fileConfig = loadConfigFile();
|
|
150
180
|
const port = parseInt(env.ACP_PORT ?? env.PORT ?? `${fileConfig.port ?? 8787}`, 10);
|
|
@@ -157,14 +187,14 @@ function loadOptions(env = process.env) {
|
|
|
157
187
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
158
188
|
for (const [k2, v2] of Object.entries(parsed)) {
|
|
159
189
|
const route = parseRouteEntry(v2);
|
|
160
|
-
if (route) routes[k2] = route;
|
|
190
|
+
if (route) routes[normalizeUrlKey(k2)] = route;
|
|
161
191
|
}
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
194
|
if (fileConfig.providers) {
|
|
165
195
|
for (const [k2, v2] of Object.entries(fileConfig.providers)) {
|
|
166
196
|
const route = parseRouteEntry(v2);
|
|
167
|
-
if (route && !routes[k2]) routes[k2] = route;
|
|
197
|
+
if (route && !routes[normalizeUrlKey(k2)]) routes[normalizeUrlKey(k2)] = route;
|
|
168
198
|
}
|
|
169
199
|
}
|
|
170
200
|
const modelContextLimit = parseInt(env.ACP_MODEL_CONTEXT_LIMIT ?? `${fileConfig.modelContextLimit ?? 2e5}`, 10);
|
|
@@ -211,14 +241,15 @@ function ensureConfigTemplate() {
|
|
|
211
241
|
return false;
|
|
212
242
|
}
|
|
213
243
|
}
|
|
244
|
+
function normalizeUrlKey(key) {
|
|
245
|
+
return key.replace(/\/+$/, "");
|
|
246
|
+
}
|
|
214
247
|
function parseRouteEntry(v2) {
|
|
215
|
-
if (typeof v2 === "
|
|
216
|
-
return { url: v2.replace(/\/$/, "") };
|
|
217
|
-
}
|
|
218
|
-
if (v2 && typeof v2 === "object" && !Array.isArray(v2) && typeof v2.url === "string" && v2.url.length > 0) {
|
|
248
|
+
if (v2 && typeof v2 === "object" && !Array.isArray(v2)) {
|
|
219
249
|
const obj = v2;
|
|
220
|
-
return {
|
|
250
|
+
return { models: obj.models };
|
|
221
251
|
}
|
|
252
|
+
if (v2 === null) return {};
|
|
222
253
|
return void 0;
|
|
223
254
|
}
|
|
224
255
|
|
|
@@ -228,6 +259,126 @@ import fs2 from "fs";
|
|
|
228
259
|
import { tmpdir as tmpdir2 } from "os";
|
|
229
260
|
import { createCore, estimateTokensFast as estimateTokensFast3, renderNudgeText, deactivateBlock as deactivateBlock4 } from "acp-kernel";
|
|
230
261
|
|
|
262
|
+
// src/registry.ts
|
|
263
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
264
|
+
import { existsSync as existsSync2 } from "fs";
|
|
265
|
+
import path3 from "path";
|
|
266
|
+
var REGISTRY_URL = "https://models.dev/models.json";
|
|
267
|
+
var CACHE_FILE = path3.join(cacheDir(), "models-dev.json");
|
|
268
|
+
var TTL_MS = 24 * 60 * 60 * 1e3;
|
|
269
|
+
var cache = null;
|
|
270
|
+
var loading = null;
|
|
271
|
+
function parse(raw) {
|
|
272
|
+
try {
|
|
273
|
+
const parsed = JSON.parse(raw);
|
|
274
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
275
|
+
return parsed;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
async function readDiskCache() {
|
|
282
|
+
try {
|
|
283
|
+
const raw = await readFile(CACHE_FILE, "utf8");
|
|
284
|
+
return parse(raw);
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function writeDiskCache(data) {
|
|
290
|
+
try {
|
|
291
|
+
await mkdir(path3.dirname(CACHE_FILE), { recursive: true });
|
|
292
|
+
await writeFile(CACHE_FILE, JSON.stringify(data), "utf8");
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function diskCacheFresh() {
|
|
297
|
+
if (!existsSync2(CACHE_FILE)) return false;
|
|
298
|
+
try {
|
|
299
|
+
const { mtimeMs } = __require("fs").statSync(CACHE_FILE);
|
|
300
|
+
return Date.now() - mtimeMs < TTL_MS;
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function fetchFresh() {
|
|
306
|
+
try {
|
|
307
|
+
const res = await fetch(REGISTRY_URL, {
|
|
308
|
+
signal: AbortSignal.timeout(15e3),
|
|
309
|
+
headers: { Accept: "application/json" }
|
|
310
|
+
});
|
|
311
|
+
if (!res.ok) return null;
|
|
312
|
+
const text = await res.text();
|
|
313
|
+
const parsed = parse(text);
|
|
314
|
+
if (parsed) await writeDiskCache(parsed);
|
|
315
|
+
return parsed;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function loadRegistry() {
|
|
321
|
+
if (cache) return cache;
|
|
322
|
+
if (diskCacheFresh()) {
|
|
323
|
+
const disk = await readDiskCache();
|
|
324
|
+
if (disk) {
|
|
325
|
+
cache = disk;
|
|
326
|
+
return cache;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (loading) return loading;
|
|
330
|
+
loading = (async () => {
|
|
331
|
+
const fresh = await fetchFresh();
|
|
332
|
+
if (fresh) {
|
|
333
|
+
cache = fresh;
|
|
334
|
+
log("info", `[acp-registry] loaded models.dev (${Object.keys(fresh).length} models)`);
|
|
335
|
+
return fresh;
|
|
336
|
+
}
|
|
337
|
+
const disk = await readDiskCache();
|
|
338
|
+
if (disk) {
|
|
339
|
+
cache = disk;
|
|
340
|
+
log("info", `[acp-registry] using stale disk cache (${Object.keys(disk).length} models, fetch failed)`);
|
|
341
|
+
return disk;
|
|
342
|
+
}
|
|
343
|
+
log("warn", `[acp-registry] could not load models.dev registry (offline + no cache)`);
|
|
344
|
+
return null;
|
|
345
|
+
})();
|
|
346
|
+
const result = await loading;
|
|
347
|
+
loading = null;
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
var HOST_TO_PROVIDER = {
|
|
351
|
+
"api.anthropic.com": "anthropic",
|
|
352
|
+
"api.openai.com": "openai",
|
|
353
|
+
"open.bigmodel.cn": "zhipuai",
|
|
354
|
+
"open.bigmodel.com": "zhipuai",
|
|
355
|
+
"coding.dashscope.aliyuncs.com": "dashscope",
|
|
356
|
+
"api.deepseek.com": "deepseek",
|
|
357
|
+
"api.moonshot.cn": "moonshot",
|
|
358
|
+
"generativelanguage.googleapis.com": "google",
|
|
359
|
+
"ai.comfly.org": "comfly"
|
|
360
|
+
};
|
|
361
|
+
function providerFromHost(host) {
|
|
362
|
+
const lower = host.toLowerCase();
|
|
363
|
+
if (HOST_TO_PROVIDER[lower]) return HOST_TO_PROVIDER[lower];
|
|
364
|
+
for (const [h, p2] of Object.entries(HOST_TO_PROVIDER)) {
|
|
365
|
+
if (lower.endsWith(h) || h.endsWith(lower)) return p2;
|
|
366
|
+
}
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
async function contextFromRegistry(model, host) {
|
|
370
|
+
const reg = await loadRegistry();
|
|
371
|
+
if (!reg || !model) return void 0;
|
|
372
|
+
const provider = host ? providerFromHost(host) : void 0;
|
|
373
|
+
const candidates = provider ? [`${provider}/${model}`, model] : [model];
|
|
374
|
+
for (const key of candidates) {
|
|
375
|
+
const entry = reg[key];
|
|
376
|
+
const ctx = entry?.limit?.context;
|
|
377
|
+
if (typeof ctx === "number" && ctx > 0) return ctx;
|
|
378
|
+
}
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
231
382
|
// src/fetch-util.ts
|
|
232
383
|
var MAX_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
233
384
|
var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -760,9 +911,9 @@ import { createInitialState as createInitialState2 } from "acp-kernel";
|
|
|
760
911
|
|
|
761
912
|
// src/persist.ts
|
|
762
913
|
import { promises as fs } from "fs";
|
|
763
|
-
import { existsSync as
|
|
914
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
764
915
|
import { createHash as createHash2 } from "crypto";
|
|
765
|
-
import * as
|
|
916
|
+
import * as path4 from "path";
|
|
766
917
|
import { createInitialState } from "acp-kernel";
|
|
767
918
|
var PERSIST_VERSION = 2;
|
|
768
919
|
function mergeState(parsed) {
|
|
@@ -788,7 +939,7 @@ function hostLabel(upstreamOrigin) {
|
|
|
788
939
|
function relPathFor(id, protocol, upstreamOrigin) {
|
|
789
940
|
const proto = protocol ?? "_unknown";
|
|
790
941
|
const host = protocol ? hostLabel(upstreamOrigin) + "_" : "";
|
|
791
|
-
return
|
|
942
|
+
return path4.join(proto, `${host}${createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24)}.json`);
|
|
792
943
|
}
|
|
793
944
|
function legacyFileNameFor(id) {
|
|
794
945
|
return createHash2("sha256").update(id, "utf8").digest("hex").slice(0, 24) + ".json";
|
|
@@ -808,13 +959,13 @@ var SessionStore = class {
|
|
|
808
959
|
this.log = opts?.log ?? defaultLogger;
|
|
809
960
|
}
|
|
810
961
|
filePath(id, protocol, upstreamOrigin) {
|
|
811
|
-
return
|
|
962
|
+
return path4.join(this.dir, relPathFor(id, protocol, upstreamOrigin));
|
|
812
963
|
}
|
|
813
964
|
/** A unique temp path per write (per process). Two overlapping writes for
|
|
814
965
|
* the same session must not share a temp file, or one rename invalidates
|
|
815
966
|
* the other. */
|
|
816
967
|
tempPath(id) {
|
|
817
|
-
return
|
|
968
|
+
return path4.join(this.dir, `.tmp-${legacyFileNameFor(id)}-${process.pid}-${this.tmpSeq++}`);
|
|
818
969
|
}
|
|
819
970
|
/** Bulk-load every persisted session from disk into a map keyed by the
|
|
820
971
|
* REAL session id (read from the file body, not the filename). Called once
|
|
@@ -838,7 +989,7 @@ var SessionStore = class {
|
|
|
838
989
|
}
|
|
839
990
|
for (const e of entries) {
|
|
840
991
|
if (e.name.startsWith(".tmp-")) continue;
|
|
841
|
-
const full =
|
|
992
|
+
const full = path4.join(dir, e.name);
|
|
842
993
|
if (e.isDirectory()) {
|
|
843
994
|
await walk(full);
|
|
844
995
|
} else if (e.isFile() && e.name.endsWith(".json")) {
|
|
@@ -848,14 +999,14 @@ var SessionStore = class {
|
|
|
848
999
|
};
|
|
849
1000
|
await walk(this.dir);
|
|
850
1001
|
for (const full of files) {
|
|
851
|
-
const name =
|
|
1002
|
+
const name = path4.basename(full);
|
|
852
1003
|
try {
|
|
853
1004
|
const parsed = JSON.parse(await fs.readFile(full, "utf8"));
|
|
854
1005
|
if (!isValidRecord(parsed)) continue;
|
|
855
1006
|
const pm = parsed.meta ?? {};
|
|
856
1007
|
const proto = pm.protocol ?? parsed.protocol;
|
|
857
1008
|
const origin = pm.upstreamOrigin ?? parsed.upstreamOrigin;
|
|
858
|
-
const expectedNamespaced =
|
|
1009
|
+
const expectedNamespaced = path4.basename(relPathFor(parsed.id, proto, origin));
|
|
859
1010
|
const expectedLegacy = legacyFileNameFor(parsed.id);
|
|
860
1011
|
if (name !== expectedNamespaced && name !== expectedLegacy) {
|
|
861
1012
|
this.log("warn", `[persist] skipping ${full}: filename does not match body id (expected ${expectedNamespaced})`);
|
|
@@ -877,7 +1028,7 @@ var SessionStore = class {
|
|
|
877
1028
|
const candidates = [this.filePath(id, meta?.protocol, meta?.upstreamOrigin)];
|
|
878
1029
|
if (meta?.protocol) candidates.push(this.filePath(id));
|
|
879
1030
|
for (const file of candidates) {
|
|
880
|
-
if (!
|
|
1031
|
+
if (!existsSync3(file)) continue;
|
|
881
1032
|
try {
|
|
882
1033
|
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
883
1034
|
if (!isValidRecord(parsed) || parsed.id !== id) continue;
|
|
@@ -910,7 +1061,7 @@ var SessionStore = class {
|
|
|
910
1061
|
const record = buildRecord(session);
|
|
911
1062
|
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
912
1063
|
try {
|
|
913
|
-
await fs.mkdir(
|
|
1064
|
+
await fs.mkdir(path4.dirname(file), { recursive: true });
|
|
914
1065
|
} catch (e) {
|
|
915
1066
|
this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
|
|
916
1067
|
}
|
|
@@ -944,7 +1095,7 @@ var SessionStore = class {
|
|
|
944
1095
|
const record = buildRecord(session);
|
|
945
1096
|
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
946
1097
|
try {
|
|
947
|
-
mkdirSync3(
|
|
1098
|
+
mkdirSync3(path4.dirname(file), { recursive: true });
|
|
948
1099
|
} catch (e) {
|
|
949
1100
|
this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
|
|
950
1101
|
}
|
|
@@ -1670,22 +1821,22 @@ async function handleConfigPut(req, res) {
|
|
|
1670
1821
|
return;
|
|
1671
1822
|
}
|
|
1672
1823
|
const routes = {};
|
|
1673
|
-
for (const [
|
|
1674
|
-
if (!
|
|
1824
|
+
for (const [url, val] of Object.entries(body.providers)) {
|
|
1825
|
+
if (!url || typeof url !== "string") {
|
|
1675
1826
|
res.writeHead(400, { "content-type": "application/json" });
|
|
1676
|
-
res.end(JSON.stringify({ error: `invalid provider
|
|
1827
|
+
res.end(JSON.stringify({ error: `invalid provider key: ${JSON.stringify(url)}` }));
|
|
1677
1828
|
return;
|
|
1678
1829
|
}
|
|
1679
1830
|
const route = parseRouteEntry(val);
|
|
1680
1831
|
if (!route) {
|
|
1681
1832
|
res.writeHead(400, { "content-type": "application/json" });
|
|
1682
|
-
res.end(JSON.stringify({ error: `invalid provider "${
|
|
1833
|
+
res.end(JSON.stringify({ error: `invalid provider "${url}": expected { "models": { ... } } or null (the key is the upstream URL)` }));
|
|
1683
1834
|
return;
|
|
1684
1835
|
}
|
|
1685
|
-
routes[
|
|
1836
|
+
routes[normalizeUrlKey(url)] = route;
|
|
1686
1837
|
}
|
|
1687
1838
|
const existing = safeReadJson(configFile()) ?? {};
|
|
1688
|
-
existing.providers =
|
|
1839
|
+
existing.providers = routes;
|
|
1689
1840
|
try {
|
|
1690
1841
|
mkdirSync4(dirname3(configFile()), { recursive: true });
|
|
1691
1842
|
writeFileSync3(configFile(), JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
@@ -1694,7 +1845,7 @@ async function handleConfigPut(req, res) {
|
|
|
1694
1845
|
res.end(JSON.stringify({ error: `failed to write: ${String(e)}` }));
|
|
1695
1846
|
return;
|
|
1696
1847
|
}
|
|
1697
|
-
log("info", `[acp-web] providers updated via web UI (${Object.keys(routes).length}
|
|
1848
|
+
log("info", `[acp-web] providers updated via web UI (${Object.keys(routes).length} upstream URL(s)) \u2014 restart to apply`);
|
|
1698
1849
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1699
1850
|
res.end(JSON.stringify({ ok: true, count: Object.keys(routes).length, note: "restart bili to apply" }));
|
|
1700
1851
|
}
|
|
@@ -1819,6 +1970,8 @@ input:focus, select:focus { outline: none; border-color: var(--accent); }
|
|
|
1819
1970
|
.snippet .label b { color: var(--fg); }
|
|
1820
1971
|
.snippet code { display: block; font-size: 13px; color: var(--ok); white-space: pre-wrap; word-break: break-all; }
|
|
1821
1972
|
.snippet .copy { position: absolute; top: 8px; right: 8px; }
|
|
1973
|
+
.client-setup { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border); }
|
|
1974
|
+
.client-head { font-size: 12px; color: var(--dim); margin-bottom: 8px; }
|
|
1822
1975
|
|
|
1823
1976
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
1824
1977
|
th { text-align: left; padding: 8px 12px; color: var(--dim); font-weight: 500; border-bottom: 1px solid var(--border); }
|
|
@@ -1850,11 +2003,10 @@ select { cursor: pointer; }
|
|
|
1850
2003
|
</header>
|
|
1851
2004
|
<nav>
|
|
1852
2005
|
<button class="active" onclick="showTab('providers')">Providers</button>
|
|
1853
|
-
<button onclick="showTab('setup')">Client Setup</button>
|
|
1854
2006
|
<button onclick="showTab('sessions')">Sessions</button>
|
|
1855
2007
|
</nav>
|
|
1856
2008
|
<main>
|
|
1857
|
-
<!-- Providers tab -->
|
|
2009
|
+
<!-- Providers tab (URL + models + client config, all in one) -->
|
|
1858
2010
|
<div id="tab-providers" class="tab active">
|
|
1859
2011
|
<div id="restart-notice" class="notice" style="display:none"></div>
|
|
1860
2012
|
<div id="providers-list"></div>
|
|
@@ -1862,19 +2014,11 @@ select { cursor: pointer; }
|
|
|
1862
2014
|
<button class="btn" onclick="addProvider()">+ Add provider</button>
|
|
1863
2015
|
</div>
|
|
1864
2016
|
<div class="actions">
|
|
2017
|
+
<button class="btn" onclick="applyProviders()">Apply</button>
|
|
1865
2018
|
<button class="btn primary" onclick="saveProviders()">Save</button>
|
|
1866
2019
|
</div>
|
|
1867
2020
|
</div>
|
|
1868
2021
|
|
|
1869
|
-
<!-- Client Setup tab -->
|
|
1870
|
-
<div id="tab-setup" class="tab">
|
|
1871
|
-
<div class="row" style="margin-bottom:16px">
|
|
1872
|
-
<label>Provider</label>
|
|
1873
|
-
<select id="setup-provider" onchange="renderSetup()"></select>
|
|
1874
|
-
</div>
|
|
1875
|
-
<div id="setup-snippets"></div>
|
|
1876
|
-
</div>
|
|
1877
|
-
|
|
1878
2022
|
<!-- Sessions tab -->
|
|
1879
2023
|
<div id="tab-sessions" class="tab">
|
|
1880
2024
|
<div class="row" style="justify-content:space-between;margin-bottom:12px">
|
|
@@ -1894,6 +2038,7 @@ var savedProviders = null;
|
|
|
1894
2038
|
// \u2500\u2500 helpers \u2500\u2500
|
|
1895
2039
|
function el(id) { return document.getElementById(id); }
|
|
1896
2040
|
function esc(s) { return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
2041
|
+
function autosize(t) { t.style.height="auto"; t.style.height=t.scrollHeight+"px"; }
|
|
1897
2042
|
function fmtTok(n) { if (n >= 1000000) return (n/1000000).toFixed(1)+"M"; if (n >= 1000) return (n/1000).toFixed(1)+"K"; return String(n); }
|
|
1898
2043
|
function toast(msg, isErr) {
|
|
1899
2044
|
var t = el("toast"); t.textContent = msg; t.className = "toast show" + (isErr ? " err" : "");
|
|
@@ -1905,16 +2050,14 @@ function showTab(name) {
|
|
|
1905
2050
|
el("tab-"+name).classList.add("active");
|
|
1906
2051
|
event.target.classList.add("active");
|
|
1907
2052
|
if (name === "sessions") refreshSessions();
|
|
1908
|
-
if (name === "setup") renderSetup();
|
|
1909
2053
|
}
|
|
1910
2054
|
|
|
1911
2055
|
// \u2500\u2500 load \u2500\u2500
|
|
1912
2056
|
async function load() {
|
|
1913
2057
|
try {
|
|
1914
|
-
var r = await fetch("/
|
|
2058
|
+
var r = await fetch("/__bili/config");
|
|
1915
2059
|
var d = await r.json();
|
|
1916
2060
|
providers = entries(d.providers);
|
|
1917
|
-
el("setup-provider").innerHTML = "";
|
|
1918
2061
|
savedProviders = JSON.stringify(providers);
|
|
1919
2062
|
renderProviders();
|
|
1920
2063
|
} catch(e) {
|
|
@@ -1923,18 +2066,17 @@ async function load() {
|
|
|
1923
2066
|
}
|
|
1924
2067
|
function entries(obj) {
|
|
1925
2068
|
if (!obj || typeof obj !== "object") return [];
|
|
1926
|
-
return Object.keys(obj).map(function(
|
|
1927
|
-
var v = obj[
|
|
1928
|
-
var
|
|
1929
|
-
if (typeof v === "
|
|
1930
|
-
|
|
1931
|
-
return { name: name, url: url, models: models };
|
|
2069
|
+
return Object.keys(obj).map(function(url){
|
|
2070
|
+
var v = obj[url];
|
|
2071
|
+
var models = [];
|
|
2072
|
+
if (v && typeof v === "object" && !Array.isArray(v)) models = entries_models(v.models);
|
|
2073
|
+
return { url: url, models: models };
|
|
1932
2074
|
});
|
|
1933
2075
|
}
|
|
1934
2076
|
function entries_models(obj) {
|
|
1935
2077
|
if (!obj || typeof obj !== "object") return [];
|
|
1936
2078
|
return Object.keys(obj).map(function(name){
|
|
1937
|
-
return { name: name, context: obj[name].context||0
|
|
2079
|
+
return { name: name, context: obj[name].context||0 };
|
|
1938
2080
|
});
|
|
1939
2081
|
}
|
|
1940
2082
|
|
|
@@ -1942,17 +2084,19 @@ function entries_models(obj) {
|
|
|
1942
2084
|
function renderProviders() {
|
|
1943
2085
|
var html = "";
|
|
1944
2086
|
if (providers.length === 0) html = '<div class="empty">No providers yet. Click "Add provider" below.</div>';
|
|
1945
|
-
|
|
2087
|
+
providers.forEach(function(p, i) {
|
|
1946
2088
|
html += '<div class="card">';
|
|
1947
|
-
html += '<div class="card-head"><div class="name"><
|
|
2089
|
+
html += '<div class="card-head" style="align-items:flex-start"><div class="name" style="flex:1;min-width:0;width:100%"><textarea class="mono url-edit" onchange="providers['+i+'].url=this.value" oninput="autosize(this)" placeholder="https://upstream/api/path" style="background:transparent;border:1px dashed var(--border);border-radius:4px;padding:6px 8px;width:100%;resize:none;overflow:hidden;font-weight:600;color:var(--accent);font-family:inherit;font-size:13px;line-height:1.4">'+esc(p.url)+'</textarea></div>';
|
|
1948
2090
|
html += '<button class="btn danger small" onclick="removeProvider('+i+')">Remove</button></div>';
|
|
1949
|
-
html += '<div class="
|
|
2091
|
+
html += '<div class="client-setup">';
|
|
2092
|
+
html += '<div class="client-head">Client base URL \u2014 copy this into Pi / OpenCode / Codex / Claude Code config:</div>';
|
|
2093
|
+
html += snippet("", ORIGIN + "/bili/" + p.url);
|
|
2094
|
+
html += '</div>';
|
|
1950
2095
|
p.models.forEach(function(m, j) {
|
|
1951
2096
|
html += '<div class="sub-card">';
|
|
1952
2097
|
html += '<div class="model-head"><span class="mname mono">'+esc(m.name)+'</span>';
|
|
1953
2098
|
html += '<button class="btn danger small" onclick="removeModel('+i+','+j+')">Remove</button></div>';
|
|
1954
|
-
html += '<div class="model-row"><label>context</label><input type="number" value="'+m.context+'" onchange="providers['+i+'].models['+j+'].context=parseInt(this.value)||0"></div>';
|
|
1955
|
-
html += '<div class="model-row"><label>output</label><input type="number" value="'+m.output+'" onchange="providers['+i+'].models['+j+'].output=parseInt(this.value)||0"></div>';
|
|
2099
|
+
html += '<div class="model-row"><label>context</label><input type="number" value="'+m.context+'" onchange="providers['+i+'].models['+j+'].context=parseInt(this.value)||0" placeholder="optional"></div>';
|
|
1956
2100
|
html += '<div class="model-row"><label>name</label><input value="'+esc(m.name)+'" onchange="providers['+i+'].models['+j+'].name=this.value"></div>';
|
|
1957
2101
|
html += '</div>';
|
|
1958
2102
|
});
|
|
@@ -1960,10 +2104,11 @@ function renderProviders() {
|
|
|
1960
2104
|
html += '</div>';
|
|
1961
2105
|
});
|
|
1962
2106
|
el("providers-list").innerHTML = html;
|
|
2107
|
+
document.querySelectorAll(".url-edit").forEach(autosize);
|
|
1963
2108
|
checkDirty();
|
|
1964
2109
|
}
|
|
1965
2110
|
function addProvider() {
|
|
1966
|
-
providers.push({
|
|
2111
|
+
providers.push({ url: "https://api.example.com", models: [] });
|
|
1967
2112
|
renderProviders();
|
|
1968
2113
|
}
|
|
1969
2114
|
function removeProvider(i) {
|
|
@@ -1971,7 +2116,7 @@ function removeProvider(i) {
|
|
|
1971
2116
|
renderProviders();
|
|
1972
2117
|
}
|
|
1973
2118
|
function addModel(i) {
|
|
1974
|
-
providers[i].models.push({ name: "model-name", context:
|
|
2119
|
+
providers[i].models.push({ name: "model-name", context: 0 });
|
|
1975
2120
|
renderProviders();
|
|
1976
2121
|
}
|
|
1977
2122
|
function removeModel(i, j) {
|
|
@@ -1981,54 +2126,56 @@ function removeModel(i, j) {
|
|
|
1981
2126
|
function checkDirty() {
|
|
1982
2127
|
var dirty = savedProviders !== null && JSON.stringify(providers) !== savedProviders;
|
|
1983
2128
|
var n = el("restart-notice");
|
|
1984
|
-
if (dirty) { n.style.display = "block"; n.textContent = "Unsaved changes \u2014 click Save to write to the config file, then
|
|
2129
|
+
if (dirty) { n.style.display = "block"; n.textContent = "Unsaved changes \u2014 click Save to write to the config file, then click Apply to activate without restart."; }
|
|
1985
2130
|
else { n.style.display = "none"; }
|
|
1986
2131
|
}
|
|
1987
2132
|
|
|
2133
|
+
// \u2500\u2500 apply (hot-reload routes into running process) \u2500\u2500
|
|
2134
|
+
async function applyProviders() {
|
|
2135
|
+
// Apply always re-reads the config FILE, so require a Save first if dirty.
|
|
2136
|
+
if (savedProviders !== JSON.stringify(providers)) {
|
|
2137
|
+
toast("Save your changes first", true);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
try {
|
|
2141
|
+
var r = await fetch("/__bili/config/reload", { method: "POST" });
|
|
2142
|
+
var d = await r.json();
|
|
2143
|
+
if (r.ok) { toast("Applied \u2014 " + d.count + " providers active (no restart needed)"); }
|
|
2144
|
+
else { toast("Apply failed: " + (d.error || "unknown"), true); }
|
|
2145
|
+
} catch(e) { toast("Apply failed: " + e, true); }
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1988
2148
|
// \u2500\u2500 save \u2500\u2500
|
|
1989
2149
|
async function saveProviders() {
|
|
1990
2150
|
var obj = {};
|
|
1991
2151
|
providers.forEach(function(p) {
|
|
1992
|
-
if (!p.
|
|
1993
|
-
|
|
2152
|
+
if (!p.url) return;
|
|
2153
|
+
// Strip trailing slashes (keys must be prefix-matchable). Done without a
|
|
2154
|
+
// regex literal because the HTML is inlined into a template string and
|
|
2155
|
+
// backslash escaping there is brittle.
|
|
2156
|
+
var key = p.url;
|
|
2157
|
+
while (key.length > 1 && key.charAt(key.length - 1) === "/") key = key.slice(0, -1);
|
|
2158
|
+
if (!key) return;
|
|
2159
|
+
if (p.models.length === 0) { obj[key] = {}; }
|
|
1994
2160
|
else {
|
|
1995
2161
|
var models = {};
|
|
1996
|
-
p.models.forEach(function(m){ if(m.name) models[m.name] = { context: m.context
|
|
1997
|
-
obj[
|
|
2162
|
+
p.models.forEach(function(m){ if(m.name) models[m.name] = { context: m.context }; });
|
|
2163
|
+
obj[key] = { models: models };
|
|
1998
2164
|
}
|
|
1999
2165
|
});
|
|
2000
2166
|
try {
|
|
2001
|
-
var r = await fetch("/
|
|
2167
|
+
var r = await fetch("/__bili/config", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ providers: obj }) });
|
|
2002
2168
|
var d = await r.json();
|
|
2003
|
-
if (r.ok) { savedProviders = JSON.stringify(providers); checkDirty(); toast("Saved " + d.count + "
|
|
2169
|
+
if (r.ok) { savedProviders = JSON.stringify(providers); checkDirty(); toast("Saved " + d.count + " provider(s) \u2014 click Apply to activate"); }
|
|
2004
2170
|
else { toast("Error: " + (d.error || "unknown"), true); }
|
|
2005
2171
|
} catch(e) { toast("Save failed: " + e, true); }
|
|
2006
2172
|
}
|
|
2007
2173
|
|
|
2008
|
-
// \u2500\u2500
|
|
2009
|
-
function renderSetup() {
|
|
2010
|
-
var sel = el("setup-provider");
|
|
2011
|
-
if (sel.options.length === 0 && providers.length > 0) {
|
|
2012
|
-
providers.forEach(function(p){ sel.options.add(new Option(p.name, p.name)); });
|
|
2013
|
-
}
|
|
2014
|
-
var name = sel.value || (providers[0] && providers[0].name) || "";
|
|
2015
|
-
var p = providers.find(function(x){ return x.name === name; });
|
|
2016
|
-
var box = el("setup-snippets");
|
|
2017
|
-
if (!p) { box.innerHTML = '<div class="empty">Add a provider first (Providers tab).</div>'; return; }
|
|
2018
|
-
var base = ORIGIN + "/" + p.name;
|
|
2019
|
-
// Pi: the original upstream URL minus host \u2192 keep the tail
|
|
2020
|
-
var tail = p.url.replace(/^https?:\\/\\/[^/]+/, "");
|
|
2021
|
-
var full = tail ? base + tail : base;
|
|
2022
|
-
var h = "";
|
|
2023
|
-
h += snippet("Pi (~/.pi/agent/models.json)", '"baseUrl": "' + full + '"');
|
|
2024
|
-
h += snippet("OpenCode (opencode.json)", '"baseURL": "' + full + '"');
|
|
2025
|
-
h += snippet("Codex (config.toml)", 'base_url = "' + full + '"');
|
|
2026
|
-
h += snippet("Full path (any client)", full);
|
|
2027
|
-
box.innerHTML = h;
|
|
2028
|
-
}
|
|
2174
|
+
// \u2500\u2500 snippets (used inline in each provider card) \u2500\u2500
|
|
2029
2175
|
function snippet(label, code) {
|
|
2030
2176
|
var c = code.replace(/"/g, """);
|
|
2031
|
-
|
|
2177
|
+
var labelHtml = label ? '<div class="label"><b>' + label + '</b></div>' : '';
|
|
2178
|
+
return '<div class="snippet">' + labelHtml + '<code>' + esc(code) + '</code><button class="btn small copy" onclick="copyText(this,\\''+c+'\\')">Copy</button></div>';
|
|
2032
2179
|
}
|
|
2033
2180
|
function copyText(btn, text) {
|
|
2034
2181
|
var t = text.replace(/"/g, '"');
|
|
@@ -2038,7 +2185,7 @@ function copyText(btn, text) {
|
|
|
2038
2185
|
// \u2500\u2500 sessions \u2500\u2500
|
|
2039
2186
|
async function refreshSessions() {
|
|
2040
2187
|
try {
|
|
2041
|
-
var r = await fetch("/
|
|
2188
|
+
var r = await fetch("/__bili/stats");
|
|
2042
2189
|
var d = await r.json();
|
|
2043
2190
|
var ss = d.sessions || [];
|
|
2044
2191
|
el("sess-total").textContent = ss.length + " session" + (ss.length !== 1 ? "s" : "");
|
|
@@ -3111,21 +3258,14 @@ var UPSTREAM_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
|
3111
3258
|
// streamed from fetch, otherwise clients try to decompress plain bytes.
|
|
3112
3259
|
"content-encoding"
|
|
3113
3260
|
]);
|
|
3114
|
-
function resolveUpstream(
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
if (RESERVED.has(name.toLowerCase())) continue;
|
|
3123
|
-
const idx = segments.indexOf(name);
|
|
3124
|
-
if (idx < 0) continue;
|
|
3125
|
-
const base = opts.routes[name].url.replace(/\/$/, "");
|
|
3126
|
-
const rest = [...segments.slice(0, idx), ...segments.slice(idx + 1)].join("/");
|
|
3127
|
-
const rewrittenUrl = base + rest;
|
|
3128
|
-
return { upstream: base, rewrittenUrl, provider: name };
|
|
3261
|
+
function resolveUpstream(_opts, reqUrl) {
|
|
3262
|
+
if (reqUrl.startsWith("/bili/http://") || reqUrl.startsWith("/bili/https://")) {
|
|
3263
|
+
const full = reqUrl.slice(6);
|
|
3264
|
+
try {
|
|
3265
|
+
const u2 = new URL(full);
|
|
3266
|
+
return { upstream: `${u2.protocol}//${u2.host}`, rewrittenUrl: full };
|
|
3267
|
+
} catch {
|
|
3268
|
+
}
|
|
3129
3269
|
}
|
|
3130
3270
|
return void 0;
|
|
3131
3271
|
}
|
|
@@ -3139,6 +3279,7 @@ async function startServer(opts) {
|
|
|
3139
3279
|
if (filePath) {
|
|
3140
3280
|
log2("info", `[log] writing to ${filePath}`);
|
|
3141
3281
|
}
|
|
3282
|
+
void loadRegistry();
|
|
3142
3283
|
const server = http.createServer(async (req, res) => {
|
|
3143
3284
|
try {
|
|
3144
3285
|
await handle(req, res, opts, core, config, log2);
|
|
@@ -3156,9 +3297,10 @@ async function startServer(opts) {
|
|
|
3156
3297
|
});
|
|
3157
3298
|
server.listen(opts.port, opts.host, () => {
|
|
3158
3299
|
const displayHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
|
|
3300
|
+
const nOverrides = Object.keys(opts.routes).length;
|
|
3159
3301
|
log2(
|
|
3160
3302
|
"info",
|
|
3161
|
-
`acp-proxy listening on http://${displayHost}:${opts.port}
|
|
3303
|
+
`acp-proxy listening on http://${displayHost}:${opts.port} \u2014 web UI: http://${displayHost}:${opts.port}/__bili/ \u2014 zero-config: prefix any baseURL with http://${displayHost}:${opts.port}/bili/` + (nOverrides ? ` \u2014 context overrides for ${nOverrides} upstream URL(s)` : "")
|
|
3162
3304
|
);
|
|
3163
3305
|
});
|
|
3164
3306
|
server.on("error", (err) => {
|
|
@@ -3204,20 +3346,32 @@ async function startServer(opts) {
|
|
|
3204
3346
|
return server;
|
|
3205
3347
|
}
|
|
3206
3348
|
async function handle(req, res, opts, core, config, log2) {
|
|
3207
|
-
if (req.method === "GET" && req.url === "/
|
|
3208
|
-
if (req.method === "GET" &&
|
|
3349
|
+
if (req.method === "GET" && req.url === "/__bili/stats") return sendStats(res);
|
|
3350
|
+
if (req.method === "GET" && req.url === "/") {
|
|
3351
|
+
const accept = req.headers.accept ?? "";
|
|
3352
|
+
if (accept.includes("text/html")) {
|
|
3353
|
+
res.writeHead(302, { location: "/__bili/" });
|
|
3354
|
+
res.end();
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3209
3357
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3210
3358
|
res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
|
|
3211
3359
|
return;
|
|
3212
3360
|
}
|
|
3213
|
-
if (req.method === "GET" && req.url === "/
|
|
3361
|
+
if (req.method === "GET" && req.url === "/__bili/health") {
|
|
3362
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
3363
|
+
res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (req.method === "GET" && req.url === "/__bili/") {
|
|
3214
3367
|
const origin = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
|
|
3215
3368
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3216
3369
|
res.end(renderUI(origin));
|
|
3217
3370
|
return;
|
|
3218
3371
|
}
|
|
3219
|
-
if (req.method === "GET" && req.url === "/
|
|
3220
|
-
if (req.method === "PUT" && req.url === "/
|
|
3372
|
+
if (req.method === "GET" && req.url === "/__bili/config") return handleConfigGet(res);
|
|
3373
|
+
if (req.method === "PUT" && req.url === "/__bili/config") return handleConfigPut(req, res);
|
|
3374
|
+
if (req.method === "POST" && req.url === "/__bili/config/reload") return handleConfigReload(opts, res, log2);
|
|
3221
3375
|
let bodyBuffer;
|
|
3222
3376
|
try {
|
|
3223
3377
|
bodyBuffer = await readBody(req);
|
|
@@ -3250,7 +3404,18 @@ async function handle(req, res, opts, core, config, log2) {
|
|
|
3250
3404
|
if (parsed && typeof parsed === "object") {
|
|
3251
3405
|
const model = parsed.model;
|
|
3252
3406
|
if (model) {
|
|
3253
|
-
const
|
|
3407
|
+
const embeddedUrl = route?.rewrittenUrl;
|
|
3408
|
+
let limit = resolveContextLimit(opts.routes, embeddedUrl, model);
|
|
3409
|
+
if (!limit && embeddedUrl) {
|
|
3410
|
+
const host = (() => {
|
|
3411
|
+
try {
|
|
3412
|
+
return new URL(embeddedUrl).host;
|
|
3413
|
+
} catch {
|
|
3414
|
+
return void 0;
|
|
3415
|
+
}
|
|
3416
|
+
})();
|
|
3417
|
+
limit = await contextFromRegistry(model, host);
|
|
3418
|
+
}
|
|
3254
3419
|
if (limit && limit !== config.modelContextLimit) {
|
|
3255
3420
|
reqConfig = { ...config, modelContextLimit: limit };
|
|
3256
3421
|
}
|
|
@@ -3505,7 +3670,7 @@ function injectResponsesTool(tools) {
|
|
|
3505
3670
|
}
|
|
3506
3671
|
async function forward(req, res, opts, body, prepared, core, config, log2, route, affinity) {
|
|
3507
3672
|
const upstreamUrl = route ? route.rewrittenUrl : opts.upstream + (req.url ?? "");
|
|
3508
|
-
log2("info", `forward ${req.method}
|
|
3673
|
+
log2("info", `forward ${req.method} \u2192 ${upstreamUrl}`);
|
|
3509
3674
|
if (process.env.ACP_DEBUG && prepared) {
|
|
3510
3675
|
const sid = prepared.session.id;
|
|
3511
3676
|
const hdrKeys = Object.keys(req.headers);
|
|
@@ -3718,6 +3883,15 @@ function deriveTitle(messages) {
|
|
|
3718
3883
|
}
|
|
3719
3884
|
return void 0;
|
|
3720
3885
|
}
|
|
3886
|
+
function handleConfigReload(opts, res, log2) {
|
|
3887
|
+
const fresh = loadRoutes();
|
|
3888
|
+
for (const k2 of Object.keys(opts.routes)) delete opts.routes[k2];
|
|
3889
|
+
Object.assign(opts.routes, fresh);
|
|
3890
|
+
const names = Object.keys(fresh);
|
|
3891
|
+
log2("info", `[acp-web] routes hot-reloaded (${names.length} providers): ${names.join(", ") || "(none)"}`);
|
|
3892
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
3893
|
+
res.end(JSON.stringify({ ok: true, count: names.length, routes: names }));
|
|
3894
|
+
}
|
|
3721
3895
|
function sendStats(res) {
|
|
3722
3896
|
const sessions2 = listSessions().map((s3) => ({
|
|
3723
3897
|
id: s3.id,
|
|
@@ -3781,7 +3955,7 @@ function logMsg(opts, level, msg2) {
|
|
|
3781
3955
|
}
|
|
3782
3956
|
|
|
3783
3957
|
// src/update.ts
|
|
3784
|
-
import { readFile, writeFile, mkdir, access, constants, rm, cp } from "fs/promises";
|
|
3958
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access, constants, rm, cp } from "fs/promises";
|
|
3785
3959
|
|
|
3786
3960
|
// node_modules/tar/dist/esm/index.min.js
|
|
3787
3961
|
import Qr from "events";
|
|
@@ -6753,12 +6927,12 @@ var To = (s3) => {
|
|
|
6753
6927
|
};
|
|
6754
6928
|
|
|
6755
6929
|
// src/update.ts
|
|
6756
|
-
import
|
|
6930
|
+
import path5 from "path";
|
|
6757
6931
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6758
6932
|
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
6759
6933
|
var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
|
|
6760
|
-
var THROTTLE_FILE =
|
|
6761
|
-
var LOCK_FILE =
|
|
6934
|
+
var THROTTLE_FILE = path5.join(cacheDir(), ".update-check");
|
|
6935
|
+
var LOCK_FILE = path5.join(cacheDir(), ".update-lock");
|
|
6762
6936
|
var LOCK_STALE_MS = 2 * 60 * 1e3;
|
|
6763
6937
|
var timer;
|
|
6764
6938
|
var inFlight = false;
|
|
@@ -6779,7 +6953,7 @@ function isNewer(latest, current) {
|
|
|
6779
6953
|
}
|
|
6780
6954
|
async function readLastCheck() {
|
|
6781
6955
|
try {
|
|
6782
|
-
const data = await
|
|
6956
|
+
const data = await readFile2(THROTTLE_FILE, "utf-8");
|
|
6783
6957
|
return parseInt(data.trim(), 10) || 0;
|
|
6784
6958
|
} catch {
|
|
6785
6959
|
return 0;
|
|
@@ -6787,27 +6961,27 @@ async function readLastCheck() {
|
|
|
6787
6961
|
}
|
|
6788
6962
|
async function writeLastCheck(ts2) {
|
|
6789
6963
|
try {
|
|
6790
|
-
await
|
|
6791
|
-
await
|
|
6964
|
+
await mkdir2(path5.dirname(THROTTLE_FILE), { recursive: true });
|
|
6965
|
+
await writeFile2(THROTTLE_FILE, String(ts2), "utf-8");
|
|
6792
6966
|
} catch {
|
|
6793
6967
|
}
|
|
6794
6968
|
}
|
|
6795
6969
|
async function findInstallDir(packageName) {
|
|
6796
|
-
let dir =
|
|
6970
|
+
let dir = path5.dirname(fileURLToPath2(import.meta.url));
|
|
6797
6971
|
for (; ; ) {
|
|
6798
6972
|
try {
|
|
6799
|
-
const pkg = JSON.parse(await
|
|
6973
|
+
const pkg = JSON.parse(await readFile2(path5.join(dir, "package.json"), "utf-8"));
|
|
6800
6974
|
if (pkg.name === packageName) return dir;
|
|
6801
6975
|
} catch {
|
|
6802
6976
|
}
|
|
6803
|
-
const parent =
|
|
6977
|
+
const parent = path5.dirname(dir);
|
|
6804
6978
|
if (parent === dir) return void 0;
|
|
6805
6979
|
dir = parent;
|
|
6806
6980
|
}
|
|
6807
6981
|
}
|
|
6808
6982
|
async function readDiskVersion(installDir) {
|
|
6809
6983
|
try {
|
|
6810
|
-
const pkg = JSON.parse(await
|
|
6984
|
+
const pkg = JSON.parse(await readFile2(path5.join(installDir, "package.json"), "utf-8"));
|
|
6811
6985
|
return pkg.version;
|
|
6812
6986
|
} catch {
|
|
6813
6987
|
return void 0;
|
|
@@ -6818,7 +6992,7 @@ async function tryAcquireLock() {
|
|
|
6818
6992
|
const now = Date.now();
|
|
6819
6993
|
async function readLock() {
|
|
6820
6994
|
try {
|
|
6821
|
-
const raw = await
|
|
6995
|
+
const raw = await readFile2(LOCK_FILE, "utf-8");
|
|
6822
6996
|
const data = JSON.parse(raw);
|
|
6823
6997
|
if (typeof data.pid === "number" && typeof data.ts === "number") {
|
|
6824
6998
|
return data;
|
|
@@ -6845,7 +7019,7 @@ async function tryAcquireLock() {
|
|
|
6845
7019
|
log("info", `[update] stealing stale lock (pid=${existing.pid}, age=${Math.round(age / 1e3)}s, alive=${holderAlive})`);
|
|
6846
7020
|
}
|
|
6847
7021
|
try {
|
|
6848
|
-
await
|
|
7022
|
+
await writeFile2(LOCK_FILE, JSON.stringify({ pid, ts: now }), { flag: "wx" });
|
|
6849
7023
|
} catch {
|
|
6850
7024
|
const winner = await readLock();
|
|
6851
7025
|
if (winner && winner.pid !== pid) {
|
|
@@ -6952,17 +7126,17 @@ async function installViaTarball(version, tarballUrl, installDir) {
|
|
|
6952
7126
|
} catch (e) {
|
|
6953
7127
|
return { ok: false, error: `tarball download failed: ${String(e)}` };
|
|
6954
7128
|
}
|
|
6955
|
-
const tmpFile =
|
|
7129
|
+
const tmpFile = path5.join(cacheDir(), `.update-${version}.tgz`);
|
|
6956
7130
|
try {
|
|
6957
|
-
await
|
|
6958
|
-
await
|
|
7131
|
+
await mkdir2(cacheDir(), { recursive: true });
|
|
7132
|
+
await writeFile2(tmpFile, tgzBuffer);
|
|
6959
7133
|
} catch (e) {
|
|
6960
7134
|
return { ok: false, error: `failed to write temp file ${tmpFile}: ${String(e)}` };
|
|
6961
7135
|
}
|
|
6962
|
-
const stagingDir =
|
|
7136
|
+
const stagingDir = path5.join(cacheDir(), `.update-staging-${version}`);
|
|
6963
7137
|
try {
|
|
6964
7138
|
await rm(stagingDir, { recursive: true, force: true });
|
|
6965
|
-
await
|
|
7139
|
+
await mkdir2(stagingDir, { recursive: true });
|
|
6966
7140
|
await So({
|
|
6967
7141
|
file: tmpFile,
|
|
6968
7142
|
cwd: stagingDir,
|
|
@@ -7004,11 +7178,11 @@ function startAutoUpdate(opts) {
|
|
|
7004
7178
|
// src/cli.ts
|
|
7005
7179
|
import { readFileSync as readFileSync4 } from "fs";
|
|
7006
7180
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7007
|
-
import
|
|
7181
|
+
import path6 from "path";
|
|
7008
7182
|
var VERSION = (() => {
|
|
7009
7183
|
try {
|
|
7010
7184
|
const here = fileURLToPath3(import.meta.url);
|
|
7011
|
-
const pkg =
|
|
7185
|
+
const pkg = path6.join(path6.dirname(here), "..", "package.json");
|
|
7012
7186
|
return JSON.parse(readFileSync4(pkg, "utf8")).version ?? "dev";
|
|
7013
7187
|
} catch {
|
|
7014
7188
|
return "dev";
|
|
@@ -7017,7 +7191,7 @@ var VERSION = (() => {
|
|
|
7017
7191
|
var PACKAGE_NAME = (() => {
|
|
7018
7192
|
try {
|
|
7019
7193
|
const here = fileURLToPath3(import.meta.url);
|
|
7020
|
-
const pkg =
|
|
7194
|
+
const pkg = path6.join(path6.dirname(here), "..", "package.json");
|
|
7021
7195
|
return JSON.parse(readFileSync4(pkg, "utf8")).name ?? "billion-context";
|
|
7022
7196
|
} catch {
|
|
7023
7197
|
return "billion-context";
|