jh-web-gateway 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7H2RJZN3.js → chunk-Y2NMKJOG.js} +255 -124
- package/dist/chunk-Y2NMKJOG.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{tui-DIQMK2CW.js → tui-GRDJWXQL.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-7H2RJZN3.js.map +0 -1
- /package/dist/{tui-DIQMK2CW.js.map → tui-GRDJWXQL.js.map} +0 -0
|
@@ -59,7 +59,7 @@ async function findOrOpenJhPage(browser) {
|
|
|
59
59
|
const contexts = browser.contexts();
|
|
60
60
|
const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
|
|
61
61
|
const page = await context.newPage();
|
|
62
|
-
await page.goto(JH_URL);
|
|
62
|
+
await page.goto(JH_URL, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
63
63
|
return page;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -638,102 +638,103 @@ async function sendChatRequestInner(page, credentials, request, options, isRetry
|
|
|
638
638
|
}
|
|
639
639
|
};
|
|
640
640
|
await page.route(streamPattern, routeHandler);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (ct.includes("text/event-stream")) {
|
|
669
|
-
const reader = res.body?.getReader();
|
|
670
|
-
if (!reader) return { error: true, status: 500, statusText: "No body", body: "No SSE body", contentType: ct };
|
|
671
|
-
const decoder = new TextDecoder();
|
|
672
|
-
let text = "";
|
|
673
|
-
while (true) {
|
|
674
|
-
const { done, value } = await reader.read();
|
|
675
|
-
if (done) break;
|
|
676
|
-
text += decoder.decode(value, { stream: true });
|
|
641
|
+
let result;
|
|
642
|
+
try {
|
|
643
|
+
const postResult = await page.evaluate(
|
|
644
|
+
async ({
|
|
645
|
+
apiBase,
|
|
646
|
+
bearerToken,
|
|
647
|
+
endpointPath,
|
|
648
|
+
requestBody
|
|
649
|
+
}) => {
|
|
650
|
+
try {
|
|
651
|
+
const res = await fetch(`${apiBase}/agents/chat/${endpointPath}`, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
Accept: "text/event-stream",
|
|
656
|
+
Authorization: `Bearer ${bearerToken}`
|
|
657
|
+
},
|
|
658
|
+
body: JSON.stringify(requestBody)
|
|
659
|
+
});
|
|
660
|
+
if (!res.ok) {
|
|
661
|
+
return {
|
|
662
|
+
error: true,
|
|
663
|
+
status: res.status,
|
|
664
|
+
statusText: res.statusText,
|
|
665
|
+
body: (await res.text()).slice(0, 2e3),
|
|
666
|
+
contentType: ""
|
|
667
|
+
};
|
|
677
668
|
}
|
|
678
|
-
|
|
669
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
670
|
+
if (ct.includes("text/event-stream")) {
|
|
671
|
+
const reader = res.body?.getReader();
|
|
672
|
+
if (!reader) return { error: true, status: 500, statusText: "No body", body: "No SSE body", contentType: ct };
|
|
673
|
+
const decoder = new TextDecoder();
|
|
674
|
+
let text = "";
|
|
675
|
+
while (true) {
|
|
676
|
+
const { done, value } = await reader.read();
|
|
677
|
+
if (done) break;
|
|
678
|
+
text += decoder.decode(value, { stream: true });
|
|
679
|
+
}
|
|
680
|
+
return { error: false, status: 200, statusText: "OK", body: text, contentType: ct };
|
|
681
|
+
}
|
|
682
|
+
const bodyText = await res.text();
|
|
683
|
+
console.log(`[gateway] POST response content-type: ${ct}, body: ${bodyText.slice(0, 500)}`);
|
|
684
|
+
return { error: false, status: 200, statusText: "OK", body: bodyText, contentType: ct };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
return { error: true, status: 500, statusText: "fetch error", body: String(err), contentType: "" };
|
|
679
687
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
apiBase: JH_API_BASE,
|
|
691
|
+
bearerToken: credentials.bearerToken,
|
|
692
|
+
endpointPath: endpoint,
|
|
693
|
+
requestBody: body
|
|
685
694
|
}
|
|
686
|
-
|
|
687
|
-
{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
endpointPath: endpoint,
|
|
691
|
-
requestBody: body
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
let result;
|
|
695
|
-
if (postResult.error) {
|
|
696
|
-
result = { error: true, status: postResult.status, statusText: postResult.statusText ?? "", body: postResult.body };
|
|
697
|
-
await page.unroute(streamPattern, routeHandler);
|
|
698
|
-
} else if (postResult.contentType.includes("text/event-stream")) {
|
|
699
|
-
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
700
|
-
await page.unroute(streamPattern, routeHandler);
|
|
701
|
-
} else {
|
|
702
|
-
let streamId;
|
|
703
|
-
try {
|
|
704
|
-
streamId = JSON.parse(postResult.body).streamId;
|
|
705
|
-
} catch {
|
|
706
|
-
}
|
|
707
|
-
if (!streamId) {
|
|
695
|
+
);
|
|
696
|
+
if (postResult.error) {
|
|
697
|
+
result = { error: true, status: postResult.status, statusText: postResult.statusText ?? "", body: postResult.body };
|
|
698
|
+
} else if (postResult.contentType.includes("text/event-stream")) {
|
|
708
699
|
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
709
|
-
await page.unroute(streamPattern, routeHandler);
|
|
710
700
|
} else {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
701
|
+
let streamId;
|
|
702
|
+
try {
|
|
703
|
+
streamId = JSON.parse(postResult.body).streamId;
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
if (!streamId) {
|
|
707
|
+
result = { error: false, status: 200, statusText: "OK", body: postResult.body };
|
|
708
|
+
} else {
|
|
709
|
+
const streamUrl = `${JH_API_BASE}/agents/chat/stream/${streamId}`;
|
|
710
|
+
page.evaluate(
|
|
711
|
+
async ({ url, token }) => {
|
|
712
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
713
|
+
try {
|
|
714
|
+
await fetch(url, {
|
|
715
|
+
method: "GET",
|
|
716
|
+
headers: { Accept: "text/event-stream", Authorization: `Bearer ${token}` }
|
|
717
|
+
});
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{ url: streamUrl, token: credentials.bearerToken }
|
|
722
|
+
).catch(() => {
|
|
723
|
+
});
|
|
724
|
+
const timeout = new Promise(
|
|
725
|
+
(res) => setTimeout(() => {
|
|
726
|
+
if (!sseResolved) {
|
|
727
|
+
sseResolved = true;
|
|
728
|
+
res({ error: true, status: 408, statusText: "timeout", body: "Stream capture timed out after 120s" });
|
|
729
|
+
}
|
|
730
|
+
}, 12e4)
|
|
731
|
+
);
|
|
732
|
+
result = await Promise.race([ssePromise, timeout]);
|
|
733
|
+
}
|
|
736
734
|
}
|
|
735
|
+
} finally {
|
|
736
|
+
await page.unroute(streamPattern, routeHandler).catch(() => {
|
|
737
|
+
});
|
|
737
738
|
}
|
|
738
739
|
if (result.error) {
|
|
739
740
|
const status = result.status;
|
|
@@ -741,7 +742,7 @@ async function sendChatRequestInner(page, credentials, request, options, isRetry
|
|
|
741
742
|
if (status === 401 && !isRetry) {
|
|
742
743
|
const cdpUrl = options?.cdpUrl ?? "http://127.0.0.1:9222";
|
|
743
744
|
try {
|
|
744
|
-
await page.reload({ waitUntil: "
|
|
745
|
+
await page.reload({ waitUntil: "commit" });
|
|
745
746
|
const fresh = await captureCredentials(cdpUrl, 3e4);
|
|
746
747
|
const newCreds = {
|
|
747
748
|
bearerToken: fresh.bearerToken,
|
|
@@ -849,11 +850,12 @@ function modelsRouter(_config) {
|
|
|
849
850
|
|
|
850
851
|
// src/routes/health.ts
|
|
851
852
|
import { Hono as Hono2 } from "hono";
|
|
852
|
-
function healthRouter(config, startTime) {
|
|
853
|
+
function healthRouter(config, startTime, deps) {
|
|
853
854
|
const app = new Hono2();
|
|
854
855
|
app.get("/", (c) => {
|
|
855
856
|
const uptime = (Date.now() - startTime) / 1e3;
|
|
856
|
-
const
|
|
857
|
+
const liveCreds = deps?.getCredentials?.() ?? config.credentials;
|
|
858
|
+
const tokenExpiry = liveCreds?.bearerToken ? getTokenExpiry(liveCreds.bearerToken) || null : null;
|
|
857
859
|
const tokenExpired = tokenExpiry !== null && Date.now() / 1e3 > tokenExpiry;
|
|
858
860
|
return c.json({
|
|
859
861
|
status: "ok",
|
|
@@ -958,6 +960,7 @@ import { randomBytes as randomBytes2 } from "crypto";
|
|
|
958
960
|
// src/core/tool-parser.ts
|
|
959
961
|
var TOOL_CALL_RE = /<tool_call\s+id="([^"]*)"\s+name="([^"]*)">([\s\S]*?)<\/tool_call>/g;
|
|
960
962
|
var THINK_RE = /<think>([\s\S]*?)<\/think>/g;
|
|
963
|
+
var PARTIAL_TOOL_CALL_RE = /<tool_call\b[^>]*>(?:(?!<\/tool_call>)[\s\S])*$/;
|
|
961
964
|
function parseToolsAndThinking(text) {
|
|
962
965
|
const toolCalls = [];
|
|
963
966
|
let thinking = null;
|
|
@@ -999,6 +1002,55 @@ function toOpenAIToolCalls(calls) {
|
|
|
999
1002
|
}
|
|
1000
1003
|
}));
|
|
1001
1004
|
}
|
|
1005
|
+
var StreamingToolBuffer = class {
|
|
1006
|
+
buffer = "";
|
|
1007
|
+
/**
|
|
1008
|
+
* Push a text chunk. Returns an object with:
|
|
1009
|
+
* - `text`: safe-to-emit text (outside any partial tag)
|
|
1010
|
+
* - `completedCalls`: fully parsed tool calls from this chunk
|
|
1011
|
+
*/
|
|
1012
|
+
push(chunk) {
|
|
1013
|
+
this.buffer += chunk;
|
|
1014
|
+
const completedCalls = [];
|
|
1015
|
+
let safeText = "";
|
|
1016
|
+
let match;
|
|
1017
|
+
const re = new RegExp(TOOL_CALL_RE.source, "g");
|
|
1018
|
+
let lastIndex = 0;
|
|
1019
|
+
while ((match = re.exec(this.buffer)) !== null) {
|
|
1020
|
+
safeText += this.buffer.slice(lastIndex, match.index);
|
|
1021
|
+
const id = match[1];
|
|
1022
|
+
const name = match[2];
|
|
1023
|
+
const rawArgs = match[3].trim();
|
|
1024
|
+
let args;
|
|
1025
|
+
try {
|
|
1026
|
+
JSON.parse(rawArgs);
|
|
1027
|
+
args = rawArgs;
|
|
1028
|
+
} catch {
|
|
1029
|
+
args = JSON.stringify(rawArgs);
|
|
1030
|
+
}
|
|
1031
|
+
completedCalls.push({ id, name, arguments: args });
|
|
1032
|
+
lastIndex = match.index + match[0].length;
|
|
1033
|
+
}
|
|
1034
|
+
const remainder = this.buffer.slice(lastIndex);
|
|
1035
|
+
if (PARTIAL_TOOL_CALL_RE.test(remainder)) {
|
|
1036
|
+
const partialStart = remainder.search(/<tool_call\b/);
|
|
1037
|
+
if (partialStart > 0) {
|
|
1038
|
+
safeText += remainder.slice(0, partialStart);
|
|
1039
|
+
}
|
|
1040
|
+
this.buffer = partialStart >= 0 ? remainder.slice(partialStart) : remainder;
|
|
1041
|
+
} else {
|
|
1042
|
+
safeText += remainder;
|
|
1043
|
+
this.buffer = "";
|
|
1044
|
+
}
|
|
1045
|
+
return { text: safeText, completedCalls };
|
|
1046
|
+
}
|
|
1047
|
+
/** Flush any remaining buffer content as raw text (end of stream). */
|
|
1048
|
+
flush() {
|
|
1049
|
+
const remaining = this.buffer;
|
|
1050
|
+
this.buffer = "";
|
|
1051
|
+
return remaining;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1002
1054
|
|
|
1003
1055
|
// src/core/stream-translator.ts
|
|
1004
1056
|
function parseSseEvents(rawSse) {
|
|
@@ -1107,6 +1159,9 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1107
1159
|
const events = parseSseEvents(rawSse);
|
|
1108
1160
|
const chunks = [];
|
|
1109
1161
|
let lastMessageText = "";
|
|
1162
|
+
const toolBuf = new StreamingToolBuffer();
|
|
1163
|
+
let toolCallIndex = 0;
|
|
1164
|
+
let hasToolCalls = false;
|
|
1110
1165
|
chunks.push({
|
|
1111
1166
|
id,
|
|
1112
1167
|
object: "chat.completion.chunk",
|
|
@@ -1115,6 +1170,37 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1115
1170
|
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
1116
1171
|
});
|
|
1117
1172
|
let gotDeltas = false;
|
|
1173
|
+
function processDelta(rawDelta) {
|
|
1174
|
+
const { text, completedCalls } = toolBuf.push(rawDelta);
|
|
1175
|
+
if (text) {
|
|
1176
|
+
chunks.push({
|
|
1177
|
+
id,
|
|
1178
|
+
object: "chat.completion.chunk",
|
|
1179
|
+
created,
|
|
1180
|
+
model,
|
|
1181
|
+
choices: [{ index: 0, delta: { content: text }, finish_reason: null }]
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
for (const call of completedCalls) {
|
|
1185
|
+
hasToolCalls = true;
|
|
1186
|
+
const toolDelta = {
|
|
1187
|
+
index: toolCallIndex++,
|
|
1188
|
+
id: call.id,
|
|
1189
|
+
type: "function",
|
|
1190
|
+
function: {
|
|
1191
|
+
name: call.name,
|
|
1192
|
+
arguments: call.arguments
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
chunks.push({
|
|
1196
|
+
id,
|
|
1197
|
+
object: "chat.completion.chunk",
|
|
1198
|
+
created,
|
|
1199
|
+
model,
|
|
1200
|
+
choices: [{ index: 0, delta: { tool_calls: [toolDelta] }, finish_reason: null }]
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1118
1204
|
for (const ev of events) {
|
|
1119
1205
|
const { type, parsed } = resolveEventType(ev);
|
|
1120
1206
|
if (isUserEcho(parsed)) continue;
|
|
@@ -1123,13 +1209,7 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1123
1209
|
const delta = extractDeltaText(parsed);
|
|
1124
1210
|
if (delta !== null) {
|
|
1125
1211
|
gotDeltas = true;
|
|
1126
|
-
|
|
1127
|
-
id,
|
|
1128
|
-
object: "chat.completion.chunk",
|
|
1129
|
-
created,
|
|
1130
|
-
model,
|
|
1131
|
-
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
|
1132
|
-
});
|
|
1212
|
+
processDelta(delta);
|
|
1133
1213
|
}
|
|
1134
1214
|
}
|
|
1135
1215
|
if (type === "message" || !type) {
|
|
@@ -1138,23 +1218,27 @@ function translateToStream(rawSse, model, completionId) {
|
|
|
1138
1218
|
const delta = msgText.slice(lastMessageText.length);
|
|
1139
1219
|
lastMessageText = msgText;
|
|
1140
1220
|
if (!gotDeltas && delta) {
|
|
1141
|
-
|
|
1142
|
-
id,
|
|
1143
|
-
object: "chat.completion.chunk",
|
|
1144
|
-
created,
|
|
1145
|
-
model,
|
|
1146
|
-
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
|
1147
|
-
});
|
|
1221
|
+
processDelta(delta);
|
|
1148
1222
|
}
|
|
1149
1223
|
}
|
|
1150
1224
|
}
|
|
1151
1225
|
}
|
|
1226
|
+
const flushed = toolBuf.flush();
|
|
1227
|
+
if (flushed) {
|
|
1228
|
+
chunks.push({
|
|
1229
|
+
id,
|
|
1230
|
+
object: "chat.completion.chunk",
|
|
1231
|
+
created,
|
|
1232
|
+
model,
|
|
1233
|
+
choices: [{ index: 0, delta: { content: flushed }, finish_reason: null }]
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1152
1236
|
chunks.push({
|
|
1153
1237
|
id,
|
|
1154
1238
|
object: "chat.completion.chunk",
|
|
1155
1239
|
created,
|
|
1156
1240
|
model,
|
|
1157
|
-
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
1241
|
+
choices: [{ index: 0, delta: {}, finish_reason: hasToolCalls ? "tool_calls" : "stop" }]
|
|
1158
1242
|
});
|
|
1159
1243
|
return chunks;
|
|
1160
1244
|
}
|
|
@@ -1177,7 +1261,7 @@ function translateToCompletion(rawSse, model, completionId) {
|
|
|
1177
1261
|
object: "chat.completion",
|
|
1178
1262
|
created,
|
|
1179
1263
|
model,
|
|
1180
|
-
choices: [{ index: 0, message, finish_reason: "stop" }],
|
|
1264
|
+
choices: [{ index: 0, message, finish_reason: parsed.toolCalls.length > 0 ? "tool_calls" : "stop" }],
|
|
1181
1265
|
usage: {
|
|
1182
1266
|
prompt_tokens: promptTokens,
|
|
1183
1267
|
completion_tokens: completionTokens,
|
|
@@ -1288,11 +1372,14 @@ function chatCompletionsRouter(_config, deps) {
|
|
|
1288
1372
|
const { page, queue, release } = acquired;
|
|
1289
1373
|
const stats = pool.stats;
|
|
1290
1374
|
console.log(`[chat] Acquired page (pool: ${stats.busy}/${stats.total} busy)`);
|
|
1375
|
+
const fullPrompt = built.systemPrompt ? `${built.systemPrompt}
|
|
1376
|
+
|
|
1377
|
+
${built.prompt}` : built.prompt;
|
|
1291
1378
|
try {
|
|
1292
1379
|
const response = await queue.enqueue(
|
|
1293
1380
|
() => sendChatRequest(page, credentials, {
|
|
1294
1381
|
model,
|
|
1295
|
-
prompt:
|
|
1382
|
+
prompt: fullPrompt
|
|
1296
1383
|
})
|
|
1297
1384
|
);
|
|
1298
1385
|
if (shouldStream) {
|
|
@@ -1349,7 +1436,7 @@ function chatCompletionsRouter(_config, deps) {
|
|
|
1349
1436
|
const retryResponse = await retry.queue.enqueue(
|
|
1350
1437
|
() => sendChatRequest(retry.page, freshCreds, {
|
|
1351
1438
|
model,
|
|
1352
|
-
prompt:
|
|
1439
|
+
prompt: fullPrompt
|
|
1353
1440
|
})
|
|
1354
1441
|
);
|
|
1355
1442
|
if (shouldStream) {
|
|
@@ -1559,7 +1646,7 @@ function createServer(config, deps) {
|
|
|
1559
1646
|
});
|
|
1560
1647
|
});
|
|
1561
1648
|
app.route("/v1/models", modelsRouter(config));
|
|
1562
|
-
app.route("/health", healthRouter(config, startTime));
|
|
1649
|
+
app.route("/health", healthRouter(config, startTime, deps ? { getCredentials: deps.getCredentials } : void 0));
|
|
1563
1650
|
if (deps) {
|
|
1564
1651
|
app.route(
|
|
1565
1652
|
"/v1/chat/completions",
|
|
@@ -1638,14 +1725,6 @@ async function startServer(config, deps) {
|
|
|
1638
1725
|
}
|
|
1639
1726
|
console.log("Shutdown complete.");
|
|
1640
1727
|
};
|
|
1641
|
-
process.on("SIGINT", async () => {
|
|
1642
|
-
await shutdown();
|
|
1643
|
-
process.exit(0);
|
|
1644
|
-
});
|
|
1645
|
-
process.on("SIGTERM", async () => {
|
|
1646
|
-
await shutdown();
|
|
1647
|
-
process.exit(0);
|
|
1648
|
-
});
|
|
1649
1728
|
return { close: shutdown };
|
|
1650
1729
|
}
|
|
1651
1730
|
|
|
@@ -1668,7 +1747,7 @@ var RequestQueue = class {
|
|
|
1668
1747
|
return await task();
|
|
1669
1748
|
} finally {
|
|
1670
1749
|
if (this.queue.length > 0) {
|
|
1671
|
-
await new Promise((r) => setTimeout(r,
|
|
1750
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1672
1751
|
}
|
|
1673
1752
|
this.release();
|
|
1674
1753
|
}
|
|
@@ -1718,6 +1797,7 @@ var PagePool = class {
|
|
|
1718
1797
|
initPromise = null;
|
|
1719
1798
|
pagesCreating = 0;
|
|
1720
1799
|
warmedUp = false;
|
|
1800
|
+
disconnected = false;
|
|
1721
1801
|
constructor(options = {}) {
|
|
1722
1802
|
this.targetUrl = options.targetUrl ?? "https://chat.ai.jh.edu";
|
|
1723
1803
|
this.maxPages = options.maxPages ?? 1;
|
|
@@ -1731,6 +1811,11 @@ var PagePool = class {
|
|
|
1731
1811
|
}
|
|
1732
1812
|
async _doInit(browser, seedPage) {
|
|
1733
1813
|
this.browser = browser;
|
|
1814
|
+
this.disconnected = false;
|
|
1815
|
+
browser.on("disconnected", () => {
|
|
1816
|
+
console.warn("[PagePool] Browser disconnected \u2014 all pages are now invalid");
|
|
1817
|
+
this.disconnected = true;
|
|
1818
|
+
});
|
|
1734
1819
|
this.pages.push({
|
|
1735
1820
|
page: seedPage,
|
|
1736
1821
|
queue: new RequestQueue(this.maxWaitMs),
|
|
@@ -1757,6 +1842,13 @@ var PagePool = class {
|
|
|
1757
1842
|
* Call `markWarmedUp()` after the first successful request to enable scaling.
|
|
1758
1843
|
*/
|
|
1759
1844
|
async acquire() {
|
|
1845
|
+
if (this.disconnected) {
|
|
1846
|
+
throw Object.assign(
|
|
1847
|
+
new Error("Browser has disconnected. Restart the gateway to reconnect."),
|
|
1848
|
+
{ statusCode: 503 }
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
this.evictDeadPages();
|
|
1760
1852
|
let pooled = this.pages.find((p2) => !p2.inUse);
|
|
1761
1853
|
if (!pooled && this.warmedUp && this.pages.length + this.pagesCreating < this.maxPages && this.browser) {
|
|
1762
1854
|
this.pagesCreating++;
|
|
@@ -1767,6 +1859,12 @@ var PagePool = class {
|
|
|
1767
1859
|
}
|
|
1768
1860
|
}
|
|
1769
1861
|
if (!pooled) {
|
|
1862
|
+
if (this.pages.length === 0) {
|
|
1863
|
+
throw Object.assign(
|
|
1864
|
+
new Error("No healthy browser pages available. Restart the gateway."),
|
|
1865
|
+
{ statusCode: 503 }
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1770
1868
|
pooled = this.pages.reduce(
|
|
1771
1869
|
(a, b) => a.queue.pending <= b.queue.pending ? a : b
|
|
1772
1870
|
);
|
|
@@ -1781,6 +1879,9 @@ var PagePool = class {
|
|
|
1781
1879
|
if (!this.warmedUp) {
|
|
1782
1880
|
this.warmedUp = true;
|
|
1783
1881
|
console.log(`[PagePool] Warm-up complete \u2014 page scaling enabled (max ${this.maxPages})`);
|
|
1882
|
+
if (this.maxPages > 1 && this.pages.length < this.maxPages && this.browser) {
|
|
1883
|
+
this.preWarmPage();
|
|
1884
|
+
}
|
|
1784
1885
|
}
|
|
1785
1886
|
}
|
|
1786
1887
|
};
|
|
@@ -1803,7 +1904,7 @@ var PagePool = class {
|
|
|
1803
1904
|
}
|
|
1804
1905
|
const page = await context.newPage();
|
|
1805
1906
|
try {
|
|
1806
|
-
await page.goto(this.targetUrl, { waitUntil: "
|
|
1907
|
+
await page.goto(this.targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
1807
1908
|
const finalUrl = page.url();
|
|
1808
1909
|
if (!finalUrl.includes("chat.ai.jh.edu")) {
|
|
1809
1910
|
throw new Error(
|
|
@@ -1824,6 +1925,36 @@ var PagePool = class {
|
|
|
1824
1925
|
throw err;
|
|
1825
1926
|
}
|
|
1826
1927
|
}
|
|
1928
|
+
/** Evict pages that have crashed or navigated away from JH */
|
|
1929
|
+
evictDeadPages() {
|
|
1930
|
+
const before = this.pages.length;
|
|
1931
|
+
this.pages = this.pages.filter((p) => {
|
|
1932
|
+
try {
|
|
1933
|
+
if (p.page.isClosed()) return false;
|
|
1934
|
+
const url = p.page.url();
|
|
1935
|
+
if (!url.includes("chat.ai.jh.edu")) {
|
|
1936
|
+
console.warn(`[PagePool] Evicting page \u2014 navigated away: ${url}`);
|
|
1937
|
+
p.page.close().catch(() => {
|
|
1938
|
+
});
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
return true;
|
|
1942
|
+
} catch {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
const evicted = before - this.pages.length;
|
|
1947
|
+
if (evicted > 0) {
|
|
1948
|
+
console.warn(`[PagePool] Evicted ${evicted} dead/stale page(s)`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/** Pre-warm a new page in the background (fire-and-forget) */
|
|
1952
|
+
preWarmPage() {
|
|
1953
|
+
this.pagesCreating++;
|
|
1954
|
+
this.createPage().then(() => console.log("[PagePool] Pre-warmed a new page")).catch((err) => console.warn(`[PagePool] Pre-warm failed: ${err.message}`)).finally(() => {
|
|
1955
|
+
this.pagesCreating--;
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1827
1958
|
/** Close all pages except the seed page */
|
|
1828
1959
|
async drain() {
|
|
1829
1960
|
const toClose = this.pages.slice(1);
|
|
@@ -2192,4 +2323,4 @@ export {
|
|
|
2192
2323
|
TokenRefresher,
|
|
2193
2324
|
ChromeManager
|
|
2194
2325
|
};
|
|
2195
|
-
//# sourceMappingURL=chunk-
|
|
2326
|
+
//# sourceMappingURL=chunk-Y2NMKJOG.js.map
|