重构popup.js
This commit is contained in:
parent
d605c08a7d
commit
792c02b283
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "渠道应用开发部知识库",
|
"name": "渠道应用开发部知识库",
|
||||||
"version": "1.5.1",
|
"version": "1.5.2",
|
||||||
"description": "通过关键词快速搜索内部文档",
|
"description": "通过关键词快速搜索内部文档",
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
|
|||||||
@ -9,6 +9,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="padding: 1rem; font-size: 14px;">正在加载,请稍候...</div>
|
<div style="padding: 1rem; font-size: 14px;">正在加载,请稍候...</div>
|
||||||
<script src="index.js"></script>
|
<script src="../scripts/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -55,6 +55,6 @@
|
|||||||
<button id="loginBtn">登录</button>
|
<button id="loginBtn">登录</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="error" id="errorBox"></div>
|
<div class="error" id="errorBox"></div>
|
||||||
<script src="login.js"></script>
|
<script src="../scripts/login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -56,6 +56,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script src="popup.js"></script>
|
<!-- JS模块加载顺序非常重要,请保持此顺序 -->
|
||||||
|
<script src="../scripts/utils.js"></script>
|
||||||
|
<script src="../scripts/auth.js"></script>
|
||||||
|
<script src="../scripts/llm.js"></script>
|
||||||
|
<script src="../scripts/popup.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
638
pages/popup.js
638
pages/popup.js
@ -1,638 +0,0 @@
|
|||||||
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 token,POST", 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("后端返回 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 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 = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
23
scripts/auth.js
Normal file
23
scripts/auth.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// scripts/auth.js
|
||||||
|
const TOKEN_EXPIRE_MS = 7 * 24 * 60 * 60 * 1000; // 7天
|
||||||
|
|
||||||
|
async function loadToken() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get(["token", "loginAt", "username"], (res) => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!res.token || !res.loginAt || isNaN(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");
|
||||||
|
});
|
||||||
|
}
|
||||||
146
scripts/llm.js
Normal file
146
scripts/llm.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// scripts/llm.js
|
||||||
|
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;
|
||||||
|
|
||||||
|
async function getLLMPassword() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.storage.local.get(["llmPassword"], (res) => {
|
||||||
|
resolve(res.llmPassword || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
log("LLM token 命中缓存");
|
||||||
|
resolve(res.llmToken);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const pwd = await getLLMPassword();
|
||||||
|
const response = await fetch(LLM_TOKEN_API, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ password: pwd }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.token) {
|
||||||
|
chrome.storage.local.set({ llmToken: data.token, llmTokenAt: now });
|
||||||
|
resolve(data.token);
|
||||||
|
} else {
|
||||||
|
reject("LLM token获取失败");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject("LLM token接口异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
resolve(res.llmWorkspaceSlug);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const response = await fetch(LLM_WORKSPACES_API, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.workspaces && data.workspaces.length > 0 && data.workspaces[0].slug) {
|
||||||
|
chrome.storage.local.set({ llmWorkspaceSlug: data.workspaces[0].slug, llmWorkspaceAt: now });
|
||||||
|
resolve(data.workspaces[0].slug);
|
||||||
|
} else {
|
||||||
|
reject("未获取到LLM工作区slug");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject("LLM工作区接口异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLLMAnswer(question, container, contentDiv) {
|
||||||
|
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}`;
|
||||||
|
contentDiv.appendChild(userItem);
|
||||||
|
container.classList.remove("hidden");
|
||||||
|
|
||||||
|
const msgBox = document.createElement("div");
|
||||||
|
msgBox.className = "llm-msg-item llm-assistant";
|
||||||
|
contentDiv.appendChild(msgBox);
|
||||||
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await getLLMToken();
|
||||||
|
const workspace = await getLLMWorkspaceSlug(token);
|
||||||
|
const url = `${LLM_STREAM_API_BASE}/${workspace}/stream-chat`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
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 = "", done = false;
|
||||||
|
let thinkStr = "", answerStr = "", inThink = false;
|
||||||
|
|
||||||
|
while (!done) {
|
||||||
|
const { value, done: readerDone } = await reader.read();
|
||||||
|
done = readerDone;
|
||||||
|
if (value) {
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop();
|
||||||
|
for (let line of lines) {
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
const chunk = JSON.parse(data);
|
||||||
|
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 {
|
||||||
|
answerStr += content.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
msgBox.innerHTML =
|
||||||
|
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
||||||
|
`<span class="assistant-label">智能助手:</span>${answerStr}`;
|
||||||
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
279
scripts/popup.js
Normal file
279
scripts/popup.js
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// ===== 自适应 / 拖拽 =====
|
||||||
|
const panel = document.getElementById("draggablePanel");
|
||||||
|
const header = document.querySelector(".header");
|
||||||
|
|
||||||
|
if (window.innerWidth > 900 && window.innerHeight > 700) {
|
||||||
|
document.body.classList.add("fullscreen");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.left = "";
|
||||||
|
panel.style.top = "";
|
||||||
|
panel.style.position = "fixed";
|
||||||
|
}
|
||||||
|
} else if (panel && header) {
|
||||||
|
let dragging = false, offsetX = 0, offsetY = 0;
|
||||||
|
header.addEventListener("mousedown", (e) => {
|
||||||
|
dragging = true;
|
||||||
|
offsetX = e.clientX - panel.offsetLeft;
|
||||||
|
offsetY = e.clientY - panel.offsetTop;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (dragging) {
|
||||||
|
panel.style.left = Math.max(0, e.clientX - offsetX) + "px";
|
||||||
|
panel.style.top = Math.max(0, e.clientY - offsetY) + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
dragging = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 元素绑定 =====
|
||||||
|
const input = document.getElementById("searchInput");
|
||||||
|
const searchBtn = document.getElementById("searchBtn");
|
||||||
|
const clearBtn = document.getElementById("clearHistoryBtn");
|
||||||
|
const historyBox = document.getElementById("historyTags");
|
||||||
|
const historyWrapper = document.getElementById("historyWrapper");
|
||||||
|
const resultBox = document.getElementById("results");
|
||||||
|
const llmResultContainer = document.getElementById("llmResultContainer");
|
||||||
|
const llmResultContent = document.getElementById("llmResultContent");
|
||||||
|
const llmInput = document.getElementById("llmInput");
|
||||||
|
const llmSendBtn = document.getElementById("llmSendBtn");
|
||||||
|
const logoutBtn = document.getElementById("logoutBtn");
|
||||||
|
const userNameLabel = document.getElementById("userName");
|
||||||
|
const userAvatar = document.getElementById("userAvatar");
|
||||||
|
const userDropdown = document.getElementById("userDropdown");
|
||||||
|
const openTabBtn = document.getElementById("openTabBtn");
|
||||||
|
|
||||||
|
// ===== 标签切换 =====
|
||||||
|
const tabEs = document.getElementById("tabEs");
|
||||||
|
const tabAi = document.getElementById("tabAi");
|
||||||
|
const tabContentEs = document.getElementById("tabContentEs");
|
||||||
|
const tabContentAi = document.getElementById("tabContentAi");
|
||||||
|
|
||||||
|
tabEs.onclick = () => {
|
||||||
|
tabEs.classList.add("active");
|
||||||
|
tabAi.classList.remove("active");
|
||||||
|
tabContentEs.classList.add("active");
|
||||||
|
tabContentAi.classList.remove("active");
|
||||||
|
};
|
||||||
|
tabAi.onclick = () => {
|
||||||
|
tabEs.classList.remove("active");
|
||||||
|
tabAi.classList.add("active");
|
||||||
|
tabContentEs.classList.remove("active");
|
||||||
|
tabContentAi.classList.add("active");
|
||||||
|
setTimeout(() => {
|
||||||
|
llmResultContent.scrollTop = llmResultContent.scrollHeight;
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== 新标签页打开 popup.html =====
|
||||||
|
if (openTabBtn) {
|
||||||
|
openTabBtn.onclick = () => {
|
||||||
|
const url = chrome?.runtime?.getURL("pages/popup.html") || "popup.html";
|
||||||
|
try {
|
||||||
|
chrome?.tabs?.create({ url });
|
||||||
|
} catch {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 状态变量 =====
|
||||||
|
let token = null;
|
||||||
|
let currentPage = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
let lastKeywords = [];
|
||||||
|
|
||||||
|
// ===== 加载 token(带用户名)=====
|
||||||
|
const auth = await loadToken();
|
||||||
|
if (!auth) {
|
||||||
|
log("未认证,登出");
|
||||||
|
handleLogout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
token = auth.token;
|
||||||
|
userNameLabel.textContent = auth.username;
|
||||||
|
log("用户认证成功:", auth.username);
|
||||||
|
|
||||||
|
// ===== 搜索触发函数 =====
|
||||||
|
async function triggerSearch() {
|
||||||
|
const raw = input?.value?.trim();
|
||||||
|
if (!token || !raw) return;
|
||||||
|
|
||||||
|
updateHistory(raw);
|
||||||
|
renderHistoryTags();
|
||||||
|
fetchLLMAnswer(raw, llmResultContainer, llmResultContent);
|
||||||
|
fetchResults(raw, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 搜索实现(含 ES + 分页)=====
|
||||||
|
function parseKeywordGroups(inputStr) {
|
||||||
|
return inputStr
|
||||||
|
.trim()
|
||||||
|
.split("||")
|
||||||
|
.map(group => {
|
||||||
|
group = group.trim();
|
||||||
|
if (group.startsWith("(") && group.endsWith(")")) group = group.slice(1, -1);
|
||||||
|
return group.split(/\s+/).filter(Boolean);
|
||||||
|
})
|
||||||
|
.filter(g => g.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 prev = document.createElement("button");
|
||||||
|
prev.textContent = "上一页";
|
||||||
|
prev.onclick = () => fetchResults(lastKeywords, currentPage - 1);
|
||||||
|
pagination.appendChild(prev);
|
||||||
|
}
|
||||||
|
if (currentPage < totalPages) {
|
||||||
|
const next = document.createElement("button");
|
||||||
|
next.textContent = "下一页";
|
||||||
|
next.onclick = () => fetchResults(lastKeywords, currentPage + 1);
|
||||||
|
pagination.appendChild(next);
|
||||||
|
}
|
||||||
|
resultBox.appendChild(pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchResults(rawInput, page) {
|
||||||
|
currentPage = page;
|
||||||
|
lastKeywords = rawInput;
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
const keywordGroups = parseKeywordGroups(rawInput);
|
||||||
|
const requestBody = {
|
||||||
|
keywordGroups,
|
||||||
|
page: currentPage,
|
||||||
|
size: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://app.wisdompulse.cn/search", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `${token}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) return handleLogout();
|
||||||
|
|
||||||
|
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 path = doc.filepath.replace(/^import-data\//, "").replace(/\.md$/, ".html");
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = `http://share.wisdompulse.cn/public/${path}`;
|
||||||
|
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) {
|
||||||
|
resultBox.innerHTML = "<li>搜索失败,请检查网络或服务状态。</li>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 历史记录 =====
|
||||||
|
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(show = false) {
|
||||||
|
chrome.storage.local.get(["searchHistory"], (res) => {
|
||||||
|
const history = res.searchHistory || [];
|
||||||
|
historyBox.innerHTML = "";
|
||||||
|
|
||||||
|
if (history.length === 0 || !show) {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== DOM 事件绑定 =====
|
||||||
|
searchBtn.addEventListener("click", triggerSearch);
|
||||||
|
input.addEventListener("keydown", e => e.key === "Enter" && triggerSearch());
|
||||||
|
input.addEventListener("focus", () => renderHistoryTags(true));
|
||||||
|
input.addEventListener("input", () => !input.value.trim() && clearResults());
|
||||||
|
input.addEventListener("blur", () => setTimeout(() => historyWrapper.classList.add("hidden"), 200));
|
||||||
|
clearBtn.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, llmResultContainer, llmResultContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
llmInput.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") llmSendBtn.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 首次展示历史
|
||||||
|
renderHistoryTags();
|
||||||
|
});
|
||||||
9
scripts/utils.js
Normal file
9
scripts/utils.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// scripts/utils.js
|
||||||
|
function log(...args) {
|
||||||
|
const version = chrome?.runtime?.getManifest?.().version || "dev";
|
||||||
|
console.log("[v" + version + "]", ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user