refactor: memory cleanup and parser hardening
This commit is contained in:
+48
-26
@@ -1,5 +1,5 @@
|
|||||||
import json, http.client, ssl, os, logging, base64
|
import json, http.client, ssl, os, logging, base64, secrets, time
|
||||||
from flask import Response, request
|
from flask import Response, request, abort
|
||||||
from searx.plugins import Plugin, PluginInfo
|
from searx.plugins import Plugin, PluginInfo
|
||||||
from searx.result_types import EngineResults
|
from searx.result_types import EngineResults
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
@@ -24,11 +24,22 @@ class SXNGPlugin(Plugin):
|
|||||||
self.max_tokens = int(os.getenv('GEMINI_MAX_TOKENS', 500))
|
self.max_tokens = int(os.getenv('GEMINI_MAX_TOKENS', 500))
|
||||||
self.temperature = float(os.getenv('GEMINI_TEMPERATURE', 0.2))
|
self.temperature = float(os.getenv('GEMINI_TEMPERATURE', 0.2))
|
||||||
self.base_url = os.getenv('OPENROUTER_BASE_URL', 'openrouter.ai')
|
self.base_url = os.getenv('OPENROUTER_BASE_URL', 'openrouter.ai')
|
||||||
|
self.valid_tokens = {}
|
||||||
|
|
||||||
def init(self, app):
|
def init(self, app):
|
||||||
@app.route('/gemini-stream', methods=['POST'])
|
@app.route('/gemini-stream', methods=['POST'])
|
||||||
def g_stream():
|
def g_stream():
|
||||||
data = request.json or {}
|
data = request.json or {}
|
||||||
|
token = data.get('tk', '')
|
||||||
|
|
||||||
|
# Maintenance: Token validation & cleanup
|
||||||
|
now = time.time()
|
||||||
|
self.valid_tokens = {k: v for k, v in self.valid_tokens.items() if v > now}
|
||||||
|
|
||||||
|
if token not in self.valid_tokens:
|
||||||
|
abort(403)
|
||||||
|
del self.valid_tokens[token]
|
||||||
|
|
||||||
context_text = data.get('context', '')
|
context_text = data.get('context', '')
|
||||||
q = data.get('q', '')
|
q = data.get('q', '')
|
||||||
|
|
||||||
@@ -61,7 +72,6 @@ class SXNGPlugin(Plugin):
|
|||||||
chunk = res.read(128)
|
chunk = res.read(128)
|
||||||
if not chunk: break
|
if not chunk: break
|
||||||
buffer += chunk.decode('utf-8')
|
buffer += chunk.decode('utf-8')
|
||||||
|
|
||||||
while buffer:
|
while buffer:
|
||||||
buffer = buffer.lstrip()
|
buffer = buffer.lstrip()
|
||||||
if not buffer: break
|
if not buffer: break
|
||||||
@@ -99,6 +109,7 @@ class SXNGPlugin(Plugin):
|
|||||||
res = conn.getresponse()
|
res = conn.getresponse()
|
||||||
if res.status != 200: return
|
if res.status != 200: return
|
||||||
|
|
||||||
|
decoder = json.JSONDecoder()
|
||||||
buffer = ""
|
buffer = ""
|
||||||
while True:
|
while True:
|
||||||
chunk = res.read(128)
|
chunk = res.read(128)
|
||||||
@@ -106,13 +117,12 @@ class SXNGPlugin(Plugin):
|
|||||||
buffer += chunk.decode('utf-8')
|
buffer += chunk.decode('utf-8')
|
||||||
while "\n" in buffer:
|
while "\n" in buffer:
|
||||||
line, buffer = buffer.split("\n", 1)
|
line, buffer = buffer.split("\n", 1)
|
||||||
line = line.strip()
|
|
||||||
if line.startswith("data: "):
|
if line.startswith("data: "):
|
||||||
data_str = line[6:].strip()
|
data_str = line[6:].strip()
|
||||||
if data_str == "[DONE]": return
|
if data_str == "[DONE]": return
|
||||||
try:
|
try:
|
||||||
data_json = json.loads(data_str)
|
obj, _ = decoder.raw_decode(data_str)
|
||||||
content = data_json.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
content = obj.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
||||||
if content: yield content
|
if content: yield content
|
||||||
except: pass
|
except: pass
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -131,61 +141,73 @@ class SXNGPlugin(Plugin):
|
|||||||
context_list = [f"[{i+1}] {r.get('title')}: {r.get('content')}" for i, r in enumerate(raw_results[:6])]
|
context_list = [f"[{i+1}] {r.get('title')}: {r.get('content')}" for i, r in enumerate(raw_results[:6])]
|
||||||
context_str = "\n".join(context_list)
|
context_str = "\n".join(context_list)
|
||||||
|
|
||||||
|
# Handshake token
|
||||||
|
tk = secrets.token_hex(16)
|
||||||
|
self.valid_tokens[tk] = time.time() + 60
|
||||||
|
|
||||||
b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8')
|
b64_context = base64.b64encode(context_str.encode('utf-8')).decode('utf-8')
|
||||||
js_q = json.dumps(search.search_query.query)
|
js_q = json.dumps(search.search_query.query)
|
||||||
|
|
||||||
html_payload = f'''
|
html_payload = f'''
|
||||||
<style>
|
<style>
|
||||||
#sxng-stream-box {{
|
@keyframes sxng-blink {{ 0%, 100% {{ opacity: 1; }} 50% {{ opacity: 0; }} }}
|
||||||
display: none !important;
|
.sxng-cursor {{
|
||||||
padding: 1rem !important;
|
display: inline-block; width: 0.5rem; height: 1rem;
|
||||||
margin-top: 0 !important;
|
background: var(--color-result-description);
|
||||||
margin-bottom: 1.5rem !important;
|
margin-left: 2px; vertical-align: middle;
|
||||||
border-bottom: 1px solid var(--color-result-border);
|
animation: sxng-blink 1s step-end infinite;
|
||||||
}}
|
|
||||||
#sxng-stream-data {{
|
|
||||||
margin: 0 !important;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--color-result-description);
|
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
<article id="sxng-stream-box" class="answer">
|
<article id="sxng-stream-box" class="answer" style="display:none; margin-bottom: 1rem;">
|
||||||
<p id="sxng-stream-data" style="white-space: pre-wrap;"></p>
|
<p id="sxng-stream-data" style="white-space: pre-wrap; color: var(--color-result-description); font-size: 0.95rem;">
|
||||||
|
<i id="sxng-loading">Thinking...</i>
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
<script>
|
<script>
|
||||||
(async () => {{
|
(async () => {{
|
||||||
const q = {js_q};
|
const q = {js_q};
|
||||||
const b64 = "{b64_context}";
|
const b64 = "{b64_context}";
|
||||||
|
const tk = "{tk}";
|
||||||
const shell = document.getElementById('sxng-stream-box');
|
const shell = document.getElementById('sxng-stream-box');
|
||||||
const data = document.getElementById('sxng-stream-data');
|
const data = document.getElementById('sxng-stream-data');
|
||||||
|
const loading = document.getElementById('sxng-loading');
|
||||||
|
|
||||||
|
const container = document.getElementById('urls') || document.getElementById('main_results');
|
||||||
|
if (container && shell) {{ container.prepend(shell); shell.style.display = 'block'; }}
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const ctx = new TextDecoder().decode(Uint8Array.from(atob(b64), c => c.charCodeAt(0)));
|
const ctx = new TextDecoder().decode(Uint8Array.from(atob(b64), c => c.charCodeAt(0)));
|
||||||
const res = await fetch('/gemini-stream', {{
|
const res = await fetch('/gemini-stream', {{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {{ 'Content-Type': 'application/json' }},
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
body: JSON.stringify({{ q: q, context: ctx }})
|
body: JSON.stringify({{ q: q, context: ctx, tk: tk }})
|
||||||
}});
|
}});
|
||||||
|
|
||||||
if (!res.ok) return;
|
if (!res.ok) {{ shell.style.display = 'none'; return; }}
|
||||||
|
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
const cursor = document.createElement('span');
|
||||||
|
cursor.className = 'sxng-cursor';
|
||||||
|
|
||||||
|
let started = false;
|
||||||
while (true) {{
|
while (true) {{
|
||||||
const {{done, value}} = await reader.read();
|
const {{done, value}} = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
|
||||||
const chunk = decoder.decode(value);
|
const chunk = decoder.decode(value);
|
||||||
if (chunk) {{
|
if (chunk) {{
|
||||||
if (shell.style.getPropertyValue('display') !== 'block') {{
|
if (!started) {{
|
||||||
shell.style.setProperty('display', 'block', 'important');
|
data.innerHTML = "";
|
||||||
|
data.appendChild(cursor);
|
||||||
|
started = true;
|
||||||
}}
|
}}
|
||||||
data.innerText += chunk;
|
cursor.before(chunk);
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
}} catch (e) {{ console.error(e); }}
|
cursor.remove();
|
||||||
|
if (!started) shell.style.display = 'none';
|
||||||
|
}} catch (e) {{ console.error(e); shell.style.display = 'none'; }}
|
||||||
}})();
|
}})();
|
||||||
</script>
|
</script>
|
||||||
'''
|
'''
|
||||||
|
|||||||
+7
-1
@@ -114,6 +114,11 @@ class PluginTestCase(unittest.TestCase):
|
|||||||
self.assertIn('/gemini-stream', content)
|
self.assertIn('/gemini-stream', content)
|
||||||
|
|
||||||
def test_stream_endpoint(self):
|
def test_stream_endpoint(self):
|
||||||
|
# Trigger index to generate a token in the plugin instance
|
||||||
|
self.app.get('/')
|
||||||
|
# Extract the last generated token
|
||||||
|
token = list(plugin.valid_tokens.keys())[-1]
|
||||||
|
|
||||||
# Check for the appropriate key based on provider
|
# Check for the appropriate key based on provider
|
||||||
key = os.getenv("OPENROUTER_API_KEY") if plugin.provider == 'openrouter' else os.getenv("GEMINI_API_KEY")
|
key = os.getenv("OPENROUTER_API_KEY") if plugin.provider == 'openrouter' else os.getenv("GEMINI_API_KEY")
|
||||||
if not key:
|
if not key:
|
||||||
@@ -121,7 +126,8 @@ class PluginTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"q": "why is the sky blue",
|
"q": "why is the sky blue",
|
||||||
"context": "The sky is blue because of Rayleigh scattering."
|
"context": "The sky is blue because of Rayleigh scattering.",
|
||||||
|
"tk": token
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.app.post('/gemini-stream', json=payload)
|
response = self.app.post('/gemini-stream', json=payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user