2025-05-28 14:16:18 +08:00

401 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

document.addEventListener("DOMContentLoaded", async () => {
const input = document.getElementById("searchInput");
const searchBtn = document.getElementById("searchBtn");
const clearHistoryBtn = document.getElementById("clearHistoryBtn");
const resultBox = document.getElementById("results");
const historyBox = document.getElementById("historyTags");
const historyWrapper = document.getElementById("historyWrapper");
// AnythingLLM 相关节点
const llmResultContainer = document.getElementById("llmResultContainer");
const llmResultBox = document.getElementById("llmResult");
const userDropdown = document.getElementById("userDropdown");
const logoutBtn = document.getElementById("logoutBtn");
const userNameLabel = document.getElementById("userName");
const userAvatar = document.getElementById("userAvatar");
let token = null;
let currentPage = 1;
let totalPages = 1;
let lastKeywords = [];
const TOKEN_EXPIRE_MS = 24 * 60 * 60 * 1000;
// === AnythingLLM 配置 ===
const LLM_PASSWORD = "lukeye@6";
const LLM_TOKEN_API = "http://llm.wisdompulse.cn/api/request-token";
const LLM_WORKSPACES_API = "http://llm.wisdompulse.cn/api/workspaces";
const LLM_STREAM_API_BASE = "http://llm.wisdompulse.cn/api/workspace";
const LLM_TOKEN_EXPIRE_MS = 23 * 60 * 60 * 1000;
// 获取 LLM Token
async function getLLMToken(forceRefresh = false) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => {
const now = Date.now();
if (
!forceRefresh &&
res.llmToken &&
res.llmTokenAt &&
now - res.llmTokenAt < LLM_TOKEN_EXPIRE_MS
) {
console.log("LLM token 命中缓存,有效期内");
resolve(res.llmToken);
} else {
try {
console.log("开始请求 LLM tokenPOST", LLM_TOKEN_API, "参数:", { password: LLM_PASSWORD });
const response = await fetch(LLM_TOKEN_API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: LLM_PASSWORD }),
});
const data = await response.json();
console.log("LLM token 接口返回:", data);
if (data.token) {
chrome.storage.local.set({ llmToken: data.token, llmTokenAt: now });
console.log("LLM token 已缓存");
resolve(data.token);
} else {
console.log("LLM token 获取失败,响应内容:", data);
reject("LLM token获取失败");
}
} catch (e) {
console.log("LLM token 请求异常:", e);
reject("LLM token接口异常");
}
}
});
});
}
// 获取 LLM workspaceSlug修正
async function getLLMWorkspaceSlug(token, forceRefresh = false) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(["llmWorkspaceSlug", "llmWorkspaceAt"], async (res) => {
const now = Date.now();
if (
!forceRefresh &&
res.llmWorkspaceSlug &&
res.llmWorkspaceAt &&
now - res.llmWorkspaceAt < LLM_TOKEN_EXPIRE_MS
) {
console.log("LLM workspaceSlug 命中缓存,有效期内:", res.llmWorkspaceSlug);
resolve(res.llmWorkspaceSlug);
} else {
try {
console.log("开始请求 LLM 工作区列表GET", LLM_WORKSPACES_API, "Authorization: Bearer", token);
const response = await fetch(LLM_WORKSPACES_API, {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
const data = await response.json();
console.log("LLM 工作区接口返回:", data);
if (data.workspaces && Array.isArray(data.workspaces) && data.workspaces.length > 0 && data.workspaces[0].slug) {
chrome.storage.local.set({
llmWorkspaceSlug: data.workspaces[0].slug,
llmWorkspaceAt: now,
});
console.log("LLM workspaceSlug 已缓存:", data.workspaces[0].slug);
resolve(data.workspaces[0].slug);
} else {
console.log("未获取到有效的 LLM 工作区 slug响应内容", data);
reject("未获取到LLM工作区slug");
}
} catch (e) {
console.log("LLM 工作区接口请求异常:", e);
reject("LLM工作区接口异常");
}
}
});
});
}
// 调用 AnythingLLM stream-chat 并流式渲染
async function fetchLLMAnswer(question) {
llmResultBox.innerHTML = "";
llmResultContainer.classList.remove("hidden");
try {
console.log("开始获取 LLM token 和 workspaceSlug...");
const llmToken = await getLLMToken();
const workspaceSlug = await getLLMWorkspaceSlug(llmToken);
const url = `${LLM_STREAM_API_BASE}/${workspaceSlug}/stream-chat`;
console.log("开始请求 LLM stream-chatPOST", url, "参数:", { message: question, attachments: [] });
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${llmToken}`,
},
body: JSON.stringify({ message: question, attachments: [] }),
});
if (!res.ok || !res.body) {
console.log("LLM stream-chat 请求失败,状态:", res.status);
llmResultBox.innerHTML = "<span style='color:red'>LLM接口请求失败</span>";
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let done = false;
let stop = false;
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split("\n");
buffer = lines.pop();
for (let line of lines) {
if (line.startsWith("data: ")) {
const data = line.replace("data: ", "");
try {
const chunk = JSON.parse(data);
console.log("LLM 返回片段:", chunk);
if (chunk.type === "textResponseChunk") {
if (chunk.close) {
stop = true;
break;
}
if (chunk.textResponse) {
llmResultBox.innerHTML += chunk.textResponse.replace(/\n/g, "<br>");
llmResultBox.scrollTop = llmResultBox.scrollHeight;
}
}
} catch (e) {
console.log("LLM 片段解析异常:", e, "行内容:", line);
}
}
}
}
if (stop) break;
}
console.log("LLM stream-chat 已完成。");
} catch (e) {
console.log("LLM stream-chat 全流程异常:", e);
llmResultBox.innerHTML = `<span style='color:red'>LLM请求异常${e}</span>`;
llmResultContainer.classList.remove("hidden");
}
}
function log(...args) {
const version = chrome?.runtime?.getManifest?.().version || "dev";
console.log("[v" + version + "]", ...args);
}
async function loadToken() {
return new Promise((resolve) => {
chrome.storage.local.get(["token", "loginAt", "username"], (res) => {
log("加载本地存储 token:", res);
const now = Date.now();
if (!res.token || !res.loginAt || (now - res.loginAt > TOKEN_EXPIRE_MS)) {
log("token 不存在或已过期");
resolve(null);
} else {
log("token 加载成功");
resolve({
token: res.token,
username: res.username || "用户"
});
}
});
});
}
function handleLogout() {
chrome.storage.local.remove(["token", "loginAt", "username"], () => {
window.location.href = chrome.runtime.getURL("pages/login.html");
});
}
function showLoading() {
resultBox.innerHTML = "<li>加载中...</li>";
}
function clearResults() {
resultBox.innerHTML = "";
llmResultBox.innerHTML = "";
llmResultContainer.classList.add("hidden");
historyWrapper.classList.add("hidden");
}
function renderPagination() {
const pagination = document.createElement("div");
pagination.className = "pagination";
if (currentPage > 1) {
const prevBtn = document.createElement("button");
prevBtn.textContent = "上一页";
prevBtn.onclick = () => fetchResults(lastKeywords, currentPage - 1);
pagination.appendChild(prevBtn);
}
if (currentPage < totalPages) {
const nextBtn = document.createElement("button");
nextBtn.textContent = "下一页";
nextBtn.onclick = () => fetchResults(lastKeywords, currentPage + 1);
pagination.appendChild(nextBtn);
}
resultBox.appendChild(pagination);
}
function updateHistory(keyword) {
chrome.storage.local.get(["searchHistory"], (res) => {
let history = res.searchHistory || [];
history = [keyword, ...history.filter(item => item !== keyword)];
if (history.length > 5) history = history.slice(0, 5);
chrome.storage.local.set({ searchHistory: history });
});
}
function renderHistoryTags() {
chrome.storage.local.get(["searchHistory"], (res) => {
const history = res.searchHistory || [];
historyBox.innerHTML = "";
if (history.length === 0) {
historyWrapper.classList.add("hidden");
return;
}
history.forEach(tagText => {
const tag = document.createElement("span");
tag.className = "tag";
tag.textContent = tagText;
tag.onclick = () => {
input.value = tagText;
triggerSearch();
};
historyBox.appendChild(tag);
});
historyWrapper.classList.remove("hidden");
});
}
async function fetchResults(keywords, page) {
log("执行搜索:", keywords, "page:", page);
if (!token) {
log("token 未初始化,跳转 login");
handleLogout();
return;
}
currentPage = page;
lastKeywords = keywords;
const keywordGroups = keywords.map(k => [k]);
const requestBody = {
keywordGroups,
page: currentPage,
size: 10
};
showLoading();
try {
const response = await fetch("http://app.wisdompulse.cn/search", {
method: "POST",
headers: {
"Authorization": `${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(requestBody)
});
log("响应状态:", response.status);
if (response.status === 401) {
log("后端返回 401token 无效,登出");
handleLogout();
return;
}
const data = await response.json();
resultBox.innerHTML = "";
if (!data.results || data.results.length === 0) {
resultBox.innerHTML = "<li>未找到相关文档</li>";
return;
}
totalPages = Math.ceil(data.total / data.size);
data.results.forEach(doc => {
const li = document.createElement("li");
const cleanPath = doc.filepath.replace(/^import-data\//, "").replace(/\.md$/, ".html");
const link = document.createElement("a");
link.href = `http://share.wisdompulse.cn/public/${cleanPath}`;
link.target = "_blank";
link.textContent = doc.filename;
const snippet = document.createElement("div");
snippet.className = "summary";
snippet.innerHTML = doc.summary?.slice(0, 300) + "...";
li.appendChild(link);
li.appendChild(snippet);
resultBox.appendChild(li);
});
renderPagination();
} catch (err) {
console.log("搜索请求失败:", err);
resultBox.innerHTML = "<li>搜索失败,请检查网络或服务状态。</li>";
}
}
function triggerSearch() {
if (!token) {
log("triggerSearch 中 token 丢失,取消搜索");
return;
}
const val = input.value.trim();
if (!val) return clearResults();
const keywords = val.split(/\s+/);
updateHistory(val);
// 并发触发 AnythingLLM + ES
fetchLLMAnswer(val);
fetchResults(keywords, 1);
}
function bindEvents() {
log("绑定事件成功");
searchBtn.addEventListener("click", triggerSearch);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
log("用户按下回车");
triggerSearch();
}
});
input.addEventListener("focus", renderHistoryTags);
input.addEventListener("input", () => !input.value.trim() && clearResults());
input.addEventListener("blur", () => setTimeout(() => historyWrapper.classList.add("hidden"), 200));
clearHistoryBtn.addEventListener("click", () => {
chrome.storage.local.remove("searchHistory", renderHistoryTags);
});
logoutBtn.addEventListener("click", handleLogout);
userAvatar.addEventListener("click", () => userDropdown.classList.toggle("hidden"));
window.addEventListener("click", (e) => {
if (!userDropdown.contains(e.target) && e.target !== userAvatar) {
userDropdown.classList.add("hidden");
}
});
}
const auth = await loadToken();
if (!auth) {
log("未通过认证,执行登出");
handleLogout();
} else {
token = auth.token;
userNameLabel.textContent = auth.username;
log("用户认证通过,用户名:", auth.username);
bindEvents();
}
});