diff --git a/manifest.json b/manifest.json index 3a3416d..a4a430b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "渠道应用开发部知识库", - "version": "1.5.1", + "version": "1.5.2", "description": "通过关键词快速搜索内部文档", "permissions": ["storage"], "host_permissions": [ diff --git a/pages/index.html b/pages/index.html index d92ae60..eeaeb0e 100644 --- a/pages/index.html +++ b/pages/index.html @@ -9,6 +9,6 @@
正在加载,请稍候...
- + diff --git a/pages/login.html b/pages/login.html index 6733d5f..b990280 100644 --- a/pages/login.html +++ b/pages/login.html @@ -55,6 +55,6 @@
- + diff --git a/pages/popup.html b/pages/popup.html index 4c7e4c3..89c6139 100644 --- a/pages/popup.html +++ b/pages/popup.html @@ -56,6 +56,11 @@ - + + + + + + diff --git a/pages/popup.js b/pages/popup.js deleted file mode 100644 index bc7879b..0000000 --- a/pages/popup.js +++ /dev/null @@ -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 = `${type === "user" ? "我:" : "智能助手:"} ${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 = `我: ${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 += "LLM接口请求失败"; - 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("")) { - inThink = true; - content = content.replace("", ""); - } - if (content.includes("")) { - inThink = false; - content = content.replace("", ""); - } - if (inThink) { - thinkStr += content.replace(/\n/g, "
"); - } else if (chunk.textResponse) { - answerStr += content.replace(/\n/g, "
"); - } - // 实时渲染,思考内容和正式回答分区块 - msgBox.innerHTML = - (thinkStr - ? `
${thinkStr}
` - : "") + - `智能助手:${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 += "LLM请求异常:" + e + ""; - } - } - - 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 = "
  • 加载中...
  • "; - } - - 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 = "
  • 未找到相关文档
  • "; - 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 = "
  • 搜索失败,请检查网络或服务状态。
  • "; - } - } - - 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 = ''; - }); - } -}); - diff --git a/scripts/auth.js b/scripts/auth.js new file mode 100644 index 0000000..8be933f --- /dev/null +++ b/scripts/auth.js @@ -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"); + }); +} diff --git a/pages/index.js b/scripts/index.js similarity index 100% rename from pages/index.js rename to scripts/index.js diff --git a/scripts/llm.js b/scripts/llm.js new file mode 100644 index 0000000..513a2ce --- /dev/null +++ b/scripts/llm.js @@ -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 = `我: ${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 = "LLM接口请求失败"; + 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("")) { + inThink = true; + content = content.replace("", ""); + } + if (content.includes("")) { + inThink = false; + content = content.replace("", ""); + } + if (inThink) { + thinkStr += content.replace(/\n/g, "
    "); + } else { + answerStr += content.replace(/\n/g, "
    "); + } + msgBox.innerHTML = + (thinkStr ? `
    ${thinkStr}
    ` : "") + + `智能助手:${answerStr}`; + contentDiv.scrollTop = contentDiv.scrollHeight; + } + } + } + } + } catch (e) { + msgBox.innerHTML += `LLM异常:${e}`; + } +} diff --git a/pages/login.js b/scripts/login.js similarity index 100% rename from pages/login.js rename to scripts/login.js diff --git a/scripts/popup.js b/scripts/popup.js new file mode 100644 index 0000000..fa40a5e --- /dev/null +++ b/scripts/popup.js @@ -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 = "
  • 加载中...
  • "; + } + + 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 = "
  • 未找到相关文档
  • "; + 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 = "
  • 搜索失败,请检查网络或服务状态。
  • "; + } + } + + // ===== 历史记录 ===== + 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(); +}); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..d06412b --- /dev/null +++ b/scripts/utils.js @@ -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)); +}