mirror of
https://github.com/BinaryBeastMaster/chat-relay.git
synced 2026-04-28 03:39:27 +00:00
Resolve merge conflicts: preserve local improvements while integrating PR changes
This commit is contained in:
parent
8536e06d56
commit
13d6e82732
12 changed files with 2438 additions and 2503 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,6 +15,7 @@ build/
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
!api-relay-server/relay.settings
|
||||||
|
|
||||||
# IDE and editor files
|
# IDE and editor files
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
13
api-relay-server/package-lock.json
generated
13
api-relay-server/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"api-relay-server": "file:",
|
"api-relay-server": "file:",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
|
|
@ -430,6 +431,18 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||||
|
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2",
|
||||||
|
"dotenv": "^16.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/body-parser": "^1.19.5",
|
"@types/body-parser": "^1.19.5",
|
||||||
|
|
|
||||||
|
|
@ -1,127 +1,248 @@
|
||||||
<!--
|
<!--
|
||||||
Chat Relay: Relay for AI Chat Interfaces
|
Chat Relay: Relay for AI Chat Interfaces
|
||||||
Copyright (C) 2025 Jamison Moore
|
Copyright (C) 2025 Jamison Moore
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU Affero General Public License as
|
it under the terms of the GNU Affero General Public License as
|
||||||
published by the Free Software Foundation, either version 3 of the
|
published by the Free Software Foundation, either version 3 of the
|
||||||
License, or (at your option) any later version.
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU Affero General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see https://www.gnu.org/licenses/.
|
along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
-->
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Chat Relay Admin</title>
|
<title>Chat Relay Admin</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #f4f4f4;
|
background-color: #f0f2f5; /* Lighter gray */
|
||||||
color: #333;
|
color: #1c1e21; /* Darker text for better contrast */
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: #333;
|
background-color: #1877f2; /* Facebook blue */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 1em;
|
padding: 1em 1.5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
background-color: #444;
|
background-color: #fff; /* White background for nav */
|
||||||
padding: 0.5em;
|
padding: 0.75em 1.5em;
|
||||||
|
border-bottom: 1px solid #dddfe2; /* Light border */
|
||||||
|
box-shadow: 0 2px 2px -2px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul {
|
nav ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li {
|
nav ul li {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-right: 20px;
|
margin-right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li a {
|
nav ul li a {
|
||||||
color: #fff;
|
color: #1877f2; /* Blue links */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: 600; /* Slightly bolder */
|
||||||
|
padding: 0.5em 0;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
nav ul li a:hover {
|
||||||
|
color: #1159bd; /* Darker blue on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul li a.active {
|
nav ul li a.active {
|
||||||
text-decoration: underline;
|
color: #1c1e21; /* Darker color for active tab */
|
||||||
|
border-bottom: 3px solid #1877f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 1em;
|
padding: 1.5em;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.tab-content { display: none; }
|
||||||
.tab-content {
|
.tab-content.active { display: block; }
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
border-bottom: 2px solid #333;
|
border-bottom: 2px solid #dddfe2;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
|
color: #1c1e21;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 1em;
|
margin-top: 1.5em;
|
||||||
|
table-layout: fixed; /* Important for controlling column widths */
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden; /* Ensures border-radius is respected by children */
|
||||||
}
|
}
|
||||||
|
th, td {
|
||||||
th,
|
border: 1px solid #dddfe2;
|
||||||
td {
|
padding: 12px 10px; /* Increased padding */
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
vertical-align: top; /* Align content to the top */
|
||||||
|
word-wrap: break-word; /* Prevent long words from breaking layout */
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #555;
|
background-color: #f5f6f7; /* Lighter header background */
|
||||||
color: white;
|
color: #4b4f56; /* Darker gray for header text */
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative; /* For positioning collapse buttons */
|
||||||
|
}
|
||||||
|
/* Column width styling */
|
||||||
|
th.col-timestamp, td.col-timestamp { width: 12%; } /* Start/End Timestamp */
|
||||||
|
th.col-request-id, td.col-request-id { width: 8%; } /* Request ID */
|
||||||
|
th.col-status, td.col-status { width: 8%; } /* Status */
|
||||||
|
/* The remaining 4 data columns will share the rest of the space.
|
||||||
|
(100 - 12 - 12 - 8 - 8) = 60%. So each gets 15% */
|
||||||
|
th.col-data, td.col-data { width: 15%; }
|
||||||
|
|
||||||
|
td pre { /* Style for the <pre> tags inside cells */
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 200px; /* Keep existing max-height */
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f9f9f9; /* Slight background for pre blocks */
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-window {
|
.log-window {
|
||||||
background-color: #222;
|
background-color: #1e1e1e; /* Darker background for log */
|
||||||
color: #0f0;
|
color: #d4d4d4; /* Lighter text for log */
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Consolas', 'Courier New', Courier, monospace;
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
height: 200px;
|
height: 250px; /* Slightly taller */
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
border: 1px solid #444;
|
border: 1px solid #333;
|
||||||
margin-top: 1em;
|
margin-top: 1.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.log-entry {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible-header {
|
.collapsible-header {
|
||||||
background-color: #555;
|
background-color: #6c757d; /* Bootstrap secondary color */
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5em;
|
padding: 0.75em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.collapsible-header:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
#refresh-messages-btn {
|
||||||
|
background-color: #007bff; /* Primary blue */
|
||||||
|
color: white;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#refresh-messages-btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
#save-settings-btn {
|
||||||
|
background-color: #28a745; /* Green for save */
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#save-settings-btn:hover {
|
||||||
|
background-color: #1e7e34;
|
||||||
|
}
|
||||||
|
#restart-server-btn {
|
||||||
|
background-color: #dc3545; /* Red for restart/danger */
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#restart-server-btn:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
input[type="text"], input[type="number"], select {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.settings-item, .status-item {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #dddfe2;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
#update-status-msg {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.collapse-btn {
|
||||||
|
font-size: 0.7em;
|
||||||
|
padding: 2px 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block; /* To sit next to header text */
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.collapse-btn:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.collapsed-cell {
|
||||||
|
/* Could add specific styling for collapsed cells if needed, e.g., a placeholder */
|
||||||
|
/* For now, they will just be hidden by JavaScript */
|
||||||
|
}
|
||||||
|
.header-collapsed {
|
||||||
|
width: 1px !important; /* Minimal width to keep column in layout but visually gone */
|
||||||
|
min-width: 1px !important; /* Ensure it doesn't expand due to content */
|
||||||
|
padding-left: 0 !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
overflow: hidden !important; /* Hide content that might overflow 1px */
|
||||||
|
color: transparent !important; /* Hide text by making it transparent */
|
||||||
|
font-size: 0 !important; /* Another way to hide text, affects children */
|
||||||
|
}
|
||||||
|
.header-collapsed .collapse-btn { /* Ensure button is also hidden if not already by font-size:0 */
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>Chat Relay Admin Dashboard</h1>
|
<h1>Chat Relay Admin Dashboard</h1>
|
||||||
|
|
@ -130,24 +251,30 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#" class="tab-link active" data-tab="messages">Messages</a></li>
|
<li><a href="#" class="tab-link active" data-tab="messages">Messages</a></li>
|
||||||
<li><a href="#" class="tab-link" data-tab="settings">Settings</a></li>
|
<li><a href="#" class="tab-link" data-tab="settings">Settings</a></li>
|
||||||
<li><a href="#" class="tab-link" data-tab="status">Status</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="messages" class="tab-content active">
|
<div id="messages" class="tab-content active">
|
||||||
<h2>Message History <button id="refresh-messages-btn" style="font-size: 0.8em; margin-left: 10px;">Refresh
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;">
|
||||||
Messages</button></h2>
|
<h2>Message History</h2>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 15px;">Server Uptime: <span id="status-uptime">N/A</span></span>
|
||||||
|
<span style="margin-right: 15px;">Connected Extensions: <span id="status-connected-extensions">0</span></span>
|
||||||
|
<button id="refresh-messages-btn" style="font-size: 0.8em; padding: 0.5em 1em; margin-right: 10px;">Refresh Messages</button>
|
||||||
|
<button id="restart-server-btn" style="font-size: 0.8em; padding: 0.5em 1em; background-color: #d9534f; color: white; border: none; cursor: pointer;">Restart Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Start Timestamp</th>
|
<th class="col-timestamp">Start Timestamp</th>
|
||||||
<th>End Timestamp</th>
|
<th class="col-timestamp">End Timestamp</th>
|
||||||
<th>Request ID</th>
|
<th class="col-request-id">Request ID</th>
|
||||||
<th>From Client (Cline)</th>
|
<th class="col-data">From Client <span class="collapse-btn" data-column="fromClient" title="Toggle Column">-</span></th>
|
||||||
<th>To Extension</th>
|
<th class="col-data">To Extension <span class="collapse-btn" data-column="toExtension" title="Toggle Column">-</span></th>
|
||||||
<th>From Extension</th>
|
<th class="col-data">From Extension <span class="collapse-btn" data-column="fromExtension" title="Toggle Column">-</span></th>
|
||||||
<th>To Client (Cline)</th>
|
<th class="col-data">To Client <span class="collapse-btn" data-column="toClient" title="Toggle Column">-</span></th>
|
||||||
<th>Status</th>
|
<th class="col-status">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="message-history-body">
|
<tbody id="message-history-body">
|
||||||
|
|
@ -170,8 +297,7 @@
|
||||||
<div style="margin-top: 0.5em;">
|
<div style="margin-top: 0.5em;">
|
||||||
<label>New Request Behavior (if extension busy):</label>
|
<label>New Request Behavior (if extension busy):</label>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="newRequestBehaviorQueue" name="newRequestBehavior" value="queue"
|
<input type="radio" id="newRequestBehaviorQueue" name="newRequestBehavior" value="queue" checked>
|
||||||
checked>
|
|
||||||
<label for="newRequestBehaviorQueue">Queue</label>
|
<label for="newRequestBehaviorQueue">Queue</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -179,25 +305,12 @@
|
||||||
<label for="newRequestBehaviorDrop">Drop</label>
|
<label for="newRequestBehaviorDrop">Drop</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 0.5em;">
|
|
||||||
<label for="auto-kill-port-input">Auto-kill conflicting port 3003 process on startup: </label>
|
|
||||||
<input type="checkbox" id="auto-kill-port-input">
|
|
||||||
</div>
|
|
||||||
<button id="save-settings-btn" style="margin-top: 1em; margin-bottom: 0.5em;">Save Settings</button>
|
<button id="save-settings-btn" style="margin-top: 1em; margin-bottom: 0.5em;">Save Settings</button>
|
||||||
<span id="update-status-msg" style="margin-left: 10px; font-style: italic;"></span>
|
<span id="update-status-msg" style="margin-left: 10px; font-style: italic;"></span>
|
||||||
<p style="margin-top: 1em;">Ping Interval (ms): <span id="setting-ping-interval"></span></p>
|
<p style="margin-top: 1em;">Ping Interval (ms): <span id="setting-ping-interval"></span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="tab-content">
|
<!-- Status tab content removed as its elements are moved to the Messages tab -->
|
||||||
<h2>Server Status</h2>
|
|
||||||
<div id="status-content">
|
|
||||||
<p>Server Uptime: <span id="status-uptime">N/A</span></p>
|
|
||||||
<p>Connected Extensions: <span id="status-connected-extensions">0</span></p>
|
|
||||||
<button id="restart-server-btn"
|
|
||||||
style="margin-top: 1em; padding: 0.5em 1em; background-color: #d9534f; color: white; border: none; cursor: pointer;">Restart
|
|
||||||
Server</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapsible-header" onclick="toggleLogWindow()">Server Logs (click to toggle)</div>
|
<div class="collapsible-header" onclick="toggleLogWindow()">Server Logs (click to toggle)</div>
|
||||||
<div class="log-window" id="log-window-content" style="display: none;">
|
<div class="log-window" id="log-window-content" style="display: none;">
|
||||||
|
|
@ -215,8 +328,13 @@
|
||||||
link.classList.add('active');
|
link.classList.add('active');
|
||||||
const activeTabContent = document.getElementById(link.dataset.tab);
|
const activeTabContent = document.getElementById(link.dataset.tab);
|
||||||
activeTabContent.classList.add('active');
|
activeTabContent.classList.add('active');
|
||||||
// If settings or status tab is activated, refresh their content
|
|
||||||
if (link.dataset.tab === 'settings' || link.dataset.tab === 'status') {
|
// If settings tab is activated, refresh its content
|
||||||
|
if (link.dataset.tab === 'settings') {
|
||||||
|
fetchAndDisplayServerInfo(); // This function now also updates status on the messages tab
|
||||||
|
}
|
||||||
|
// Always ensure server info (which includes status) is fresh when messages tab is active
|
||||||
|
if (link.dataset.tab === 'messages') {
|
||||||
fetchAndDisplayServerInfo();
|
fetchAndDisplayServerInfo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -231,21 +349,23 @@
|
||||||
}
|
}
|
||||||
const messageHistoryBody = document.getElementById('message-history-body');
|
const messageHistoryBody = document.getElementById('message-history-body');
|
||||||
const refreshButton = document.getElementById('refresh-messages-btn');
|
const refreshButton = document.getElementById('refresh-messages-btn');
|
||||||
|
|
||||||
function createPreCell(data) {
|
function createPreCell(data) {
|
||||||
const cell = document.createElement('td');
|
const cell = document.createElement('td');
|
||||||
if (data === undefined || data === null) {
|
if (data === undefined || data === null) {
|
||||||
cell.textContent = 'N/A';
|
cell.textContent = 'N/A';
|
||||||
} else {
|
} else {
|
||||||
const pre = document.createElement('pre');
|
const pre = document.createElement('pre');
|
||||||
pre.style.margin = '0';
|
// pre.style.margin = '0'; // Handled by td pre CSS
|
||||||
pre.style.whiteSpace = 'pre-wrap';
|
// pre.style.whiteSpace = 'pre-wrap'; // Handled by td pre CSS
|
||||||
pre.style.maxHeight = '200px'; // Added max height
|
// pre.style.maxHeight = '200px'; // Handled by td pre CSS
|
||||||
pre.style.overflowY = 'auto'; // Added scrollability
|
// pre.style.overflowY = 'auto'; // Handled by td pre CSS
|
||||||
pre.textContent = JSON.stringify(data, null, 2);
|
pre.textContent = JSON.stringify(data, null, 2);
|
||||||
cell.appendChild(pre);
|
cell.appendChild(pre);
|
||||||
}
|
}
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAndDisplayMessageHistory() {
|
async function fetchAndDisplayMessageHistory() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/v1/admin/message-history');
|
const response = await fetch('/v1/admin/message-history');
|
||||||
|
|
@ -253,7 +373,9 @@
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const messages = await response.json();
|
const messages = await response.json();
|
||||||
|
|
||||||
messageHistoryBody.innerHTML = ''; // Clear existing rows
|
messageHistoryBody.innerHTML = ''; // Clear existing rows
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
const row = messageHistoryBody.insertRow();
|
const row = messageHistoryBody.insertRow();
|
||||||
const cell = row.insertCell();
|
const cell = row.insertCell();
|
||||||
|
|
@ -262,6 +384,7 @@
|
||||||
cell.style.textAlign = 'center';
|
cell.style.textAlign = 'center';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group messages by requestId
|
// Group messages by requestId
|
||||||
const groupedMessages = messages.reduce((acc, logEntry) => {
|
const groupedMessages = messages.reduce((acc, logEntry) => {
|
||||||
const id = logEntry.requestId;
|
const id = logEntry.requestId;
|
||||||
|
|
@ -277,6 +400,7 @@
|
||||||
status: "Unknown"
|
status: "Unknown"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update fields based on log type
|
// Update fields based on log type
|
||||||
switch (logEntry.type) {
|
switch (logEntry.type) {
|
||||||
case 'CHAT_REQUEST_RECEIVED':
|
case 'CHAT_REQUEST_RECEIVED':
|
||||||
|
|
@ -304,29 +428,35 @@
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Convert grouped messages object to an array and sort by timestamp (most recent first)
|
// Convert grouped messages object to an array and sort by timestamp (most recent first)
|
||||||
const consolidatedMessages = Object.values(groupedMessages).sort((a, b) => {
|
const consolidatedMessages = Object.values(groupedMessages).sort((a, b) => {
|
||||||
// Sort by startTimestamp, most recent first
|
// Sort by startTimestamp, most recent first
|
||||||
return new Date(b.startTimestamp) - new Date(a.startTimestamp);
|
return new Date(b.startTimestamp) - new Date(a.startTimestamp);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (consolidatedMessages.length === 0) {
|
if (consolidatedMessages.length === 0) {
|
||||||
const row = messageHistoryBody.insertRow();
|
const row = messageHistoryBody.insertRow();
|
||||||
const cell = row.insertCell();
|
const cell = row.insertCell();
|
||||||
cell.colSpan = 8; // Adjusted for new column
|
cell.colSpan = 8; // Adjusted for new column
|
||||||
cell.textContent = 'No consolidated message history to display.';
|
cell.textContent = 'No consolidated message history to display.';
|
||||||
cell.style.textAlign = 'center';
|
cell.style.textAlign = 'center';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
consolidatedMessages.forEach(msg => {
|
consolidatedMessages.forEach(msg => {
|
||||||
const row = messageHistoryBody.insertRow();
|
const row = messageHistoryBody.insertRow();
|
||||||
row.insertCell().textContent = new Date(msg.startTimestamp).toLocaleString();
|
let cell;
|
||||||
row.insertCell().textContent = msg.endTimestamp ? new Date(msg.endTimestamp).toLocaleString() : (msg.status === "Request In Progress" ? "In Progress" : "N/A");
|
cell = row.insertCell(); cell.textContent = new Date(msg.startTimestamp).toLocaleString(); cell.classList.add('col-timestamp');
|
||||||
row.insertCell().textContent = msg.requestId;
|
cell = row.insertCell(); cell.textContent = msg.endTimestamp ? new Date(msg.endTimestamp).toLocaleString() : (msg.status === "Request In Progress" ? "In Progress" : "N/A"); cell.classList.add('col-timestamp');
|
||||||
row.appendChild(createPreCell(msg.fromClient));
|
cell = row.insertCell(); cell.textContent = msg.requestId; cell.classList.add('col-request-id');
|
||||||
row.appendChild(createPreCell(msg.toExtension));
|
|
||||||
row.appendChild(createPreCell(msg.fromExtension));
|
cell = createPreCell(msg.fromClient); cell.classList.add('col-data', 'cell-fromClient'); row.appendChild(cell);
|
||||||
row.appendChild(createPreCell(msg.toClient));
|
cell = createPreCell(msg.toExtension); cell.classList.add('col-data', 'cell-toExtension'); row.appendChild(cell);
|
||||||
row.insertCell().textContent = msg.status;
|
cell = createPreCell(msg.fromExtension); cell.classList.add('col-data', 'cell-fromExtension'); row.appendChild(cell);
|
||||||
|
cell = createPreCell(msg.toClient); cell.classList.add('col-data', 'cell-toClient'); row.appendChild(cell);
|
||||||
|
|
||||||
|
cell = row.insertCell(); cell.textContent = msg.status; cell.classList.add('col-status');
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching message history:', error);
|
console.error('Error fetching message history:', error);
|
||||||
|
|
@ -339,16 +469,19 @@
|
||||||
cell.style.textAlign = 'center';
|
cell.style.textAlign = 'center';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refreshButton) {
|
if (refreshButton) {
|
||||||
refreshButton.addEventListener('click', fetchAndDisplayMessageHistory);
|
refreshButton.addEventListener('click', fetchAndDisplayMessageHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAndDisplayMessageHistory(); // Initial load for messages
|
fetchAndDisplayMessageHistory(); // Initial load for messages
|
||||||
|
fetchAndDisplayServerInfo(); // Initial load for server info (status) on the messages tab
|
||||||
|
|
||||||
// Elements for settings and status
|
// Elements for settings and status
|
||||||
const portInputEl = document.getElementById('port-input'); // Corrected ID
|
const portInputEl = document.getElementById('port-input'); // Corrected ID
|
||||||
const requestTimeoutInputEl = document.getElementById('request-timeout-input');
|
const requestTimeoutInputEl = document.getElementById('request-timeout-input');
|
||||||
const newRequestBehaviorQueueEl = document.getElementById('newRequestBehaviorQueue');
|
const newRequestBehaviorQueueEl = document.getElementById('newRequestBehaviorQueue');
|
||||||
const newRequestBehaviorDropEl = document.getElementById('newRequestBehaviorDrop');
|
const newRequestBehaviorDropEl = document.getElementById('newRequestBehaviorDrop');
|
||||||
const autoKillPortInputEl = document.getElementById('auto-kill-port-input');
|
|
||||||
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
const saveSettingsBtn = document.getElementById('save-settings-btn');
|
||||||
const updateStatusMsgEl = document.getElementById('update-status-msg');
|
const updateStatusMsgEl = document.getElementById('update-status-msg');
|
||||||
const settingPingIntervalEl = document.getElementById('setting-ping-interval');
|
const settingPingIntervalEl = document.getElementById('setting-ping-interval');
|
||||||
|
|
@ -356,6 +489,7 @@
|
||||||
const statusUptimeEl = document.getElementById('status-uptime');
|
const statusUptimeEl = document.getElementById('status-uptime');
|
||||||
const statusConnectedExtensionsEl = document.getElementById('status-connected-extensions');
|
const statusConnectedExtensionsEl = document.getElementById('status-connected-extensions');
|
||||||
const restartServerBtn = document.getElementById('restart-server-btn');
|
const restartServerBtn = document.getElementById('restart-server-btn');
|
||||||
|
|
||||||
function formatUptime(totalSeconds) {
|
function formatUptime(totalSeconds) {
|
||||||
if (totalSeconds === null || totalSeconds === undefined) return 'N/A';
|
if (totalSeconds === null || totalSeconds === undefined) return 'N/A';
|
||||||
const days = Math.floor(totalSeconds / (3600 * 24));
|
const days = Math.floor(totalSeconds / (3600 * 24));
|
||||||
|
|
@ -364,6 +498,7 @@
|
||||||
totalSeconds %= 3600;
|
totalSeconds %= 3600;
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
const seconds = totalSeconds % 60;
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
let uptimeString = '';
|
let uptimeString = '';
|
||||||
if (days > 0) uptimeString += `${days}d `;
|
if (days > 0) uptimeString += `${days}d `;
|
||||||
if (hours > 0) uptimeString += `${hours}h `;
|
if (hours > 0) uptimeString += `${hours}h `;
|
||||||
|
|
@ -371,83 +506,123 @@
|
||||||
uptimeString += `${seconds}s`;
|
uptimeString += `${seconds}s`;
|
||||||
return uptimeString.trim() || '0s';
|
return uptimeString.trim() || '0s';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAndDisplayServerInfo() {
|
async function fetchAndDisplayServerInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/v1/admin/server-info');
|
// UPDATED: Fetch from the new consolidated settings endpoint
|
||||||
|
const response = await fetch('/admin/settings');
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const serverInfo = await response.json();
|
const currentSettings = await response.json();
|
||||||
// Populate Settings
|
|
||||||
if (portInputEl) portInputEl.value = serverInfo.port || ''; // Use portInputEl
|
// Populate Settings using the new response structure
|
||||||
if (requestTimeoutInputEl) requestTimeoutInputEl.value = serverInfo.requestTimeoutMs !== null ? serverInfo.requestTimeoutMs : '';
|
if(portInputEl) portInputEl.value = currentSettings.serverPort || '';
|
||||||
if (settingPingIntervalEl) settingPingIntervalEl.textContent = serverInfo.pingIntervalMs !== null ? `${serverInfo.pingIntervalMs} ms` : 'N/A (Not Implemented)';
|
if(requestTimeoutInputEl) requestTimeoutInputEl.value = currentSettings.requestTimeout !== null ? currentSettings.requestTimeout : '';
|
||||||
if (serverInfo.newRequestBehavior === 'drop') {
|
// Ping interval is not part of /admin/settings, assuming it's static or handled elsewhere if needed
|
||||||
if (newRequestBehaviorDropEl) newRequestBehaviorDropEl.checked = true;
|
// if(settingPingIntervalEl) settingPingIntervalEl.textContent = currentSettings.pingIntervalMs !== null ? `${currentSettings.pingIntervalMs} ms` : 'N/A (Not Implemented)';
|
||||||
|
|
||||||
|
if (currentSettings.messageSendStrategy === 'drop') {
|
||||||
|
if(newRequestBehaviorDropEl) newRequestBehaviorDropEl.checked = true;
|
||||||
} else {
|
} else {
|
||||||
if (newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default to queue
|
if(newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default to queue
|
||||||
}
|
}
|
||||||
if (autoKillPortInputEl) autoKillPortInputEl.checked = serverInfo.autoKillPort || false;
|
|
||||||
// Populate Status
|
// Populate Status - Assuming server-info might still be used for uptime/connections or these are separate
|
||||||
if (statusUptimeEl) statusUptimeEl.textContent = formatUptime(serverInfo.uptimeSeconds);
|
// For now, focusing on settings. If status also needs to come from /admin/settings, that's another change.
|
||||||
if (statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = serverInfo.connectedExtensionsCount !== null ? serverInfo.connectedExtensionsCount : 'N/A';
|
// Let's assume uptime and connectedExtensionsCount are fetched separately or are less critical for this immediate fix.
|
||||||
} catch (error) {
|
// For simplicity, I'll comment out the status population from this specific fetch if it was tied to the old server-info structure.
|
||||||
console.error('Error fetching server info:', error);
|
// If your /admin/settings also returns uptime and connectedExtensionsCount, uncomment and adjust field names.
|
||||||
if (portInputEl) portInputEl.value = 'Error';
|
/*
|
||||||
if (requestTimeoutInputEl) requestTimeoutInputEl.value = 'Error';
|
if(statusUptimeEl) statusUptimeEl.textContent = formatUptime(currentSettings.uptimeSeconds);
|
||||||
if (settingPingIntervalEl) settingPingIntervalEl.textContent = 'Error';
|
if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = currentSettings.connectedExtensionsCount !== null ? currentSettings.connectedExtensionsCount : 'N/A';
|
||||||
if (newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default on error
|
*/
|
||||||
if (autoKillPortInputEl) autoKillPortInputEl.checked = false; // Default on error
|
// If you still have a /v1/admin/server-info for uptime and connections, that part can remain as is.
|
||||||
// Ensure error handling for status elements
|
// This function will now primarily handle settings population.
|
||||||
if (statusUptimeEl) statusUptimeEl.textContent = 'Error loading uptime';
|
// To keep status functional if it's from a different endpoint, we might need to separate concerns
|
||||||
if (statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Error loading connections';
|
// or ensure /admin/settings returns everything. For now, focusing on fixing settings load.
|
||||||
|
|
||||||
|
// Fetch separate status info if needed (example if /v1/admin/server-info is still used for status)
|
||||||
|
const statusResponse = await fetch('/v1/admin/server-info');
|
||||||
|
if (statusResponse.ok) {
|
||||||
|
const serverInfoForStatus = await statusResponse.json();
|
||||||
|
if(statusUptimeEl) statusUptimeEl.textContent = formatUptime(serverInfoForStatus.uptimeSeconds);
|
||||||
|
if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = serverInfoForStatus.connectedExtensionsCount !== null ? serverInfoForStatus.connectedExtensionsCount : 'N/A';
|
||||||
|
if(settingPingIntervalEl && serverInfoForStatus.pingIntervalMs !== undefined) settingPingIntervalEl.textContent = serverInfoForStatus.pingIntervalMs !== null ? `${serverInfoForStatus.pingIntervalMs} ms` : 'N/A (Not Implemented)';
|
||||||
|
} else {
|
||||||
|
// Handle case where statusResponse itself is not ok
|
||||||
|
console.warn(`Failed to fetch status from /v1/admin/server-info: ${statusResponse.status}`);
|
||||||
|
if(statusUptimeEl) statusUptimeEl.textContent = 'Status N/A';
|
||||||
|
if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Status N/A';
|
||||||
|
if(settingPingIntervalEl) settingPingIntervalEl.textContent = 'Status N/A';
|
||||||
|
}
|
||||||
|
} catch (error) { // This is the single catch block for the try starting at line 310
|
||||||
|
console.error('Error in fetchAndDisplayServerInfo:', error);
|
||||||
|
// Set all relevant fields to an error state
|
||||||
|
if(portInputEl) portInputEl.value = 'Error';
|
||||||
|
if(requestTimeoutInputEl) requestTimeoutInputEl.value = 'Error';
|
||||||
|
if(settingPingIntervalEl) settingPingIntervalEl.textContent = 'Error';
|
||||||
|
if(newRequestBehaviorQueueEl) newRequestBehaviorQueueEl.checked = true; // Default on error
|
||||||
|
if(statusUptimeEl) statusUptimeEl.textContent = 'Error loading';
|
||||||
|
if(statusConnectedExtensionsEl) statusConnectedExtensionsEl.textContent = 'Error loading';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveSettings() {
|
async function handleSaveSettings() {
|
||||||
if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl || !autoKillPortInputEl) return;
|
if (!requestTimeoutInputEl || !portInputEl || !updateStatusMsgEl || !newRequestBehaviorQueueEl || !newRequestBehaviorDropEl) return;
|
||||||
|
|
||||||
const newTimeout = parseInt(requestTimeoutInputEl.value, 10);
|
const newTimeout = parseInt(requestTimeoutInputEl.value, 10);
|
||||||
const newPort = parseInt(portInputEl.value, 10);
|
const newPort = parseInt(portInputEl.value, 10);
|
||||||
const selectedNewRequestBehavior = newRequestBehaviorQueueEl.checked ? 'queue' : 'drop';
|
const selectedNewRequestBehavior = newRequestBehaviorQueueEl.checked ? 'queue' : 'drop';
|
||||||
const autoKillPort = autoKillPortInputEl.checked;
|
|
||||||
let settingsToUpdate = {};
|
let settingsToUpdate = {};
|
||||||
let validationError = false;
|
let validationError = false;
|
||||||
let messages = [];
|
let messages = [];
|
||||||
|
|
||||||
if (requestTimeoutInputEl.value.trim() !== '') { // Only process if there's input
|
if (requestTimeoutInputEl.value.trim() !== '') { // Only process if there's input
|
||||||
if (!isNaN(newTimeout) && newTimeout > 0) {
|
if (!isNaN(newTimeout) && newTimeout > 0) {
|
||||||
settingsToUpdate.requestTimeoutMs = newTimeout;
|
// UPDATED: Key name for server
|
||||||
|
settingsToUpdate.requestTimeout = newTimeout;
|
||||||
} else {
|
} else {
|
||||||
messages.push('Invalid timeout: Must be a positive number.');
|
messages.push('Invalid timeout: Must be a positive number.');
|
||||||
validationError = true;
|
validationError = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (portInputEl.value.trim() !== '') { // Only process if there's input
|
if (portInputEl.value.trim() !== '') { // Only process if there's input
|
||||||
if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
|
if (!isNaN(newPort) && newPort > 0 && newPort <= 65535) {
|
||||||
settingsToUpdate.port = newPort;
|
// UPDATED: Key name for server
|
||||||
|
settingsToUpdate.serverPort = newPort;
|
||||||
} else {
|
} else {
|
||||||
messages.push('Invalid port: Must be between 1 and 65535.');
|
messages.push('Invalid port: Must be between 1 and 65535.');
|
||||||
validationError = true;
|
validationError = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always include newRequestBehavior as it's controlled by radio buttons
|
// Always include newRequestBehavior as it's controlled by radio buttons
|
||||||
// No specific validation needed here as it's either 'queue' or 'drop'
|
// No specific validation needed here as it's either 'queue' or 'drop'
|
||||||
settingsToUpdate.newRequestBehavior = selectedNewRequestBehavior;
|
// UPDATED: Key name for server
|
||||||
settingsToUpdate.autoKillPort = autoKillPort; // Add the new setting
|
settingsToUpdate.messageSendStrategy = selectedNewRequestBehavior;
|
||||||
|
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
updateStatusMsgEl.textContent = messages.join(' ');
|
updateStatusMsgEl.textContent = messages.join(' ');
|
||||||
updateStatusMsgEl.style.color = 'red';
|
updateStatusMsgEl.style.color = 'red';
|
||||||
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 7000);
|
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 7000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(settingsToUpdate).length === 0) {
|
if (Object.keys(settingsToUpdate).length === 0) {
|
||||||
updateStatusMsgEl.textContent = 'No changes to save.';
|
updateStatusMsgEl.textContent = 'No changes to save.';
|
||||||
updateStatusMsgEl.style.color = 'blue';
|
updateStatusMsgEl.style.color = 'blue';
|
||||||
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 5000);
|
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusMsgEl.textContent = 'Saving settings...';
|
updateStatusMsgEl.textContent = 'Saving settings...';
|
||||||
updateStatusMsgEl.style.color = 'orange';
|
updateStatusMsgEl.style.color = 'orange';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/v1/admin/update-settings', {
|
// UPDATED: Endpoint for saving settings
|
||||||
|
const response = await fetch('/admin/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(settingsToUpdate)
|
body: JSON.stringify(settingsToUpdate)
|
||||||
|
|
@ -471,9 +646,11 @@
|
||||||
const clearTime = updateStatusMsgEl.textContent.toLowerCase().includes('restart') ? 15000 : 7000;
|
const clearTime = updateStatusMsgEl.textContent.toLowerCase().includes('restart') ? 15000 : 7000;
|
||||||
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, clearTime);
|
setTimeout(() => { updateStatusMsgEl.textContent = ''; }, clearTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveSettingsBtn) {
|
if (saveSettingsBtn) {
|
||||||
saveSettingsBtn.addEventListener('click', handleSaveSettings);
|
saveSettingsBtn.addEventListener('click', handleSaveSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRestartServer() {
|
async function handleRestartServer() {
|
||||||
if (confirm('Are you sure you want to restart the server?')) {
|
if (confirm('Are you sure you want to restart the server?')) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -486,13 +663,61 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restartServerBtn) {
|
if (restartServerBtn) {
|
||||||
restartServerBtn.addEventListener('click', handleRestartServer);
|
restartServerBtn.addEventListener('click', handleRestartServer);
|
||||||
}
|
}
|
||||||
// Initial load for settings and status
|
|
||||||
fetchAndDisplayServerInfo();
|
// Initial load for settings is handled by tab click or initial messages tab load.
|
||||||
console.log("Admin UI initialized. Message history, settings, status, and restart functionality implemented.");
|
// fetchAndDisplayServerInfo(); // This is called when messages tab is active or settings tab is clicked.
|
||||||
|
|
||||||
|
// Collapsible column logic
|
||||||
|
const columnStates = { // To store the collapsed state
|
||||||
|
fromClient: false,
|
||||||
|
toExtension: false,
|
||||||
|
fromExtension: false,
|
||||||
|
toClient: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleColumn(columnKey) {
|
||||||
|
columnStates[columnKey] = !columnStates[columnKey];
|
||||||
|
const isCollapsed = columnStates[columnKey];
|
||||||
|
|
||||||
|
// Update button text/indicator
|
||||||
|
const button = document.querySelector(`.collapse-btn[data-column="${columnKey}"]`);
|
||||||
|
if (button) {
|
||||||
|
button.textContent = isCollapsed ? '+' : '-';
|
||||||
|
button.title = isCollapsed ? 'Expand Column' : 'Collapse Column';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle header visibility (optional, if you want to hide header text too)
|
||||||
|
// const headerCell = document.querySelector(`th.col-data[data-column-key="${columnKey}"]`); // Need to add data-column-key to th if this is desired
|
||||||
|
|
||||||
|
// Toggle data cell visibility (these are the <td> elements)
|
||||||
|
const cellsToToggle = document.querySelectorAll(`#message-history-body td.cell-${columnKey}`);
|
||||||
|
cellsToToggle.forEach(cell => {
|
||||||
|
cell.style.display = isCollapsed ? 'none' : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special handling for the header cell (<th>) due to table-layout: fixed
|
||||||
|
const headerTh = document.querySelector(`th[data-column="${columnKey}"]`);
|
||||||
|
if (headerTh) {
|
||||||
|
if (isCollapsed) {
|
||||||
|
headerTh.classList.add('header-collapsed');
|
||||||
|
} else {
|
||||||
|
headerTh.classList.remove('header-collapsed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.collapse-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const columnKey = this.dataset.column;
|
||||||
|
toggleColumn(columnKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Admin UI initialized. Message history, settings, status, restart, and collapsible columns functionality implemented.");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* Chat Relay: Relay for AI Chat Interfaces
|
|
||||||
* Copyright (C) 2025 Jamison Moore
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
// Import the server
|
|
||||||
require('./server');
|
|
||||||
|
|
@ -1,473 +0,0 @@
|
||||||
/*
|
|
||||||
* Chat Relay: Relay for AI Chat Interfaces
|
|
||||||
* Copyright (C) 2025 Jamison Moore
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
const express = require('express');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const cors = require('cors');
|
|
||||||
const { WebSocketServer } = require('ws');
|
|
||||||
const http = require('http');
|
|
||||||
|
|
||||||
// Create Express app
|
|
||||||
const app = express();
|
|
||||||
app.use(cors());
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/health', (req, res) => {
|
|
||||||
const aliveConnections = activeConnections.filter(conn => conn.isAlive);
|
|
||||||
res.status(200).json({
|
|
||||||
status: 'ok',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
activeBrowserConnections: aliveConnections.length,
|
|
||||||
totalTrackedBrowserConnections: activeConnections.length,
|
|
||||||
webSocketServerState: wss.options.server.listening ? 'listening' : 'not_listening' // wss.readyState is not standard for server
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create HTTP server
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
// Create WebSocket server for browser extension communication
|
|
||||||
const wss = new WebSocketServer({ server });
|
|
||||||
|
|
||||||
// Global variables
|
|
||||||
let activeConnections = [];
|
|
||||||
const pendingRequests = new Map();
|
|
||||||
let requestCounter = 0;
|
|
||||||
|
|
||||||
// Connection health check interval (in milliseconds)
|
|
||||||
const PING_INTERVAL = 30000; // 30 seconds
|
|
||||||
const CONNECTION_TIMEOUT = 45000; // 45 seconds
|
|
||||||
|
|
||||||
// Handle WebSocket connections from browser extensions
|
|
||||||
wss.on('connection', (ws, req) => { // Added req to log client IP
|
|
||||||
const clientIp = req.socket.remoteAddress;
|
|
||||||
console.log(`SERVER: Browser extension connected from IP: ${clientIp}`);
|
|
||||||
|
|
||||||
// Initialize connection state
|
|
||||||
ws.isAlive = true;
|
|
||||||
ws.pendingPing = false;
|
|
||||||
ws.lastActivity = Date.now();
|
|
||||||
|
|
||||||
// Add to active connections
|
|
||||||
activeConnections.push(ws);
|
|
||||||
|
|
||||||
// Set up ping interval for this connection
|
|
||||||
const pingInterval = setInterval(() => {
|
|
||||||
// Check if connection is still alive
|
|
||||||
if (!ws.isAlive) {
|
|
||||||
console.log('Browser extension connection timed out, terminating');
|
|
||||||
clearInterval(pingInterval);
|
|
||||||
ws.terminate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're still waiting for a pong from the last ping, mark as not alive
|
|
||||||
if (ws.pendingPing) {
|
|
||||||
console.log('Browser extension not responding to ping, marking as inactive');
|
|
||||||
ws.isAlive = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's been activity recently
|
|
||||||
const inactiveTime = Date.now() - ws.lastActivity;
|
|
||||||
if (inactiveTime > CONNECTION_TIMEOUT) {
|
|
||||||
console.log(`Browser extension inactive for ${inactiveTime}ms, sending ping`);
|
|
||||||
// Send a ping to check if still alive
|
|
||||||
ws.pendingPing = true;
|
|
||||||
try {
|
|
||||||
ws.ping();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending ping:', error);
|
|
||||||
ws.isAlive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, PING_INTERVAL);
|
|
||||||
|
|
||||||
// Handle pong messages (response to ping)
|
|
||||||
ws.on('pong', () => {
|
|
||||||
ws.isAlive = true;
|
|
||||||
ws.pendingPing = false;
|
|
||||||
ws.lastActivity = Date.now();
|
|
||||||
console.log('Browser extension responded to ping');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle messages from browser extension
|
|
||||||
ws.on('message', (messageBuffer) => {
|
|
||||||
const rawMessage = messageBuffer.toString();
|
|
||||||
console.log(`SERVER: Received raw message from extension (IP: ${clientIp}): ${rawMessage.substring(0, 500)}${rawMessage.length > 500 ? '...' : ''}`);
|
|
||||||
try {
|
|
||||||
// Update last activity timestamp
|
|
||||||
ws.lastActivity = Date.now();
|
|
||||||
|
|
||||||
const data = JSON.parse(rawMessage);
|
|
||||||
console.log(`SERVER: Parsed message data from extension (IP: ${clientIp}):`, data);
|
|
||||||
|
|
||||||
const { requestId, type } = data;
|
|
||||||
|
|
||||||
if (requestId === undefined) {
|
|
||||||
console.warn(`SERVER: Received message without requestId from IP ${clientIp}:`, data);
|
|
||||||
// Handle other non-request-specific messages if any (e.g., status pings initiated by extension)
|
|
||||||
if (type === 'EXTENSION_STATUS') {
|
|
||||||
console.log(`SERVER: Browser extension status from IP ${clientIp}: ${data.status}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log based on new message types from background.js
|
|
||||||
if (type === 'CHAT_RESPONSE_CHUNK') {
|
|
||||||
const chunkContent = data.chunk ? data.chunk.substring(0, 200) + (data.chunk.length > 200 ? '...' : '') : '[empty chunk]';
|
|
||||||
console.log(`SERVER: Received CHAT_RESPONSE_CHUNK for requestId: ${requestId} from IP ${clientIp}. Chunk (first 200): ${chunkContent}. IsFinal: ${data.isFinal}`);
|
|
||||||
|
|
||||||
const pendingRequest = pendingRequests.get(requestId);
|
|
||||||
if (pendingRequest) {
|
|
||||||
console.log(`SERVER: Processing CHAT_RESPONSE_CHUNK for pending request ${requestId} from IP ${clientIp}. IsFinal: ${data.isFinal}, Chunk (first 200): ${chunkContent}`);
|
|
||||||
// Initialize accumulatedChunks if it doesn't exist (should be set on creation)
|
|
||||||
if (typeof pendingRequest.accumulatedChunks === 'undefined') {
|
|
||||||
pendingRequest.accumulatedChunks = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.chunk) { // Ensure chunk is not null or undefined
|
|
||||||
pendingRequest.accumulatedChunks += data.chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.isFinal) {
|
|
||||||
console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) received final CHAT_RESPONSE_CHUNK. Attempting to resolve promise.`);
|
|
||||||
if (pendingRequest.timeoutId) {
|
|
||||||
clearTimeout(pendingRequest.timeoutId);
|
|
||||||
console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) timeout cleared.`);
|
|
||||||
}
|
|
||||||
pendingRequest.resolve(pendingRequest.accumulatedChunks);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) promise resolved and removed from pending. Total length: ${pendingRequest.accumulatedChunks.length}`);
|
|
||||||
} else {
|
|
||||||
console.log(`SERVER: Accumulated chunk for requestId ${requestId} (IP: ${clientIp}). Current total length: ${pendingRequest.accumulatedChunks.length}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`SERVER: Received CHAT_RESPONSE_CHUNK for request ${requestId} (IP: ${clientIp}, isFinal: ${data.isFinal}), but no pending request found.`);
|
|
||||||
}
|
|
||||||
} else if (type === 'CHAT_RESPONSE_STREAM_ENDED') {
|
|
||||||
const pendingRequestStream = pendingRequests.get(requestId);
|
|
||||||
if (pendingRequestStream) {
|
|
||||||
console.log(`SERVER: Processing CHAT_RESPONSE_STREAM_ENDED for pending request ${requestId} (IP: ${clientIp}).`);
|
|
||||||
// This message type now primarily signals the end. The actual data comes in CHAT_RESPONSE_CHUNK.
|
|
||||||
// If a request is still pending and we haven't resolved it with a final chunk,
|
|
||||||
// it might indicate an issue or a stream that ended without complete data.
|
|
||||||
if (!pendingRequestStream.resolved) {
|
|
||||||
console.warn(`SERVER: Stream ended for requestId ${requestId} (IP: ${clientIp}), but request was not fully resolved with data. This might be an issue.`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`SERVER: Received CHAT_RESPONSE_STREAM_ENDED for request ${requestId} (IP: ${clientIp}), but no pending request found.`);
|
|
||||||
}
|
|
||||||
} else if (type === 'CHAT_RESPONSE_ERROR') {
|
|
||||||
const errorMsg = data.error || "Unknown error from extension.";
|
|
||||||
console.error(`SERVER: Received CHAT_RESPONSE_ERROR for requestId: ${requestId} (IP: ${clientIp}). Error: ${errorMsg}`);
|
|
||||||
const pendingRequestError = pendingRequests.get(requestId);
|
|
||||||
if (pendingRequestError) {
|
|
||||||
console.log(`SERVER: Processing CHAT_RESPONSE_ERROR for pending request ${requestId} (IP: ${clientIp}).`);
|
|
||||||
if (pendingRequestError.timeoutId) {
|
|
||||||
clearTimeout(pendingRequestError.timeoutId);
|
|
||||||
console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) timeout cleared due to error.`);
|
|
||||||
}
|
|
||||||
pendingRequestError.reject(new Error(`Extension reported error for request ${requestId}: ${errorMsg}`));
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
console.log(`SERVER: Request ${requestId} (IP: ${clientIp}) rejected due to CHAT_RESPONSE_ERROR and removed from pending.`);
|
|
||||||
} else {
|
|
||||||
console.log(`SERVER: Received CHAT_RESPONSE_ERROR for request ${requestId} (IP: ${clientIp}), but no pending request found.`);
|
|
||||||
}
|
|
||||||
} else if (type === 'CHAT_RESPONSE') { // Keep old CHAT_RESPONSE for compatibility if content script DOM fallback sends it
|
|
||||||
const { response } = data;
|
|
||||||
console.log(`SERVER: Received (legacy) CHAT_RESPONSE for requestId: ${requestId} from IP ${clientIp}. Response (first 100): ${response ? response.substring(0,100) : '[empty]'}`);
|
|
||||||
const pendingRequest = pendingRequests.get(requestId);
|
|
||||||
if (pendingRequest) {
|
|
||||||
if (pendingRequest.timeoutId) clearTimeout(pendingRequest.timeoutId);
|
|
||||||
pendingRequest.resolve(response);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
console.log(`SERVER: Request ${requestId} resolved with (legacy) CHAT_RESPONSE from IP ${clientIp}.`);
|
|
||||||
} else {
|
|
||||||
console.log(`SERVER: Received (legacy) CHAT_RESPONSE for request ${requestId} from IP ${clientIp}, but no pending request found.`);
|
|
||||||
}
|
|
||||||
} else if (type === 'EXTENSION_ERROR') { // General extension error not tied to a request
|
|
||||||
console.error(`SERVER: Browser extension (IP: ${clientIp}) reported general error: ${data.error}`);
|
|
||||||
} else if (type === 'EXTENSION_STATUS') {
|
|
||||||
console.log(`SERVER: Browser extension (IP: ${clientIp}) status: ${data.status}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`SERVER: Received unknown message type '${type}' from IP ${clientIp} for requestId ${requestId}:`, data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`SERVER: Error processing WebSocket message from IP ${clientIp}:`, error, `Raw message: ${rawMessage}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle disconnection
|
|
||||||
ws.on('close', (code, reason) => {
|
|
||||||
const reasonString = reason ? reason.toString() : 'No reason given';
|
|
||||||
console.log(`SERVER: Browser extension (IP: ${clientIp}) disconnected. Code: ${code}, Reason: ${reasonString}`);
|
|
||||||
clearInterval(pingInterval);
|
|
||||||
activeConnections = activeConnections.filter(conn => conn !== ws);
|
|
||||||
|
|
||||||
// Check if there are any pending requests that were using this connection
|
|
||||||
// and reject them with a connection closed error
|
|
||||||
pendingRequests.forEach((request, requestId) => {
|
|
||||||
if (request.connection === ws) {
|
|
||||||
console.log(`Rejecting request ${requestId} due to connection close`);
|
|
||||||
request.reject(new Error('Browser extension disconnected'));
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors
|
|
||||||
ws.on('error', (error) => {
|
|
||||||
console.error(`SERVER: WebSocket error for connection from IP ${clientIp}:`, error);
|
|
||||||
ws.isAlive = false; // Mark as not alive on error
|
|
||||||
// Consider terminating and cleaning up like in 'close' if error is fatal
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create API router
|
|
||||||
const apiRouter = express.Router();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const REQUEST_TIMEOUT = 300000; // 5 minutes (in milliseconds)
|
|
||||||
const MAX_RETRIES = 2; // Maximum number of retries for a failed request
|
|
||||||
|
|
||||||
// Helper function to find the best active connection
|
|
||||||
function getBestConnection() {
|
|
||||||
// Filter out connections that are not alive
|
|
||||||
const aliveConnections = activeConnections.filter(conn => conn.isAlive);
|
|
||||||
|
|
||||||
if (aliveConnections.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort connections by last activity (most recent first)
|
|
||||||
aliveConnections.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
||||||
|
|
||||||
return aliveConnections[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAI-compatible chat completions endpoint
|
|
||||||
apiRouter.post('/chat/completions', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { messages, model, temperature, max_tokens } = req.body;
|
|
||||||
console.log(`SERVER: Full incoming HTTP request body for request ID (to be generated):`, JSON.stringify(req.body, null, 2));
|
|
||||||
|
|
||||||
// Generate a unique request ID
|
|
||||||
const requestId = requestCounter++;
|
|
||||||
|
|
||||||
// Extract the user's message (last message in the array)
|
|
||||||
const userMessage = messages[messages.length - 1].content;
|
|
||||||
|
|
||||||
// Get the best active connection
|
|
||||||
const extension = getBestConnection();
|
|
||||||
|
|
||||||
// Check if we have any active connections
|
|
||||||
if (!extension) {
|
|
||||||
return res.status(503).json({
|
|
||||||
error: {
|
|
||||||
message: "No active browser extension connected. Please open the chat interface and ensure the extension is active.",
|
|
||||||
type: "server_error",
|
|
||||||
code: "no_extension_connected"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a promise that will be resolved when the response is received
|
|
||||||
console.log(`SERVER: Request ${requestId} creating response promise.`);
|
|
||||||
const responsePromise = new Promise((resolve, reject) => {
|
|
||||||
const internalResolve = (value) => {
|
|
||||||
console.log(`SERVER: Request ${requestId} internal promise resolve function called.`);
|
|
||||||
resolve(value);
|
|
||||||
};
|
|
||||||
const internalReject = (reason) => {
|
|
||||||
console.log(`SERVER: Request ${requestId} internal promise reject function called.`);
|
|
||||||
reject(reason);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set a timeout to reject the promise after the configured timeout
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
if (pendingRequests.has(requestId)) {
|
|
||||||
console.error(`SERVER: Request ${requestId} timed out after ${REQUEST_TIMEOUT}ms. Rejecting promise.`);
|
|
||||||
pendingRequests.delete(requestId); // Ensure cleanup
|
|
||||||
internalReject(new Error('Request timed out'));
|
|
||||||
} else {
|
|
||||||
console.warn(`SERVER: Request ${requestId} timeout triggered, but request no longer in pendingRequests. It might have resolved or errored just before timeout.`);
|
|
||||||
}
|
|
||||||
}, REQUEST_TIMEOUT);
|
|
||||||
|
|
||||||
// Store the promise resolvers and the connection being used
|
|
||||||
pendingRequests.set(requestId, {
|
|
||||||
resolve: internalResolve,
|
|
||||||
reject: internalReject,
|
|
||||||
connection: extension,
|
|
||||||
timeoutId,
|
|
||||||
retryCount: 0,
|
|
||||||
accumulatedChunks: '' // Initialize for chunk accumulation
|
|
||||||
});
|
|
||||||
console.log(`SERVER: Request ${requestId} added to pendingRequests. Timeout ID: ${timeoutId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prepare the message
|
|
||||||
const message = {
|
|
||||||
type: 'SEND_CHAT_MESSAGE',
|
|
||||||
requestId,
|
|
||||||
message: userMessage,
|
|
||||||
settings: {
|
|
||||||
model,
|
|
||||||
temperature,
|
|
||||||
max_tokens
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send the message to the browser extension
|
|
||||||
try {
|
|
||||||
console.log(`SERVER: Request ${requestId} - Sending full message to extension:`, JSON.stringify(message, null, 2));
|
|
||||||
extension.send(JSON.stringify(message));
|
|
||||||
console.log(`SERVER: Request ${requestId} (message type: ${message.type}) sent to browser extension (IP: ${extension.remoteAddress || 'unknown'}). Waiting for response...`);
|
|
||||||
|
|
||||||
// Update last activity timestamp
|
|
||||||
extension.lastActivity = Date.now();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error sending message to extension for request ${requestId}:`, error);
|
|
||||||
|
|
||||||
// Clean up the pending request
|
|
||||||
if (pendingRequests.has(requestId)) {
|
|
||||||
const pendingRequest = pendingRequests.get(requestId);
|
|
||||||
if (pendingRequest.timeoutId) {
|
|
||||||
clearTimeout(pendingRequest.timeoutId);
|
|
||||||
}
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(500).json({
|
|
||||||
error: {
|
|
||||||
message: "Failed to send message to browser extension",
|
|
||||||
type: "server_error",
|
|
||||||
code: "extension_communication_error"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the response
|
|
||||||
const awaitStartTime = Date.now();
|
|
||||||
console.log(`SERVER: Request ${requestId} is now awaiting responsePromise (extension response). Timeout set to ${REQUEST_TIMEOUT}ms.`);
|
|
||||||
const response = await responsePromise;
|
|
||||||
const awaitEndTime = Date.now();
|
|
||||||
console.log(`SERVER: Request ${requestId} await responsePromise completed in ${awaitEndTime - awaitStartTime}ms. Received response from extension. Preparing to send to client.`);
|
|
||||||
|
|
||||||
// Format the response in OpenAI format
|
|
||||||
const formatStartTime = Date.now();
|
|
||||||
const formattedResponse = {
|
|
||||||
id: `chatcmpl-${Date.now()}`,
|
|
||||||
object: "chat.completion",
|
|
||||||
created: Math.floor(Date.now() / 1000),
|
|
||||||
model: model || "relay-model", // model is from req.body
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
message: {
|
|
||||||
role: "assistant",
|
|
||||||
content: response // response is the string from the extension
|
|
||||||
},
|
|
||||||
finish_reason: "stop"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
usage: {
|
|
||||||
prompt_tokens: -1, // We don't track tokens
|
|
||||||
completion_tokens: -1,
|
|
||||||
total_tokens: -1
|
|
||||||
}
|
|
||||||
// Removed service_tier, logprobs, refusal, annotations, and detailed usage to match simpler working version
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`SERVER: Request ${requestId} - Full outgoing HTTP response body:`, JSON.stringify(formattedResponse, null, 2));
|
|
||||||
res.json(formattedResponse);
|
|
||||||
const sendEndTime = Date.now();
|
|
||||||
console.log(`SERVER: Request ${requestId} formatted and sent response to client in ${sendEndTime - formatStartTime}ms (total after await: ${sendEndTime - awaitEndTime}ms).`);
|
|
||||||
} catch (error) {
|
|
||||||
const reqIdForLog = typeof requestId !== 'undefined' ? requestId : (error && typeof error.requestId !== 'undefined' ? error.requestId : 'UNKNOWN');
|
|
||||||
console.error(`SERVER: Error processing chat completion for request ${reqIdForLog}:`, error);
|
|
||||||
if (typeof requestId === 'undefined') {
|
|
||||||
console.error(`SERVER: CRITICAL - 'requestId' was undefined in catch block. Error object requestId: ${error && error.requestId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the appropriate status code based on the error
|
|
||||||
let statusCode = 500;
|
|
||||||
let errorType = "server_error";
|
|
||||||
let errorCode = "internal_error";
|
|
||||||
|
|
||||||
if (error.message === 'Request timed out') {
|
|
||||||
statusCode = 504; // Gateway Timeout
|
|
||||||
errorType = "timeout_error";
|
|
||||||
errorCode = "request_timeout";
|
|
||||||
} else if (error.message === 'Browser extension disconnected') {
|
|
||||||
statusCode = 503; // Service Unavailable
|
|
||||||
errorType = "server_error";
|
|
||||||
errorCode = "extension_disconnected";
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorResponsePayload = {
|
|
||||||
error: {
|
|
||||||
message: error.message,
|
|
||||||
type: errorType,
|
|
||||||
code: errorCode
|
|
||||||
}
|
|
||||||
};
|
|
||||||
console.log(`SERVER: Request ${reqIdForLog} - Sending error response to client:`, JSON.stringify(errorResponsePayload, null, 2));
|
|
||||||
res.status(statusCode).json(errorResponsePayload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Models endpoint
|
|
||||||
apiRouter.get('/models', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
object: "list",
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: "gemini-pro",
|
|
||||||
object: "model",
|
|
||||||
created: 1677610602,
|
|
||||||
owned_by: "relay"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "chatgpt",
|
|
||||||
object: "model",
|
|
||||||
created: 1677610602,
|
|
||||||
owned_by: "relay"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "claude-3",
|
|
||||||
object: "model",
|
|
||||||
created: 1677610602,
|
|
||||||
owned_by: "relay"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mount the API router
|
|
||||||
app.use('/v1', apiRouter);
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
const PORT = process.env.PORT || 3003;
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log(`OpenAI-compatible relay server running on port ${PORT}`);
|
|
||||||
console.log(`WebSocket server for browser extensions running on ws://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = server;
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,8 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowJs": true // Add this line
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -294,20 +294,30 @@ class ClaudeProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDebuggerData(requestId, rawData, isFinalFromBackground) {
|
handleDebuggerData(requestId, rawData, isFinalFromBackground, errorFromBackground = null) {
|
||||||
// !!!!! VERY IMPORTANT ENTRY LOG !!!!!
|
// !!!!! VERY IMPORTANT ENTRY LOG !!!!!
|
||||||
console.log('[[ClaudeProvider]] handleDebuggerData ENTERED. RequestId: ' + requestId + ', isFinalFromBackground: ' + isFinalFromBackground + ', RawData Length: ' + (rawData ? rawData.length : 'null'));
|
console.log('[[ClaudeProvider]] handleDebuggerData ENTERED. RequestId: ' + requestId + ', isFinalFromBackground: ' + isFinalFromBackground + ', RawData Length: ' + (rawData ? rawData.length : 'null') + ', ErrorFromBG: ' + errorFromBackground);
|
||||||
|
|
||||||
const callback = this.pendingResponseCallbacks.get(requestId);
|
const callback = this.pendingResponseCallbacks.get(requestId);
|
||||||
|
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
console.warn('[' + this.name + '] No pending callback for requestId: ' + requestId + '. Ignoring.');
|
console.warn('[' + this.name + '] No pending callback for requestId: ' + requestId + '. Ignoring debugger data/error.');
|
||||||
if (this.requestBuffers.has(requestId)) {
|
if (this.requestBuffers.has(requestId)) {
|
||||||
this.requestBuffers.delete(requestId);
|
this.requestBuffers.delete(requestId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorFromBackground) {
|
||||||
|
console.warn(`[${this.name}] handleDebuggerData: Propagating error for requestId ${requestId}: ${errorFromBackground}`);
|
||||||
|
callback(requestId, `[Provider Error from Background] ${errorFromBackground}`, true); // Pass error as text, mark as final
|
||||||
|
this.pendingResponseCallbacks.delete(requestId);
|
||||||
|
if (this.requestBuffers.has(requestId)) {
|
||||||
|
this.requestBuffers.delete(requestId); // Clean up buffer too
|
||||||
|
}
|
||||||
|
return; // Stop further processing
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.requestBuffers.has(requestId)) {
|
if (!this.requestBuffers.has(requestId)) {
|
||||||
this.requestBuffers.set(requestId, { accumulatedText: "" });
|
this.requestBuffers.set(requestId, { accumulatedText: "" });
|
||||||
}
|
}
|
||||||
|
|
@ -510,7 +520,7 @@ class ClaudeProvider {
|
||||||
console.log(`[${this.name}] getStreamingApiPatterns called. Capture method: ${this.captureMethod}`);
|
console.log(`[${this.name}] getStreamingApiPatterns called. Capture method: ${this.captureMethod}`);
|
||||||
if (this.captureMethod === "debugger" && this.debuggerUrlPattern) {
|
if (this.captureMethod === "debugger" && this.debuggerUrlPattern) {
|
||||||
console.log(`[${this.name}] Using debugger URL pattern: ${this.debuggerUrlPattern}`);
|
console.log(`[${this.name}] Using debugger URL pattern: ${this.debuggerUrlPattern}`);
|
||||||
return [{ urlPattern: this.debuggerUrlPattern, requestStage: "Response" }];
|
return [{ urlPattern: this.debuggerUrlPattern }];
|
||||||
}
|
}
|
||||||
console.log(`[${this.name}] No debugger patterns to return (captureMethod is not 'debugger' or no pattern set).`);
|
console.log(`[${this.name}] No debugger patterns to return (captureMethod is not 'debugger' or no pattern set).`);
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -22,58 +22,69 @@ class GeminiProvider {
|
||||||
this.name = 'GeminiProvider'; // Updated
|
this.name = 'GeminiProvider'; // Updated
|
||||||
this.supportedDomains = ['gemini.google.com'];
|
this.supportedDomains = ['gemini.google.com'];
|
||||||
|
|
||||||
|
// --- START OF CONFIGURABLE PROPERTIES (similar to other providers) ---
|
||||||
|
this.captureMethod = "dom"; // Switched to DOM capture by default
|
||||||
|
// TODO: DEVELOPER ACTION REQUIRED! Verify this URL pattern with Gemini's actual network requests.
|
||||||
|
this.debuggerUrlPattern = "*StreamGenerate*"; // Kept for potential future switch
|
||||||
|
this.includeThinkingInMessage = false; // Gemini likely doesn't have a separate "thinking" stream like some others.
|
||||||
|
// --- END OF CONFIGURABLE PROPERTIES ---
|
||||||
|
|
||||||
// Selectors for the Gemini interface
|
// Selectors for the Gemini interface
|
||||||
this.inputSelector = 'div.ql-editor, div[contenteditable="true"], textarea[placeholder="Enter a prompt here"], textarea.message-input, textarea.input-area';
|
this.inputSelector = 'div.ql-editor, div[contenteditable="true"], textarea[placeholder="Enter a prompt here"], textarea.message-input, textarea.input-area';
|
||||||
this.sendButtonSelector = 'button[aria-label="Send message"], button.send-button, button.send-message-button';
|
this.sendButtonSelector = 'button[aria-label="Send message"], button.send-button, button.send-message-button';
|
||||||
|
|
||||||
// Response selector - updated to match the actual elements
|
// Response selector - updated to the specific div for DOM capture
|
||||||
this.responseSelector = 'model-response, message-content, .model-response-text, .markdown-main-panel, .model-response, div[id^="model-response-message"]';
|
this.responseSelector = 'div.markdown.markdown-main-panel[id^="model-response-message-content"]';
|
||||||
// Thinking indicator selector
|
// Thinking indicator selector - kept as is, assuming these are still relevant
|
||||||
this.thinkingIndicatorSelector = '.thinking-indicator, .loading-indicator, .typing-indicator, .response-loading, .blue-circle, .stop-icon';
|
this.thinkingIndicatorSelector = '.thinking-indicator, .loading-indicator, .typing-indicator, .response-loading, .blue-circle, .stop-icon';
|
||||||
|
|
||||||
// Fallback selectors (NEW)
|
// Fallback selectors are less relevant now that DOM is primary but kept for completeness
|
||||||
this.responseSelectorForDOMFallback = 'model-response, message-content, .model-response-text, .markdown-main-panel'; // Placeholder
|
this.responseSelectorForDOMFallback = 'model-response, message-content, .model-response-text, .markdown-main-panel';
|
||||||
this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .loading-indicator, .blue-circle, .stop-icon'; // Placeholder
|
this.thinkingIndicatorSelectorForDOM = '.thinking-indicator, .loading-indicator, .blue-circle, .stop-icon';
|
||||||
|
|
||||||
// Last sent message to avoid capturing it as a response
|
// Last sent message to avoid capturing it as a response
|
||||||
this.lastSentMessage = '';
|
this.lastSentMessage = '';
|
||||||
// Flag to prevent double-sending - IMPORTANT: This must be false by default
|
// Flag to prevent double-sending - IMPORTANT: This must be false by default
|
||||||
this.hasSentMessage = false;
|
this.hasSentMessage = false;
|
||||||
|
|
||||||
|
// For debugger-based response capture
|
||||||
|
this.pendingResponseCallbacks = new Map();
|
||||||
|
this.requestAccumulators = new Map(); // To accumulate response chunks for a given request
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a message to the chat interface (MODIFIED)
|
// Send a message to the chat interface (MODIFIED)
|
||||||
async sendChatMessage(text) {
|
async sendChatMessage(text) {
|
||||||
console.log(`[${this.name}] sendChatMessage called with:`, text);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] sendChatMessage called with:`, text);
|
||||||
const inputElement = document.querySelector(this.inputSelector);
|
const inputElement = document.querySelector(this.inputSelector);
|
||||||
const sendButton = document.querySelector(this.sendButtonSelector);
|
const sendButton = document.querySelector(this.sendButtonSelector);
|
||||||
|
|
||||||
if (!inputElement || !sendButton) {
|
if (!inputElement || !sendButton) {
|
||||||
console.error(`[${this.name}] Missing input field (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
|
console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Missing input field (${this.inputSelector}) or send button (${this.sendButtonSelector})`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[${this.name}] Attempting to send message with:`, {
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Attempting to send message with:`, {
|
||||||
inputFieldInfo: inputElement.outerHTML.substring(0,100),
|
inputFieldInfo: inputElement.outerHTML.substring(0,100),
|
||||||
sendButtonInfo: sendButton.outerHTML.substring(0,100)
|
sendButtonInfo: sendButton.outerHTML.substring(0,100)
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.lastSentMessage = text;
|
this.lastSentMessage = text;
|
||||||
console.log(`[${this.name}] Stored last sent message:`, this.lastSentMessage);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Stored last sent message:`, this.lastSentMessage);
|
||||||
|
|
||||||
if (inputElement.tagName.toLowerCase() === 'div' && (inputElement.contentEditable === 'true' || inputElement.getAttribute('contenteditable') === 'true')) {
|
if (inputElement.tagName.toLowerCase() === 'div' && (inputElement.contentEditable === 'true' || inputElement.getAttribute('contenteditable') === 'true')) {
|
||||||
console.log(`[${this.name}] Input field is a contentEditable div.`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Input field is a contentEditable div.`);
|
||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
inputElement.innerHTML = ''; // Clear existing content
|
inputElement.innerHTML = ''; // Clear existing content
|
||||||
inputElement.textContent = text; // Set the new text content
|
inputElement.textContent = text; // Set the new text content
|
||||||
inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||||
console.log(`[${this.name}] Set text content and dispatched input event for contentEditable div.`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Set text content and dispatched input event for contentEditable div.`);
|
||||||
} else { // Standard input or textarea
|
} else { // Standard input or textarea
|
||||||
console.log(`[${this.name}] Input field is textarea/input.`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Input field is textarea/input.`);
|
||||||
inputElement.value = text;
|
inputElement.value = text;
|
||||||
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
|
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
console.log(`[${this.name}] Set value and dispatched input event for textarea/input.`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Set value and dispatched input event for textarea/input.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500)); // Preserved delay
|
await new Promise(resolve => setTimeout(resolve, 500)); // Preserved delay
|
||||||
|
|
@ -83,261 +94,355 @@ class GeminiProvider {
|
||||||
sendButton.classList.contains('disabled');
|
sendButton.classList.contains('disabled');
|
||||||
|
|
||||||
if (!isDisabled) {
|
if (!isDisabled) {
|
||||||
console.log(`[${this.name}] Clicking send button.`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Clicking send button.`);
|
||||||
sendButton.click();
|
sendButton.click();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[${this.name}] Send button is disabled. Cannot send message.`);
|
console.warn(`[${this.name}] [${this.captureMethod.toUpperCase()}] Send button is disabled. Cannot send message.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[${this.name}] Error sending message:`, error);
|
console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Error sending message:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture response from the chat interface (Original logic, logs updated for consistency)
|
// Capture response from the chat interface (Original logic, logs updated for consistency)
|
||||||
captureResponse(element) {
|
captureResponse(element) {
|
||||||
|
// This method is called when this.captureMethod === "dom"
|
||||||
|
// 'element' is expected to be the one matching this.responseSelector
|
||||||
if (!element) {
|
if (!element) {
|
||||||
console.log(`[${this.name}] No element provided to captureResponse`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] No element provided to captureResponse (expected match for ${this.responseSelector})`);
|
||||||
return { found: false, text: '' };
|
return { found: false, text: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[${this.name}] Attempting to capture response from Gemini:`, element);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Attempting to capture response from Gemini element:`, element.outerHTML.substring(0, 200) + "...");
|
||||||
|
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
let foundResponse = false;
|
let foundResponse = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[${this.name}] Looking for response in various elements...`);
|
// Primarily rely on the textContent of the matched element
|
||||||
|
|
||||||
if (element.textContent) {
|
if (element.textContent) {
|
||||||
console.log(`[${this.name}] Element has text content`);
|
|
||||||
responseText = element.textContent.trim();
|
responseText = element.textContent.trim();
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Raw textContent from element (len ${responseText.length}): "${responseText.substring(0, 100)}..."`);
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
if (responseText &&
|
if (responseText &&
|
||||||
responseText !== this.lastSentMessage &&
|
responseText !== this.lastSentMessage &&
|
||||||
!responseText.includes("Loading") &&
|
!responseText.toLowerCase().includes("loading") && // Case-insensitive for common words
|
||||||
!responseText.includes("Thinking") &&
|
!responseText.toLowerCase().includes("thinking") &&
|
||||||
!responseText.includes("You stopped this response")) {
|
!responseText.includes("You stopped this response")) {
|
||||||
console.log(`[${this.name}] Found response in element:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
|
// HTML entities like < are automatically decoded by textContent
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Valid response found:`, responseText.substring(0, 100) + (responseText.length > 100 ? "..." : ""));
|
||||||
foundResponse = true;
|
foundResponse = true;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[${this.name}] Element text appears to be invalid:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
if (responseText === this.lastSentMessage) {
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Text content matches last sent message. Not a new response.`);
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Text content appears to be invalid or loading state: "${responseText.substring(0, 100)}..."`);
|
||||||
|
}
|
||||||
|
responseText = ""; // Clear if not a valid new response
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`[${this.name}] Element has no text content`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Element has no textContent.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[${this.name}] Trying to find the most recent conversation container...`);
|
// Fallback: if textContent was empty but there are <p> tags inside the matched element.
|
||||||
|
if (!foundResponse && responseText === "" && element.querySelectorAll) {
|
||||||
const conversationContainers = document.querySelectorAll('.conversation-container');
|
const paragraphs = element.querySelectorAll('p');
|
||||||
if (conversationContainers && conversationContainers.length > 0) {
|
if (paragraphs && paragraphs.length > 0) {
|
||||||
console.log(`[${this.name}] Found ${conversationContainers.length} conversation containers`);
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] textContent was empty, trying to combine ${paragraphs.length} <p> tags.`);
|
||||||
const lastContainer = conversationContainers[conversationContainers.length - 1];
|
let combinedPText = "";
|
||||||
console.log(`[${this.name}] Last container ID:`, lastContainer.id);
|
paragraphs.forEach(p => {
|
||||||
|
combinedPText += p.textContent.trim() + "\n";
|
||||||
const userQuery = lastContainer.querySelector('.user-query-container');
|
|
||||||
const userText = userQuery ? userQuery.textContent.trim() : '';
|
|
||||||
|
|
||||||
if (userText === this.lastSentMessage) {
|
|
||||||
console.log(`[${this.name}] Found container with our last sent message, looking for response`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelResponse = lastContainer.querySelector('model-response');
|
|
||||||
if (modelResponse) {
|
|
||||||
console.log(`[${this.name}] Found model-response in last conversation container`);
|
|
||||||
const messageContent = modelResponse.querySelector('message-content.model-response-text');
|
|
||||||
if (messageContent) {
|
|
||||||
console.log(`[${this.name}] Found message-content in model-response`);
|
|
||||||
const markdownDiv = messageContent.querySelector('.markdown');
|
|
||||||
if (markdownDiv) {
|
|
||||||
console.log(`[${this.name}] Found markdown div in message-content`);
|
|
||||||
const text = markdownDiv.textContent.trim();
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
responseText = text;
|
|
||||||
console.log(`[${this.name}] Found response in markdown div:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
foundResponse = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No conversation containers found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundResponse) {
|
|
||||||
console.log(`[${this.name}] Trying to find model-response-message-content elements...`);
|
|
||||||
const responseMessages = document.querySelectorAll('div[id^="model-response-message-content"]');
|
|
||||||
if (responseMessages && responseMessages.length > 0) {
|
|
||||||
console.log(`[${this.name}] Found ${responseMessages.length} model-response-message-content elements`);
|
|
||||||
const sortedMessages = Array.from(responseMessages).sort((a, b) => {
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
});
|
|
||||||
const responseMessage = sortedMessages[sortedMessages.length - 1];
|
|
||||||
console.log(`[${this.name}] Last response message ID:`, responseMessage.id);
|
|
||||||
const text = responseMessage.textContent.trim();
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
responseText = text;
|
|
||||||
console.log(`[${this.name}] Found response in model-response-message:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
foundResponse = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No model-response-message-content elements found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundResponse) {
|
|
||||||
console.log(`[${this.name}] Trying to find message-content elements...`);
|
|
||||||
const messageContents = document.querySelectorAll('message-content.model-response-text');
|
|
||||||
if (messageContents && messageContents.length > 0) {
|
|
||||||
console.log(`[${this.name}] Found ${messageContents.length} message-content elements`);
|
|
||||||
const sortedContents = Array.from(messageContents).sort((a, b) => {
|
|
||||||
return (a.id || '').localeCompare(b.id || '');
|
|
||||||
});
|
|
||||||
const lastMessageContent = sortedContents[sortedContents.length - 1];
|
|
||||||
console.log(`[${this.name}] Last message content ID:`, lastMessageContent.id || 'no-id');
|
|
||||||
const markdownDiv = lastMessageContent.querySelector('.markdown');
|
|
||||||
if (markdownDiv) {
|
|
||||||
console.log(`[${this.name}] Found markdown div`);
|
|
||||||
const paragraphs = markdownDiv.querySelectorAll('p');
|
|
||||||
if (paragraphs && paragraphs.length > 0) {
|
|
||||||
console.log(`[${this.name}] Found ${paragraphs.length} paragraphs in markdown div`);
|
|
||||||
let combinedText = "";
|
|
||||||
paragraphs.forEach((p, index) => {
|
|
||||||
const text = p.textContent.trim();
|
|
||||||
console.log(`[${this.name}] Paragraph ${index} text:`, text.substring(0, 30) + (text.length > 30 ? "..." : ""));
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
combinedText += text + "\n";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (combinedText.trim()) {
|
responseText = combinedPText.trim();
|
||||||
responseText = combinedText.trim();
|
// Re-validate
|
||||||
console.log(`[${this.name}] Found response in paragraphs:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
if (responseText &&
|
||||||
foundResponse = true;
|
responseText !== this.lastSentMessage &&
|
||||||
|
!responseText.toLowerCase().includes("loading") &&
|
||||||
|
!responseText.toLowerCase().includes("thinking") &&
|
||||||
|
!responseText.includes("You stopped this response")) {
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] Valid response found from combined <p> tags:`, responseText.substring(0, 100) + (responseText.length > 100 ? "..." : ""));
|
||||||
|
foundResponse = true;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[${this.name}] No valid text found in paragraphs`);
|
responseText = ""; // Clear if not valid after combining paragraphs
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No paragraphs found in markdown div`);
|
|
||||||
const text = markdownDiv.textContent.trim();
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
responseText = text;
|
|
||||||
console.log(`[${this.name}] Found response in markdown div:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
foundResponse = true;
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] Markdown div text appears to be invalid:`, text.substring(0, 50) + (text.length > 50 ? "..." : ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No markdown div found in message-content`);
|
|
||||||
const text = lastMessageContent.textContent.trim();
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
responseText = text;
|
|
||||||
console.log(`[${this.name}] Found response in message-content:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
foundResponse = true;
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] Message-content text appears to be invalid:`, text.substring(0, 50) + (text.length > 50 ? "..." : ""));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No message-content elements found`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundResponse) {
|
|
||||||
console.log(`[${this.name}] Trying to find paragraphs in the document...`);
|
|
||||||
const paragraphs = document.querySelectorAll('p');
|
|
||||||
if (paragraphs && paragraphs.length > 0) {
|
|
||||||
console.log(`[${this.name}] Found ${paragraphs.length} paragraphs`);
|
|
||||||
let combinedText = "";
|
|
||||||
for (let i = paragraphs.length - 1; i >= 0; i--) {
|
|
||||||
const paragraph = paragraphs[i];
|
|
||||||
const text = paragraph.textContent.trim();
|
|
||||||
const isUserQuery = paragraph.closest('.user-query-container, .user-query-bubble-container');
|
|
||||||
if (isUserQuery) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (text &&
|
|
||||||
text !== this.lastSentMessage &&
|
|
||||||
!text.includes("Loading") &&
|
|
||||||
!text.includes("Thinking") &&
|
|
||||||
!text.includes("You stopped this response")) {
|
|
||||||
combinedText = text + "\n" + combinedText;
|
|
||||||
if (text.startsWith("Hello") || text.includes("I'm doing") || text.includes("How can I assist")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (combinedText.trim()) {
|
|
||||||
responseText = combinedText.trim();
|
|
||||||
console.log(`[${this.name}] Found response in paragraphs:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
foundResponse = true;
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No valid text found in paragraphs`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[${this.name}] No paragraphs found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundResponse) {
|
|
||||||
console.log(`[${this.name}] Response not found yet, will try again in the next polling cycle`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[${this.name}] Error capturing response from Gemini:`, error);
|
console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Error capturing response from Gemini:`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final cleanup (mostly for newlines)
|
||||||
if (foundResponse && responseText) {
|
if (foundResponse && responseText) {
|
||||||
console.log(`[${this.name}] Cleaning up response text...`);
|
responseText = responseText.replace(/\n{3,}/g, '\n\n').trim();
|
||||||
responseText = responseText.trim()
|
|
||||||
.replace(/^(Loading|Thinking).*/gim, '')
|
|
||||||
.replace(/You stopped this response.*/gim, '')
|
|
||||||
.replace(/\n{3,}/g, '\n\n')
|
|
||||||
.trim();
|
|
||||||
console.log(`[${this.name}] Cleaned response text:`, responseText.substring(0, 50) + (responseText.length > 50 ? "..." : ""));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
found: foundResponse && !!responseText.trim(),
|
found: foundResponse && !!responseText.trim(),
|
||||||
text: responseText
|
text: responseText
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// (NEW) Method for streaming API patterns
|
// --- START: Methods for Debugger-based Response Capture ---
|
||||||
getStreamingApiPatterns() {
|
|
||||||
console.log(`[${this.name}] getStreamingApiPatterns called`);
|
initiateResponseCapture(requestId, responseCallback) {
|
||||||
// TODO: DEVELOPER ACTION REQUIRED!
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] initiateResponseCapture called for requestId: ${requestId}. Current provider captureMethod: ${this.captureMethod}`);
|
||||||
// Use browser Network DevTools on gemini.google.com to identify the
|
this.pendingResponseCallbacks.set(requestId, responseCallback);
|
||||||
// exact URL(s) that deliver the AI's streaming response when a prompt is sent.
|
if (this.captureMethod === "debugger") {
|
||||||
// Replace the placeholder pattern below with the correct one(s).
|
// Ensure accumulator is ready for this request
|
||||||
// Example: return [{ urlPattern: "*://gemini.google.com/api/generate*", requestStage: "Response" }];
|
if (!this.requestAccumulators.has(requestId)) {
|
||||||
return [
|
this.requestAccumulators.set(requestId, { text: "", isDefinitelyFinal: false });
|
||||||
{ urlPattern: "*://gemini.google.com/api/stream/generateContent*", requestStage: "Response" } // Placeholder - VERIFY THIS!
|
}
|
||||||
];
|
console.log(`[${this.name}] [DEBUGGER] Debugger capture selected. Callback stored for requestId: ${requestId}. Ensure background script is set up for '${this.debuggerUrlPattern}'.`);
|
||||||
|
} else if (this.captureMethod === "dom") {
|
||||||
|
// For DOM method, no specific initiation is needed here as polling is handled by content.js
|
||||||
|
console.log(`[${this.name}] [DOM] DOM capture selected. Response will be handled by polling in content.js using captureResponse.`);
|
||||||
|
} else {
|
||||||
|
console.error(`[${this.name}] [${this.captureMethod.toUpperCase()}] Unknown capture method: ${this.captureMethod}`);
|
||||||
|
responseCallback(requestId, `[Error: Unknown capture method '${this.captureMethod}' in provider]`, true);
|
||||||
|
this.pendingResponseCallbacks.delete(requestId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (NEW) Optional Fallback Methods
|
handleDebuggerData(requestId, rawData, isFinalFromBackground) {
|
||||||
|
// This method is only relevant if this.captureMethod === "debugger"
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData ENTER - requestId: ${requestId}, isFinalFromBackground: ${isFinalFromBackground}, rawData: "${rawData ? rawData.substring(0,100) + "..." : "null/empty"}"`);
|
||||||
|
const callback = this.pendingResponseCallbacks.get(requestId);
|
||||||
|
if (!callback) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] handleDebuggerData - No callback for requestId: ${requestId}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let accumulator = this.requestAccumulators.get(requestId);
|
||||||
|
if (!accumulator) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] handleDebuggerData - No accumulator for requestId: ${requestId}. Initializing.`);
|
||||||
|
accumulator = { text: "", isDefinitelyFinal: false };
|
||||||
|
this.requestAccumulators.set(requestId, accumulator);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulator.isDefinitelyFinal) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - Accumulator for ${requestId} is already final. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData && rawData.trim() !== "") {
|
||||||
|
const parseOutput = this.parseDebuggerResponse(rawData); // Gemini-specific parsing
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - requestId: ${requestId}, parseOutput: ${JSON.stringify(parseOutput)}`);
|
||||||
|
|
||||||
|
if (parseOutput.text !== null) { // Allow empty string if it's a valid part of the response
|
||||||
|
accumulator.text = parseOutput.text; // parseOutput.text is the total text from rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseOutput.isFinalResponse) { // If the parser itself detected a definitive end
|
||||||
|
accumulator.isDefinitelyFinal = true;
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - ${requestId} marked as definitelyFinal by parseOutput.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFinalForCallback = accumulator.isDefinitelyFinal || isFinalFromBackground;
|
||||||
|
|
||||||
|
if (accumulator.text || isFinalForCallback) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - INVOKING CALLBACK for ${requestId}. Text length: ${accumulator.text.length}, isFinal: ${isFinalForCallback}`);
|
||||||
|
callback(requestId, accumulator.text, isFinalForCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFinalForCallback) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] handleDebuggerData - CLEANING UP for ${requestId} (isDefinitelyFinal: ${accumulator.isDefinitelyFinal}, isFinalFromBackground: ${isFinalFromBackground}).`);
|
||||||
|
this.pendingResponseCallbacks.delete(requestId);
|
||||||
|
this.requestAccumulators.delete(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: DEVELOPER ACTION REQUIRED!
|
||||||
|
// This is a placeholder parser. You MUST inspect Gemini's actual SSE
|
||||||
|
// stream format and update this parser accordingly.
|
||||||
|
parseDebuggerResponse(rawDataString) {
|
||||||
|
let accumulatedTextFromThisCall = "";
|
||||||
|
let isStreamDefinitelyFinished = false;
|
||||||
|
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse INPUT rawDataString (len ${rawDataString.length}): ${rawDataString.substring(0, 300)}`);
|
||||||
|
|
||||||
|
if (!rawDataString) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse: Empty rawDataString received.`);
|
||||||
|
return { text: "", isFinalResponse: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanData = rawDataString;
|
||||||
|
if (cleanData.startsWith(")]}'")) {
|
||||||
|
const firstNewlineIndex = cleanData.indexOf('\n');
|
||||||
|
if (firstNewlineIndex !== -1) {
|
||||||
|
cleanData = cleanData.substring(firstNewlineIndex + 1);
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse: rawDataString starts with )]}' but no newline found.`);
|
||||||
|
cleanData = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanData = cleanData.trimStart();
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse cleanData after prefix strip (len ${cleanData.length}): ${cleanData.substring(0, 300)}`);
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
let currentIndex = 0;
|
||||||
|
while (currentIndex < cleanData.length) {
|
||||||
|
// Skip leading whitespace/newlines to find the start of a potential length line
|
||||||
|
while (currentIndex < cleanData.length && /\s/.test(cleanData.charAt(currentIndex))) {
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
if (currentIndex >= cleanData.length) { // Reached end after skipping whitespace
|
||||||
|
// console.log(`[${this.name}] Reached end of data after skipping initial whitespace.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextNewline = cleanData.indexOf('\n', currentIndex);
|
||||||
|
if (nextNewline === -1) {
|
||||||
|
// Last line processing (potential trailing length or garbage)
|
||||||
|
const remainingStr = cleanData.substring(currentIndex).trim();
|
||||||
|
if (/^\d+$/.test(remainingStr) && chunks.length > 0) {
|
||||||
|
// console.log(`[${this.name}] [DEBUGGER] Trailing number found, likely length for a future (missing) chunk: ${remainingStr}`);
|
||||||
|
} else if (remainingStr.length > 0 && chunks.length > 0) { // If there's non-numeric trailing data and we have prior chunks, it might be an error or unparseable.
|
||||||
|
// console.warn(`[${this.name}] [DEBUGGER] Trailing non-numeric, non-empty data found: ${remainingStr.substring(0,100)}`);
|
||||||
|
} else if (remainingStr.length > 0 && chunks.length === 0) { // If it's the *only* data and not a number.
|
||||||
|
// console.warn(`[${this.name}] [DEBUGGER] Single line of non-numeric, non-empty data found, cannot parse as chunk: ${remainingStr.substring(0,100)}`);
|
||||||
|
}
|
||||||
|
break; // End of data or unparseable trailing data
|
||||||
|
}
|
||||||
|
|
||||||
|
const lengthLineContent = cleanData.substring(currentIndex, nextNewline).trim();
|
||||||
|
let length = NaN;
|
||||||
|
|
||||||
|
if (/^\d+$/.test(lengthLineContent)) { // Check if the line *only* contains digits
|
||||||
|
length = parseInt(lengthLineContent, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(`[${this.name}] [DEBUGGER] Chunk parsing: Potential lengthLineContent="${lengthLineContent}", parsed length=${length}`);
|
||||||
|
|
||||||
|
if (isNaN(length) || length < 0) { // This will catch non-numeric lines or negative/invalid lengths
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Invalid, non-positive, or non-numeric length line: "${lengthLineContent}". CurrentIndex: ${currentIndex}. Skipping to next line.`);
|
||||||
|
currentIndex = nextNewline + 1; // Advance past the problematic line
|
||||||
|
continue; // Restart loop to find next potential length line from the new currentIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonStringStart = nextNewline + 1;
|
||||||
|
const jsonStringEnd = jsonStringStart + length;
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
// console.log(`[${this.name}] [DEBUGGER] Encountered 0-length chunk at currentIndex ${currentIndex}. Advancing to after its conceptual position.`);
|
||||||
|
currentIndex = jsonStringEnd; // This is effectively nextNewline + 1, the start of where the empty JSON "was"
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonStringEnd > cleanData.length) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Declared length ${length} (from line "${lengthLineContent}") exceeds available data. cleanData.length: ${cleanData.length}, required end: ${jsonStringEnd}. Discarding this length and attempting to resynchronize.`);
|
||||||
|
currentIndex = nextNewline + 1; // Skip the problematic length line
|
||||||
|
continue; // Try to find the next valid length line
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonString = cleanData.substring(jsonStringStart, jsonStringEnd);
|
||||||
|
// console.log(`[${this.name}] [DEBUGGER] Extracted jsonString (len ${jsonString.length}, expected ${length}): ${jsonString.substring(0,100)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(jsonString);
|
||||||
|
chunks.push(jsonString);
|
||||||
|
currentIndex = jsonStringEnd;
|
||||||
|
} catch (parseError) {
|
||||||
|
const errorMsg = parseError.message || "";
|
||||||
|
const nonWhitespaceMatch = errorMsg.match(/Unexpected non-whitespace character after JSON at position (\d+)/);
|
||||||
|
|
||||||
|
if (nonWhitespaceMatch && nonWhitespaceMatch[1]) {
|
||||||
|
const actualEndPositionInJsonString = parseInt(nonWhitespaceMatch[1], 10);
|
||||||
|
const validJsonSubstring = jsonString.substring(0, actualEndPositionInJsonString);
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSON.parse(validJsonSubstring);
|
||||||
|
chunks.push(validJsonSubstring);
|
||||||
|
currentIndex = jsonStringStart + actualEndPositionInJsonString;
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Corrected oversized JSON chunk. Original length ${length} (from line "${lengthLineContent}") was too long. Used shorter valid part of length ${actualEndPositionInJsonString}. New currentIndex: ${currentIndex}. Original error: ${parseError.message}`);
|
||||||
|
} catch (innerParseError) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Failed to parse even the corrected shorter JSON substring (original length ${length} from line "${lengthLineContent}", attempted correction to ${actualEndPositionInJsonString}). Inner Error: ${innerParseError.message}. JSON hint for corrected: "${validJsonSubstring.substring(0,100)}...". Skipping original length declaration.`);
|
||||||
|
currentIndex = nextNewline + 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Failed to parse JSON chunk (length ${length} from line "${lengthLineContent}", Error: ${parseError.message}). This suggests the reported length was incorrect or JSON malformed. JSON hint: "${jsonString.substring(0, 100)}...". Skipping this length declaration and attempting to resynchronize.`);
|
||||||
|
currentIndex = nextNewline + 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rawChunkJson of chunks) {
|
||||||
|
try {
|
||||||
|
const chunkJsonToParse = rawChunkJson.trim();
|
||||||
|
if (!chunkJsonToParse) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const outerArray = JSON.parse(chunkJsonToParse);
|
||||||
|
if (!Array.isArray(outerArray)) continue;
|
||||||
|
|
||||||
|
for (const item of outerArray) {
|
||||||
|
if (Array.isArray(item) && item.length > 0) {
|
||||||
|
if (item[0] === "wrb.fr" && item.length >= 3 && typeof item[2] === 'string') {
|
||||||
|
const nestedJsonString = item[2];
|
||||||
|
try {
|
||||||
|
const trimmedNestedJsonString = nestedJsonString.trim();
|
||||||
|
if (!trimmedNestedJsonString) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const innerData = JSON.parse(trimmedNestedJsonString);
|
||||||
|
if (Array.isArray(innerData) && innerData.length > 4 && Array.isArray(innerData[4])) {
|
||||||
|
for (const contentBlock of innerData[4]) {
|
||||||
|
if (Array.isArray(contentBlock) && contentBlock.length >= 2 && typeof contentBlock[0] === 'string' && contentBlock[0].startsWith("rc_") && Array.isArray(contentBlock[1])) {
|
||||||
|
const newText = contentBlock[1].join("");
|
||||||
|
if (newText) {
|
||||||
|
accumulatedTextFromThisCall += newText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Failed to parse nested JSON: "${nestedJsonString.substring(0,100)}...". Error: ${e.message}`);
|
||||||
|
}
|
||||||
|
} else if (item[0] === "e" && item.length >= 1) {
|
||||||
|
isStreamDefinitelyFinished = true;
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] End-of-stream marker 'e' detected in chunk: ${JSON.stringify(item)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[${this.name}] [DEBUGGER] Failed to parse chunk JSON: "${chunkJsonToParse.substring(0,100)}...". Error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] parseDebuggerResponse FINAL output - text length: ${accumulatedTextFromThisCall.length}, accumulatedText: "${accumulatedTextFromThisCall.substring(0,200)}", isFinalByParser: ${isStreamDefinitelyFinished}`);
|
||||||
|
|
||||||
|
if (accumulatedTextFromThisCall.includes("\\<") || accumulatedTextFromThisCall.includes("\\>")) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] Unescaping escaped tool call brackets in final text.`);
|
||||||
|
accumulatedTextFromThisCall = accumulatedTextFromThisCall.replace(/\\</g, "<").replace(/\\>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulatedTextFromThisCall.includes("\\_")) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] Unescaping escaped underscores in final text.`);
|
||||||
|
accumulatedTextFromThisCall = accumulatedTextFromThisCall.replace(/\\_/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: accumulatedTextFromThisCall, isFinalResponse: isStreamDefinitelyFinished };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- END: Methods for Debugger-based Response Capture ---
|
||||||
|
|
||||||
|
getStreamingApiPatterns() {
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] getStreamingApiPatterns called. Current provider captureMethod: ${this.captureMethod}`);
|
||||||
|
if (this.captureMethod === "debugger" && this.debuggerUrlPattern) {
|
||||||
|
console.log(`[${this.name}] [DEBUGGER] Using debugger URL pattern: ${this.debuggerUrlPattern}`);
|
||||||
|
return [{ urlPattern: this.debuggerUrlPattern, requestStage: "Response" }];
|
||||||
|
}
|
||||||
|
console.log(`[${this.name}] [${this.captureMethod.toUpperCase()}] No debugger patterns to return (captureMethod is '${this.captureMethod}' or no pattern set).`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// (NEW) Optional Fallback Methods - these are largely placeholders if debugger is primary
|
||||||
async captureResponseDOMFallback() {
|
async captureResponseDOMFallback() {
|
||||||
console.log(`[${this.name}] captureResponseDOMFallback called. Implement DOM observation logic here if needed as a fallback.`);
|
console.log(`[${this.name}] captureResponseDOMFallback called. Implement DOM observation logic here if needed as a fallback.`);
|
||||||
// TODO: Implement or verify existing DOM fallback logic for Gemini if it's to be kept.
|
// TODO: Implement or verify existing DOM fallback logic for Gemini if it's to be kept.
|
||||||
|
|
@ -424,10 +529,11 @@ class GeminiProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should skip response monitoring (Original - UNCHANGED)
|
// Check if we should skip response monitoring
|
||||||
shouldSkipResponseMonitoring() {
|
shouldSkipResponseMonitoring() {
|
||||||
// We want to monitor for responses now that we've fixed the response capturing
|
// If using debugger, we skip DOM-based monitoring.
|
||||||
return false;
|
// console.log(`[${this.name}] shouldSkipResponseMonitoring called. Capture method: ${this.captureMethod}`);
|
||||||
|
return this.captureMethod === "debugger";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue