diff --git a/manifest.json b/manifest.json index 9ae8053..91a7d93 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "渠道应用开发部知识库", - "version": "1.3", + "version": "1.3.1", "description": "通过关键词快速搜索内部文档", "permissions": ["storage"], "host_permissions": [ diff --git a/pages/popup.html b/pages/popup.html index ada0d8a..1d0c4d7 100644 --- a/pages/popup.html +++ b/pages/popup.html @@ -34,9 +34,16 @@
+ + + + diff --git a/pages/popup.js b/pages/popup.js index bffd145..0fb5664 100644 --- a/pages/popup.js +++ b/pages/popup.js @@ -6,6 +6,10 @@ document.addEventListener("DOMContentLoaded", async () => { const historyBox = document.getElementById("historyTags"); const historyWrapper = document.getElementById("historyWrapper"); + // AnythingLLM 相关节点 + const llmResultContainer = document.getElementById("llmResultContainer"); + const llmResultBox = document.getElementById("llmResult"); + const userDropdown = document.getElementById("userDropdown"); const logoutBtn = document.getElementById("logoutBtn"); const userNameLabel = document.getElementById("userName"); @@ -18,9 +22,166 @@ document.addEventListener("DOMContentLoaded", async () => { const TOKEN_EXPIRE_MS = 24 * 60 * 60 * 1000; + // === AnythingLLM 配置 === + const LLM_PASSWORD = "lukeye@6"; + const LLM_TOKEN_API = "http://llm.wisdompulse.cn/api/request-token"; + const LLM_WORKSPACES_API = "http://llm.wisdompulse.cn/api/workspaces"; + const LLM_STREAM_API_BASE = "http://llm.wisdompulse.cn/api/workspace"; + const LLM_TOKEN_EXPIRE_MS = 23 * 60 * 60 * 1000; + + // 获取 LLM Token + async function getLLMToken(forceRefresh = false) { + return new Promise((resolve, reject) => { + chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => { + const now = Date.now(); + if ( + !forceRefresh && + res.llmToken && + res.llmTokenAt && + now - res.llmTokenAt < LLM_TOKEN_EXPIRE_MS + ) { + console.log("LLM token 命中缓存,有效期内"); + resolve(res.llmToken); + } else { + try { + console.log("开始请求 LLM token,POST", LLM_TOKEN_API, "参数:", { password: LLM_PASSWORD }); + const response = await fetch(LLM_TOKEN_API, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password: LLM_PASSWORD }), + }); + const data = await response.json(); + console.log("LLM token 接口返回:", data); + if (data.token) { + chrome.storage.local.set({ llmToken: data.token, llmTokenAt: now }); + console.log("LLM token 已缓存"); + resolve(data.token); + } else { + console.log("LLM token 获取失败,响应内容:", data); + reject("LLM token获取失败"); + } + } catch (e) { + console.log("LLM token 请求异常:", e); + reject("LLM token接口异常"); + } + } + }); + }); + } + + // 获取 LLM workspaceSlug(修正) + async function getLLMWorkspaceSlug(token, forceRefresh = false) { + return new Promise((resolve, reject) => { + chrome.storage.local.get(["llmWorkspaceSlug", "llmWorkspaceAt"], async (res) => { + const now = Date.now(); + if ( + !forceRefresh && + res.llmWorkspaceSlug && + res.llmWorkspaceAt && + now - res.llmWorkspaceAt < LLM_TOKEN_EXPIRE_MS + ) { + console.log("LLM workspaceSlug 命中缓存,有效期内:", res.llmWorkspaceSlug); + resolve(res.llmWorkspaceSlug); + } else { + try { + console.log("开始请求 LLM 工作区列表,GET", LLM_WORKSPACES_API, "Authorization: Bearer", token); + const response = await fetch(LLM_WORKSPACES_API, { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + console.log("LLM 工作区接口返回:", data); + if (data.workspaces && Array.isArray(data.workspaces) && data.workspaces.length > 0 && data.workspaces[0].slug) { + chrome.storage.local.set({ + llmWorkspaceSlug: data.workspaces[0].slug, + llmWorkspaceAt: now, + }); + console.log("LLM workspaceSlug 已缓存:", data.workspaces[0].slug); + resolve(data.workspaces[0].slug); + } else { + console.log("未获取到有效的 LLM 工作区 slug,响应内容:", data); + reject("未获取到LLM工作区slug"); + } + } catch (e) { + console.log("LLM 工作区接口请求异常:", e); + reject("LLM工作区接口异常"); + } + } + }); + }); + } + + // 调用 AnythingLLM stream-chat 并流式渲染 + async function fetchLLMAnswer(question) { + llmResultBox.innerHTML = ""; + llmResultContainer.classList.remove("hidden"); + try { + console.log("开始获取 LLM token 和 workspaceSlug..."); + const llmToken = await getLLMToken(); + const workspaceSlug = await getLLMWorkspaceSlug(llmToken); + + const url = `${LLM_STREAM_API_BASE}/${workspaceSlug}/stream-chat`; + console.log("开始请求 LLM stream-chat,POST", url, "参数:", { message: question, attachments: [] }); + 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) { + console.log("LLM stream-chat 请求失败,状态:", res.status); + llmResultBox.innerHTML = "LLM接口请求失败"; + return; + } + const reader = res.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let done = false; + let stop = false; + 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); + console.log("LLM 返回片段:", chunk); + if (chunk.type === "textResponseChunk") { + if (chunk.close) { + stop = true; + break; + } + if (chunk.textResponse) { + llmResultBox.innerHTML += chunk.textResponse.replace(/\n/g, "
"); + llmResultBox.scrollTop = llmResultBox.scrollHeight; + } + } + } catch (e) { + console.log("LLM 片段解析异常:", e, "行内容:", line); + } + } + } + } + if (stop) break; + } + console.log("LLM stream-chat 已完成。"); + } catch (e) { + console.log("LLM stream-chat 全流程异常:", e); + llmResultBox.innerHTML = `LLM请求异常:${e}`; + llmResultContainer.classList.remove("hidden"); + } + } + function log(...args) { const version = chrome?.runtime?.getManifest?.().version || "dev"; - console.log(`[v${version}]`, ...args); + console.log("[v" + version + "]", ...args); } async function loadToken() { @@ -48,13 +209,14 @@ document.addEventListener("DOMContentLoaded", async () => { }); } - function showLoading() { resultBox.innerHTML = "
  • 加载中...
  • "; } function clearResults() { resultBox.innerHTML = ""; + llmResultBox.innerHTML = ""; + llmResultContainer.classList.add("hidden"); historyWrapper.classList.add("hidden"); } @@ -181,7 +343,7 @@ document.addEventListener("DOMContentLoaded", async () => { renderPagination(); } catch (err) { - console.error("搜索请求失败:", err); + console.log("搜索请求失败:", err); resultBox.innerHTML = "
  • 搜索失败,请检查网络或服务状态。
  • "; } } @@ -195,6 +357,9 @@ document.addEventListener("DOMContentLoaded", async () => { if (!val) return clearResults(); const keywords = val.split(/\s+/); updateHistory(val); + + // 并发触发 AnythingLLM + ES + fetchLLMAnswer(val); fetchResults(keywords, 1); } @@ -203,7 +368,7 @@ document.addEventListener("DOMContentLoaded", async () => { searchBtn.addEventListener("click", triggerSearch); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { - log("用户按下回车 ⏎"); + log("用户按下回车"); triggerSearch(); } }); diff --git a/pages/styles.css b/pages/styles.css index b0de8e9..bd63db1 100644 --- a/pages/styles.css +++ b/pages/styles.css @@ -269,4 +269,28 @@ h1 { display: none; } +.llm-result-container { + margin-bottom: 14px; + max-height: 140px; + overflow-y: auto; + border: 1px solid #e0e9f4; + border-radius: 7px; + padding: 8px 12px; + background: #f8fbff; +} + +.llm-title { + font-weight: bold; + color: #2453b5; + margin-bottom: 4px; + font-size: 14px; +} + +#llmResult { + font-size: 14px; + color: #233; + line-height: 1.7; + word-break: break-all; + white-space: pre-wrap; +}