2025-05-28 16:42:34 +08:00

462 lines
15 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 llmResultContent = document.getElementById("llmResultContent");
const llmInput = document.getElementById("llmInput");
const llmSendBtn = document.getElementById("llmSendBtn");
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 = 7 * 24 * 60 * 60 * 1000; // 7天有效期
// === 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工作区接口异常");
}
}
});
});
}
// 追加智能助手问答内容
function appendLLMMsg(msg, type) {
const item = document.createElement("div");
item.className = "llm-msg-item " + (type === "user" ? "llm-user" : "llm-assistant");
item.style.margin = "6px 0";
item.innerHTML = `<span style="color:${type === "user" ? "#2a63c8" : "#2453b5"};">${type === "user" ? "我:" : "智能助手:"}</span> ${msg}`;
llmResultContent.appendChild(item);
llmResultContent.scrollTop = llmResultContent.scrollHeight;
}
// 支持多次提问、历史堆叠
// 新增/修正流式渲染 assistant 的方式
async function fetchLLMAnswer(question) {
if (!question || !question.trim()) return;
appendLLMMsg(question, "user");
llmResultContainer.classList.remove("hidden");
// 创建 assistant 消息卡片并持续写入
const msgBox = document.createElement("div");
msgBox.className = "llm-msg-item llm-assistant";
msgBox.style.margin = "3px 0 7px 0";
msgBox.innerHTML = `<span style="color:#2453b5;">智能助手:</span> `;
llmResultContent.appendChild(msgBox);
llmResultContent.scrollTop = llmResultContent.scrollHeight;
try {
const llmToken = await getLLMToken();
const workspaceSlug = await getLLMWorkspaceSlug(llmToken);
const url = `${LLM_STREAM_API_BASE}/${workspaceSlug}/stream-chat`;
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) {
msgBox.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;
let answer = "";
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);
if (chunk.type === "textResponseChunk") {
if (chunk.close) {
stop = true;
break;
}
if (chunk.textResponse) {
answer += chunk.textResponse.replace(/\n/g, "<br>");
msgBox.innerHTML = `<span style="color:#2453b5;">智能助手:</span> ${answer}`;
llmResultContent.scrollTop = llmResultContent.scrollHeight;
}
}
} catch (e) {}
}
}
}
if (stop) break;
}
if (!answer) {
msgBox.innerHTML += "无有效回答";
}
} catch (e) {
msgBox.innerHTML += "<span style='color:red;'>LLM请求异常" + e + "</span>";
}
}
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) => {
console.log("本地存储内容:", res);
const now = Date.now();
if (!res.token || !res.loginAt || isNaN(res.loginAt) || (now - res.loginAt > TOKEN_EXPIRE_MS)) {
console.log("token 不存在或已过期");
resolve(null);
} else {
console.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 = "";
llmResultContent.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");
}
});
// 多轮智能助手输入/回车发送
llmSendBtn.addEventListener("click", () => {
const val = llmInput.value.trim();
if (!val) return;
llmInput.value = "";
fetchLLMAnswer(val);
});
llmInput.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
llmSendBtn.click();
}
});
}
const auth = await loadToken();
if (!auth) {
log("未通过认证,执行登出");
handleLogout();
} else {
token = auth.token;
userNameLabel.textContent = auth.username;
log("用户认证通过,用户名:", auth.username);
bindEvents();
}
});
// 拖动+缩放
(function() {
const panel = document.getElementById('draggablePanel');
const header = document.querySelector('.header');
let dragging = false, offsetX = 0, offsetY = 0;
header.addEventListener('mousedown', function(e) {
dragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function(e) {
if (dragging) {
let left = Math.max(0, e.clientX - offsetX);
let top = Math.max(0, e.clientY - offsetY);
panel.style.left = left + 'px';
panel.style.top = top + 'px';
}
});
document.addEventListener('mouseup', function() {
dragging = false;
document.body.style.userSelect = '';
});
})();