532 lines
18 KiB
JavaScript
532 lines
18 KiB
JavaScript
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 token,POST", 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;
|
||
}
|
||
|
||
// 流式渲染智能助手
|
||
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(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("后端返回 401,token 无效,登出");
|
||
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 = '';
|
||
});
|
||
})();
|
||
|
||
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 = '';
|
||
});
|
||
}
|
||
});
|
||
|