重构popup.js

This commit is contained in:
luke 2025-06-01 11:34:10 +08:00
parent d605c08a7d
commit 792c02b283
11 changed files with 466 additions and 642 deletions

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "渠道应用开发部知识库",
"version": "1.5.1",
"version": "1.5.2",
"description": "通过关键词快速搜索内部文档",
"permissions": ["storage"],
"host_permissions": [

View File

@ -9,6 +9,6 @@
</head>
<body>
<div style="padding: 1rem; font-size: 14px;">正在加载,请稍候...</div>
<script src="index.js"></script>
<script src="../scripts/index.js"></script>
</body>
</html>

View File

@ -55,6 +55,6 @@
<button id="loginBtn">登录</button>
</div>
<div class="error" id="errorBox"></div>
<script src="login.js"></script>
<script src="../scripts/login.js"></script>
</body>
</html>

View File

@ -56,6 +56,11 @@
</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>
</html>

View File

@ -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 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 = '';
});
}
});

23
scripts/auth.js Normal file
View 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
View 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
View 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
View 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));
}