172 lines
6.4 KiB
JavaScript
172 lines
6.4 KiB
JavaScript
// ── Side panel ───────────────────────────────────────────────────────────────
|
|
|
|
if (chrome.sidePanel) {
|
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
|
}
|
|
chrome.action.onClicked.addListener((tab) => {
|
|
if (chrome.sidePanel && tab.id) chrome.sidePanel.open({ tabId: tab.id }).catch(() => {});
|
|
});
|
|
|
|
// ── Active stream registry (for cancellation) ────────────────────────────────
|
|
|
|
const activeStreams = new Map(); // requestId → AbortController
|
|
|
|
// ── Message router ────────────────────────────────────────────────────────────
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.action === 'OLLAMA_GET') {
|
|
handleGet(message, sendResponse);
|
|
return true;
|
|
}
|
|
if (message.action === 'OLLAMA_FETCH') {
|
|
handleStream(message);
|
|
sendResponse({ started: true });
|
|
return false;
|
|
}
|
|
if (message.action === 'CANCEL_STREAM') {
|
|
const ctrl = activeStreams.get(message.requestId);
|
|
if (ctrl) { ctrl.abort(); activeStreams.delete(message.requestId); }
|
|
sendResponse({ ok: true });
|
|
return false;
|
|
}
|
|
if (message.action === 'GET_PAGE_CONTENT') {
|
|
handleGetPageContent(message.tabId, sendResponse);
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
function broadcast(msg) {
|
|
chrome.runtime.sendMessage(msg).catch(() => {});
|
|
}
|
|
|
|
// ── Page content relay ────────────────────────────────────────────────────────
|
|
|
|
function handleGetPageContent(tabId, sendResponse) {
|
|
chrome.tabs.sendMessage(tabId, { type: 'GET_PAGE_CONTENT' }, (response) => {
|
|
if (chrome.runtime.lastError) {
|
|
sendResponse({ error: chrome.runtime.lastError.message });
|
|
return;
|
|
}
|
|
sendResponse(response || { error: 'Content script did not respond' });
|
|
});
|
|
}
|
|
|
|
// ── Non-streaming GET ─────────────────────────────────────────────────────────
|
|
|
|
async function handleGet(message, sendResponse) {
|
|
try {
|
|
const res = await fetch(message.url, { method: 'GET', headers: message.headers || {} });
|
|
if (!res.ok) { sendResponse({ ok: false, status: res.status, error: 'HTTP ' + res.status }); return; }
|
|
const data = await res.json();
|
|
sendResponse({ ok: true, data });
|
|
} catch (e) {
|
|
sendResponse({ ok: false, error: e.message });
|
|
}
|
|
}
|
|
|
|
// ── Streaming POST ────────────────────────────────────────────────────────────
|
|
// Requests go through the background service worker so they carry no page
|
|
// Origin header — avoiding 403s from reverse proxies that block extension origins.
|
|
|
|
async function handleStream(message) {
|
|
const { url, headers, body, requestId } = message;
|
|
const controller = new AbortController();
|
|
activeStreams.set(requestId, controller);
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: headers || {},
|
|
body,
|
|
signal: controller.signal
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let errBody = '';
|
|
try { errBody = await res.text(); } catch (_) {}
|
|
broadcast({ action: 'STREAM_ERROR', requestId,
|
|
error: 'HTTP ' + res.status + (errBody ? ': ' + errBody.slice(0, 200) : '') });
|
|
return;
|
|
}
|
|
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buf = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buf += decoder.decode(value, { stream: true });
|
|
const lines = buf.split('\n');
|
|
buf = lines.pop() || '';
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
let chunk;
|
|
try { chunk = JSON.parse(trimmed); } catch (_) { continue; }
|
|
|
|
if (chunk.error) {
|
|
broadcast({ action: 'STREAM_ERROR', requestId, error: chunk.error });
|
|
return;
|
|
}
|
|
|
|
if (chunk.message) {
|
|
if (chunk.message.thinking) {
|
|
broadcast({ action: 'STREAM_THINKING', requestId, text: chunk.message.thinking });
|
|
}
|
|
if (chunk.message.content) {
|
|
broadcast({ action: 'STREAM_CHUNK', requestId, text: chunk.message.content });
|
|
}
|
|
}
|
|
|
|
if (chunk.done) {
|
|
if (chunk.eval_count || chunk.total_duration) {
|
|
broadcast({
|
|
action: 'STREAM_METRICS', requestId,
|
|
metrics: {
|
|
eval_count: chunk.eval_count || 0,
|
|
eval_duration: chunk.eval_duration || 0,
|
|
prompt_eval_count: chunk.prompt_eval_count || 0,
|
|
total_duration: chunk.total_duration || 0
|
|
}
|
|
});
|
|
}
|
|
broadcast({ action: 'STREAM_DONE', requestId });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drain remaining buffer
|
|
if (buf.trim()) {
|
|
try {
|
|
const last = JSON.parse(buf.trim());
|
|
if (last.message && last.message.content) {
|
|
broadcast({ action: 'STREAM_CHUNK', requestId, text: last.message.content });
|
|
}
|
|
if (last.error) { broadcast({ action: 'STREAM_ERROR', requestId, error: last.error }); return; }
|
|
if (last.done && (last.eval_count || last.total_duration)) {
|
|
broadcast({ action: 'STREAM_METRICS', requestId,
|
|
metrics: { eval_count: last.eval_count || 0, eval_duration: last.eval_duration || 0,
|
|
prompt_eval_count: last.prompt_eval_count || 0, total_duration: last.total_duration || 0 } });
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
broadcast({ action: 'STREAM_DONE', requestId });
|
|
|
|
} catch (e) {
|
|
if (e.name === 'AbortError') {
|
|
broadcast({ action: 'STREAM_DONE', requestId, aborted: true });
|
|
} else {
|
|
broadcast({ action: 'STREAM_ERROR', requestId, error: e.message });
|
|
}
|
|
} finally {
|
|
activeStreams.delete(requestId);
|
|
}
|
|
}
|