639 lines
21 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 () => {
// 标签页/弹窗自适应:宽大则自动全屏自适应
if (window.innerWidth > 900 && window.innerHeight > 700) {
document.body.classList.add('fullscreen');
// 禁用拖拽
const panel = document.getElementById('draggablePanel');
if (panel) {
panel.style.left = '';
panel.style.top = '';
panel.style.position = 'fixed';
}
} else {
// 弹窗环境支持拖拽
const panel = document.getElementById('draggablePanel');
const header = document.querySelector('.header');
let dragging = false, offsetX = 0, offsetY = 0;
if (panel && header) {
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 = '';
});
}
}
// 标签页打开按钮
const openTabBtn = document.getElementById('openTabBtn');
if (openTabBtn) {
openTabBtn.onclick = function () {
if (window.chrome && chrome.runtime && chrome.runtime.id && chrome.tabs && chrome.runtime.getURL) {
try {
chrome.tabs.create({ url: chrome.runtime.getURL("pages/popup.html") });
} catch (e) {
window.open("popup.html", "_blank");
}
} else {
window.open("popup.html", "_blank");
}
};
}
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_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;
// 新增tab切换功能
const tabEs = document.getElementById("tabEs");
const tabAi = document.getElementById("tabAi");
const tabContentEs = document.getElementById("tabContentEs");
const tabContentAi = document.getElementById("tabContentAi");
tabEs.onclick = function() {
tabEs.classList.add("active");
tabAi.classList.remove("active");
tabContentEs.classList.add("active");
tabContentAi.classList.remove("active");
};
tabAi.onclick = function() {
tabEs.classList.remove("active");
tabAi.classList.add("active");
tabContentEs.classList.remove("active");
tabContentAi.classList.add("active");
// 自动滚动到最底部
setTimeout(() => {
const llmContent = document.getElementById("llmResultContent");
if (llmContent) llmContent.scrollTop = llmContent.scrollHeight;
}, 100);
};
// 动态获取 AnythingLLM 密码
async function getLLMPassword() {
return new Promise((resolve) => {
chrome.storage.local.get(["llmPassword"], (res) => {
resolve(res.llmPassword || "");
});
});
}
// 获取 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 {
const LLM_PASSWORD = await getLLMPassword();
console.log("开始请求 LLM tokenPOST", LLM_TOKEN_API, "参数:", { 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;
}
// 流式渲染智能助手
async function fetchLLMAnswer(question) {
if (!question || !question.trim()) return;
// 追加用户提问
const userItem = document.createElement("div");
userItem.className = "llm-msg-item llm-user";
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${question}`;
llmResultContent.appendChild(userItem);
llmResultContainer.classList.remove("hidden");
llmResultContent.scrollTop = llmResultContent.scrollHeight;
// 创建 assistant 消息卡片
const msgBox = document.createElement("div");
msgBox.className = "llm-msg-item llm-assistant";
msgBox.style.margin = "3px 0 7px 0";
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 inThink = false;
let thinkStr = "";
let answerStr = "";
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") {
let content = chunk.textResponse || "";
if (content.includes("<think>")) {
inThink = true;
content = content.replace("<think>", "");
}
if (content.includes("</think>")) {
inThink = false;
content = content.replace("</think>", "");
}
if (inThink) {
thinkStr += content.replace(/\n/g, "<br>");
} else if (chunk.textResponse) {
answerStr += content.replace(/\n/g, "<br>");
}
// 实时渲染,思考内容和正式回答分区块
msgBox.innerHTML =
(thinkStr
? `<div class="llm-think">${thinkStr}</div>`
: "") +
`<span class="assistant-label">智能助手:</span>${answerStr}`;
llmResultContent.scrollTop = llmResultContent.scrollHeight;
if (chunk.close) {
stop = true;
break;
}
}
} catch (e) {}
}
}
}
if (stop) break;
}
if (!thinkStr && !answerStr) {
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(rawInput, page) {
log("执行搜索:", rawInput, "page:", page);
if (!token) {
log("token 未初始化,跳转 login");
handleLogout();
return;
}
currentPage = page;
lastKeywords = rawInput;
const keywordGroups = parseKeywordGroups(rawInput);
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 parseKeywordGroups(inputStr) {
if (typeof inputStr !== "string") return [];
return inputStr
.trim()
.split("||") // 用 || 分组(表示 OR
.map(group => {
group = group.trim();
if (group.startsWith("(") && group.endsWith(")")) {
group = group.slice(1, -1); // 去括号
}
return group.split(/\s+/).filter(Boolean); // 空格分词AND
})
.filter(g => g.length > 0);
}
function triggerSearch() {
if (!token) {
log("triggerSearch 中 token 丢失,取消搜索");
return;
}
const raw = input?.value?.trim();
if (!raw) return clearResults();
updateHistory(raw);
// 并发触发 AnythingLLM + ES
fetchLLMAnswer(raw);
fetchResults(raw, 1); // 传字符串,不是 keywords[]
}
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 = '';
});
})();
document.addEventListener('DOMContentLoaded', function () {
// 标签页打开按钮事件
const openTabBtn = document.getElementById('openTabBtn');
if (openTabBtn) {
openTabBtn.onclick = function () {
if (window.chrome && chrome.runtime && chrome.runtime.id && chrome.tabs && chrome.runtime.getURL) {
try {
chrome.tabs.create({ url: chrome.runtime.getURL("pages/popup.html") });
} catch (e) {
window.open("popup.html", "_blank");
}
} else {
window.open("popup.html", "_blank");
}
};
}
// 拖拽功能(如你的页面有 .draggable-panel
const panel = document.getElementById('draggablePanel');
const header = document.querySelector('.header');
let dragging = false, offsetX = 0, offsetY = 0;
if (panel && header) {
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 = '';
});
}
});