diff --git a/manifest.json b/manifest.json index cea2096..f5d4cb1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "渠道应用开发部知识库", - "version": "2.0.1", + "version": "2.0.0", "description": "通过关键词快速搜索内部文档", "permissions": ["storage"], "host_permissions": [ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3ef0410 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ + + +## 修订记录 +| 时间 | 版本 | 修订人 | 说明 | +| ---------- | ------ | ------- | ------------------ | +| 2025.06.02 | V2.0.0 | Luke.Ye | 完成插件配置化改造 | + diff --git a/scripts/llm.js b/scripts/llm.js index 4f24ef5..8179986 100644 --- a/scripts/llm.js +++ b/scripts/llm.js @@ -1,47 +1,48 @@ -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 resolveJumpLinks(titles) { if (!titles || titles.length === 0) return {}; + const config = await loadMergedConfig(); + const filePathCfg = config.apiConfig.filePath; + const publicBase = config.apiConfig.materialPublicUrl.url; + const reqField = filePathCfg.requestParams[0].field; + const respField = filePathCfg.responseParams[0].field; + return new Promise((resolveOuter) => { chrome.storage.local.get(["token"], async (res) => { const token = res.token; - if (!token) { - console.warn("resolveJumpLinks 缺少 token,跳过"); - return resolveOuter({}); - } - + if (!token) return resolveOuter({}); try { - const res = await fetch("http://app.wisdompulse.cn/search/file-path", { + const body = {}; + body[reqField] = titles; + const resp = await fetch(filePathCfg.url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": token }, - body: JSON.stringify({ fileNames: titles }) + body: JSON.stringify(body) }); - const data = await res.json(); + const data = await resp.json(); + const fileMap = data[respField] || {}; const map = {}; - for (const title of Object.keys(data)) { - if (data[title]) { - const cleanPath = data[title].replace(/\.md$/, ".html"); - map[title] = `http://share.wisdompulse.cn/public/${cleanPath}`; + for (const title of Object.keys(fileMap)) { + if (fileMap[title]) { + const cleanPath = fileMap[title].replace(/\.md$/, ".html"); + map[title] = `${publicBase}/${cleanPath}`; } } - resolveOuter(map); } catch (e) { - console.warn("resolveJumpLinks 接口异常:", e); resolveOuter({}); } }); }); } + async function renderSources(sourceList, sources, jumpMap) { sourceList.style.marginTop = "12px"; sourceList.style.borderTop = "1px solid #dbe3f3"; @@ -99,76 +100,10 @@ async function renderSources(sourceList, sources, jumpMap) { ul.appendChild(li); } - sourceList.appendChild(ul); } -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工作区接口异常"); - } - } - }); - }); -} - -// 代码块复制按钮插入 +// === 复制代码块功能 === function wrapHtmlWithCopyBtn(html) { const wrapper = document.createElement('div'); wrapper.innerHTML = html; @@ -215,17 +150,13 @@ function bindAllCopyButtons(container) { btn.innerText = '失败'; setTimeout(() => btn.innerText = '复制', 1200); } - if (success) { - log("[复制代码] 成功,内容:", codeText); - } else { - console.error("[复制代码] 失败", error, "内容:", codeText); + if (!success) { alert("复制失败: " + (error ? error : "未知错误")); } }); }); } -// 复制全部回答按钮,从HTML解析ul/li/a结构 function insertCopyFullAnswerBtn(msgBox, answerStr) { if (msgBox.querySelector('.copy-full-answer-btn')) return; @@ -294,10 +225,92 @@ function insertCopyFullAnswerBtn(msgBox, answerStr) { msgBox.appendChild(copyBtn); } -// 主体流式渲染 fetchLLMAnswer +async function getLLMPassword() { + return new Promise((resolve) => { + chrome.storage.local.get(["llmPassword"], (res) => { + resolve(res.llmPassword || ""); + }); + }); +} + +async function getLLMToken(forceRefresh = false) { + const config = await loadMergedConfig(); + const tokenCfg = config.apiConfig.llmToken; + 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) { + resolve(res.llmToken); + } else { + try { + const pwd = await getLLMPassword(); + const body = {}; + body[tokenCfg.requestParams[0].field] = pwd; + + const response = await fetch(tokenCfg.url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + const tokenField = tokenCfg.responseParams.find(p => p.label.includes("Token")).field; + if (data[tokenField]) { + chrome.storage.local.set({ llmToken: data[tokenField], llmTokenAt: now }); + resolve(data[tokenField]); + } else { + reject("LLM token获取失败"); + } + } catch (e) { + reject("LLM token接口异常"); + } + } + }); + }); +} + +async function getLLMWorkspaceSlug(token, forceRefresh = false) { + const config = await loadMergedConfig(); + const wsCfg = config.apiConfig.workspaces; + const workspacesField = wsCfg.responseParams.find(p => p.field)?.field; + + 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(wsCfg.url, { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + + const data = await response.json(); + const list = data[workspacesField] || []; + if (list.length > 0 && list[0].slug) { + chrome.storage.local.set({ llmWorkspaceSlug: list[0].slug, llmWorkspaceAt: now }); + resolve(list[0].slug); + } else { + reject("未获取到LLM工作区slug"); + } + } catch (e) { + reject("LLM工作区接口异常"); + } + } + }); + }); +} + async function fetchLLMAnswer(question, container, contentDiv) { if (!question || !question.trim()) return; + const config = await loadMergedConfig(); + const llmChatCfg = config.apiConfig.llmChat; + const llmToken = await getLLMToken(); + const workspaceSlug = await getLLMWorkspaceSlug(llmToken); + const url = llmChatCfg.url.replace("${workId}", workspaceSlug); + const userItem = document.createElement("div"); userItem.className = "llm-msg-item llm-user"; userItem.innerHTML = `我: ${question}`; @@ -311,19 +324,18 @@ async function fetchLLMAnswer(question, container, contentDiv) { let collectedSources = []; let jumpMap = {}; - try { - const token = await getLLMToken(); - const workspace = await getLLMWorkspaceSlug(token); - const url = `${LLM_STREAM_API_BASE}/${workspace}/stream-chat`; + const body = {}; + body[llmChatCfg.requestParams[0].field] = question; + body[llmChatCfg.requestParams[1].field] = []; const res = await fetch(url, { method: "POST", headers: { - "Authorization": `Bearer ${token}`, + "Authorization": `Bearer ${llmToken}`, "Content-Type": "application/json" }, - body: JSON.stringify({ message: question, attachments: [] }) + body: JSON.stringify(body) }); if (!res.ok || !res.body) { @@ -345,14 +357,11 @@ async function fetchLLMAnswer(question, container, contentDiv) { buffer = lines.pop(); for (let line of lines) { if (line.startsWith("data: ")) { - const data = line.slice(6); - const chunk = JSON.parse(data); - - // 收集 sources(去重按 id) - if (chunk.sources && Array.isArray(chunk.sources)) { - for (const src of chunk.sources) { - if (!collectedSources.find(s => s.id === src.id)) { - collectedSources.push(src); + const chunk = JSON.parse(line.slice(6)); + if (chunk.sources) { + for (const s of chunk.sources) { + if (!collectedSources.find(x => x.id === s.id)) { + collectedSources.push(s); } } } @@ -369,20 +378,20 @@ async function fetchLLMAnswer(question, container, contentDiv) { if (inThink) { thinkStr += content.replace(/\n/g, "
"); } else { - answerStr += content; // 直接累计 markdown 原文 + answerStr += content; } // 渲染 markdown,代码块加复制按钮 let html = ""; try { - html = window.marked ? marked.parse(answerStr) : answerStr.replace(/\n/g, "
"); + html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(answerStr)) : answerStr.replace(/\n/g, "
"); } catch (e) { html = answerStr.replace(/\n/g, "
"); } msgBox.innerHTML = (thinkStr ? `
${thinkStr}
` : "") + - `智能助手:
${wrapHtmlWithCopyBtn(html)}
`; + `智能助手:
${html}
`; bindAllCopyButtons(msgBox); insertCopyFullAnswerBtn(msgBox, answerStr); @@ -393,22 +402,17 @@ async function fetchLLMAnswer(question, container, contentDiv) { } } - // 渲染参考资料并再次插入复制按钮 if (collectedSources.length > 0) { const titles = collectedSources.map(s => s.title).filter(Boolean); jumpMap = await resolveJumpLinks(titles); const sourceList = document.createElement("div"); sourceList.className = "llm-sources"; - sourceList.style.marginTop = "10px"; - sourceList.style.fontSize = "13px"; - sourceList.style.color = "#555"; msgBox.appendChild(sourceList); await renderSources(sourceList, collectedSources, jumpMap); insertCopyFullAnswerBtn(msgBox, answerStr); } - } catch (e) { msgBox.innerHTML += `LLM异常:${e}`; }