mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 08:27:55 +00:00
This build was committed by a bot.
This commit is contained in:
200
docs/sw.js
200
docs/sw.js
@@ -1,94 +1,140 @@
|
|||||||
const CH = "chat-stream-v1";
|
self.importScripts("https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js");
|
||||||
let bc, streams = /* @__PURE__ */ new Map();
|
localforage.config({ name: "localforage", storeName: "keyvaluepairs" });
|
||||||
self.addEventListener("install", (e) => self.skipWaiting());
|
const TKEY = "threads_v1";
|
||||||
self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
|
const now = () => Date.now();
|
||||||
function send(m) {
|
const gid = () => Math.random().toString(36).slice(2, 9);
|
||||||
try {
|
const titleFrom = (t) => String(t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
|
||||||
bc || (bc = new BroadcastChannel(CH));
|
async function loadThreads() {
|
||||||
bc.postMessage(m);
|
const v = await localforage.getItem(TKEY);
|
||||||
} catch {
|
return Array.isArray(v) ? v : [];
|
||||||
|
}
|
||||||
|
async function saveThreads(v) {
|
||||||
|
await localforage.setItem(TKEY, v);
|
||||||
|
}
|
||||||
|
async function pickLatestThread(threads) {
|
||||||
|
if (!threads.length) return null;
|
||||||
|
let best = threads[0], bu = +best.updatedAt || 0;
|
||||||
|
for (const t of threads) {
|
||||||
|
const u = +t.updatedAt || 0;
|
||||||
|
if (u > bu) {
|
||||||
|
best = t;
|
||||||
|
bu = u;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hint(x) {
|
return best;
|
||||||
x = String(x || "");
|
}
|
||||||
if (/401|unauthorized/i.test(x)) return "Unauthorized (check API key).";
|
async function ensureThreadForWrite(reqMeta) {
|
||||||
if (/429|rate/i.test(x)) return "Rate limited (slow down or upgrade).";
|
let threads = await loadThreads();
|
||||||
if (/403|forbidden|access/i.test(x)) return "Forbidden (model or key scope).";
|
let th = await pickLatestThread(threads);
|
||||||
return "Request failed.";
|
if (!th) {
|
||||||
|
const id = gid();
|
||||||
|
const firstUser = (reqMeta.messages || []).find((m) => m && m.role === "user")?.content || "";
|
||||||
|
th = { id, title: titleFrom(firstUser), pinned: false, updatedAt: now(), messages: [] };
|
||||||
|
threads.unshift(th);
|
||||||
|
await saveThreads(threads);
|
||||||
|
}
|
||||||
|
return th.id;
|
||||||
|
}
|
||||||
|
async function appendAssistantPlaceholder(threadId, meta) {
|
||||||
|
let threads = await loadThreads();
|
||||||
|
let th = threads.find((t) => t.id === threadId);
|
||||||
|
if (!th) {
|
||||||
|
th = { id: threadId, title: "Untitled", pinned: false, updatedAt: now(), messages: [] };
|
||||||
|
threads.unshift(th);
|
||||||
|
}
|
||||||
|
const mid = "sw_" + gid();
|
||||||
|
const msg = { id: mid, role: "assistant", content: "", sune_name: "", model: meta?.model || "", avatar: "", sw: true };
|
||||||
|
th.messages = [...Array.isArray(th.messages) ? th.messages : [], msg];
|
||||||
|
th.updatedAt = now();
|
||||||
|
await saveThreads(threads);
|
||||||
|
return mid;
|
||||||
|
}
|
||||||
|
async function updateAssistantContent(threadId, mid, content) {
|
||||||
|
let threads = await loadThreads();
|
||||||
|
const th = threads.find((t) => t.id === threadId);
|
||||||
|
if (!th) return;
|
||||||
|
const i = (th.messages || []).findIndex((m) => m && m.id === mid);
|
||||||
|
if (i < 0) return;
|
||||||
|
th.messages[i].content = content;
|
||||||
|
th.updatedAt = now();
|
||||||
|
await saveThreads(threads);
|
||||||
|
}
|
||||||
|
async function parseAndPersist(stream, threadId, mid) {
|
||||||
|
const reader = stream.getReader();
|
||||||
|
const dec = new TextDecoder("utf-8");
|
||||||
|
let buf = "", full = "", lastWrite = 0;
|
||||||
|
const flush = async (force = false) => {
|
||||||
|
const t = Date.now();
|
||||||
|
if (force || t - lastWrite > 250) {
|
||||||
|
await updateAssistantContent(threadId, mid, full);
|
||||||
|
lastWrite = t;
|
||||||
}
|
}
|
||||||
async function start(id, p) {
|
|
||||||
bc || (bc = new BroadcastChannel(CH));
|
|
||||||
const body = typeof p.body === "string" ? p.body : JSON.stringify(p.body || {}), ctrl = new AbortController();
|
|
||||||
let done = false, buf = "";
|
|
||||||
streams.set(id, { ctrl, buf: () => buf, done: () => done });
|
|
||||||
try {
|
|
||||||
const r = await fetch(p.url, { method: "POST", headers: p.headers || {}, body, signal: ctrl.signal });
|
|
||||||
if (!r.ok) throw new Error(await r.text().catch(() => "") || "HTTP " + r.status);
|
|
||||||
const rd = r.body.getReader(), dc = new TextDecoder("utf-8");
|
|
||||||
let acc = "";
|
|
||||||
const doneOnce = () => {
|
|
||||||
if (done) return;
|
|
||||||
done = true;
|
|
||||||
send({ t: "done", id });
|
|
||||||
};
|
};
|
||||||
for (; ; ) {
|
while (true) {
|
||||||
const { value, done: d } = await rd.read();
|
const { value, done } = await reader.read();
|
||||||
if (d) break;
|
if (done) break;
|
||||||
acc += dc.decode(value, { stream: true });
|
buf += dec.decode(value, { stream: true });
|
||||||
let i;
|
let idx;
|
||||||
while ((i = acc.indexOf("\n\n")) !== -1) {
|
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
||||||
const raw = acc.slice(0, i).trim();
|
const chunk = buf.slice(0, idx).trim();
|
||||||
acc = acc.slice(i + 2);
|
buf = buf.slice(idx + 2);
|
||||||
if (!raw || !raw.startsWith("data:")) continue;
|
if (!chunk) continue;
|
||||||
const data = raw.slice(5).trim();
|
if (chunk.startsWith("data:")) {
|
||||||
|
const data = chunk.slice(5).trim();
|
||||||
if (data === "[DONE]") {
|
if (data === "[DONE]") {
|
||||||
doneOnce();
|
await flush(true);
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const j = JSON.parse(data);
|
const json = JSON.parse(data);
|
||||||
const delta = j.choices?.[0]?.delta?.content ?? "";
|
const d = json && json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content || "";
|
||||||
if (delta) {
|
if (d) {
|
||||||
buf += delta;
|
full += d;
|
||||||
send({ t: "delta", id, data: delta });
|
await flush(false);
|
||||||
}
|
}
|
||||||
const fin = j.choices?.[0]?.finish_reason;
|
} catch (_) {
|
||||||
if (fin) doneOnce();
|
|
||||||
} catch {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doneOnce();
|
|
||||||
} catch (e) {
|
|
||||||
const aborted = e?.name === "AbortError" || /abort/i.test(String(e?.message || ""));
|
|
||||||
if (aborted) send({ t: "done", id, aborted: true });
|
|
||||||
else send({ t: "error", id, hint: hint(e?.message) });
|
|
||||||
} finally {
|
|
||||||
streams.delete(id);
|
|
||||||
}
|
}
|
||||||
|
await flush(true);
|
||||||
}
|
}
|
||||||
function stop(id) {
|
async function handleOpenRouterEvent(event) {
|
||||||
const s = streams.get(id);
|
const req = event.request;
|
||||||
if (!s) return;
|
const url = new URL(req.url);
|
||||||
|
const isTarget = url.hostname === "openrouter.ai" && /\/api\/v1\/chat\/completions$/.test(url.pathname);
|
||||||
|
if (!isTarget) return fetch(req);
|
||||||
|
let reqMeta = {};
|
||||||
try {
|
try {
|
||||||
s.ctrl.abort();
|
const t = await req.clone().text();
|
||||||
} catch {
|
reqMeta = JSON.parse(t || "{}");
|
||||||
} finally {
|
} catch (_) {
|
||||||
streams.delete(id);
|
|
||||||
}
|
}
|
||||||
|
if (!reqMeta || !reqMeta.stream) {
|
||||||
|
return fetch(req);
|
||||||
}
|
}
|
||||||
function replay(id, offset = 0) {
|
const res = await fetch(req);
|
||||||
const s = streams.get(id);
|
if (!res.body) return res;
|
||||||
if (!s) return;
|
const [toClient, toTap] = res.body.tee();
|
||||||
const cur = s.buf(), remain = cur.slice(Math.max(0, offset));
|
event.waitUntil((async () => {
|
||||||
if (remain) send({ t: "delta", id, data: remain });
|
try {
|
||||||
if (s.done()) send({ t: "done", id });
|
const msgs = Array.isArray(reqMeta.messages) ? reqMeta.messages : [];
|
||||||
|
const meta = { sune_name: "", model: reqMeta.model || "", avatar: "" };
|
||||||
|
const threadId = await ensureThreadForWrite({ messages: msgs });
|
||||||
|
const mid = await appendAssistantPlaceholder(threadId, meta);
|
||||||
|
await parseAndPersist(toTap, threadId, mid);
|
||||||
|
} catch (_) {
|
||||||
}
|
}
|
||||||
bc = new BroadcastChannel(CH);
|
})());
|
||||||
bc.onmessage = (e) => {
|
const headers = new Headers(res.headers);
|
||||||
const m = e.data || {};
|
return new Response(toClient, { status: res.status, statusText: res.statusText, headers });
|
||||||
if (m.t === "hello") send({ t: "hello-ack" });
|
}
|
||||||
else if (m.t === "start" && m.id && m.payload) start(m.id, m.payload);
|
self.addEventListener("install", (e) => {
|
||||||
else if (m.t === "stop" && m.id) stop(m.id);
|
self.skipWaiting();
|
||||||
else if (m.t === "replay" && m.id) replay(m.id, m.offset | 0);
|
});
|
||||||
};
|
self.addEventListener("activate", (e) => {
|
||||||
|
e.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
self.addEventListener("fetch", (e) => {
|
||||||
|
e.respondWith(handleOpenRouterEvent(e));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user