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 {}; return new Promise((resolveOuter) => { chrome.storage.local.get(["token"], async (res) => { const token = res.token; if (!token) { console.warn("resolveJumpLinks 缺少 token,跳过"); return resolveOuter({}); } try { const res = await fetch("http://app.wisdompulse.cn/search/file-path", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": token }, body: JSON.stringify({ fileNames: titles }) }); const data = await res.json(); 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}`; } } resolveOuter(map); } catch (e) { console.warn("resolveJumpLinks 接口异常:", e); resolveOuter({}); } }); }); } 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; wrapper.querySelectorAll('pre > code').forEach(codeEl => { const pre = codeEl.parentElement; if (pre.querySelector('.copy-btn')) return; const btn = document.createElement('button'); btn.className = 'copy-btn'; btn.innerText = '复制'; pre.appendChild(btn); pre.style.position = 'relative'; }); return wrapper.innerHTML; } // 渲染后批量绑定复制事件 function bindAllCopyButtons(container) { (container || document).querySelectorAll('.llm-md .copy-btn').forEach(btn => { // 避免重复绑定 if (btn.dataset.bind === "true") return; btn.dataset.bind = "true"; btn.addEventListener('click', function (e) { e.stopPropagation(); // 只复制紧邻 code 元素的内容 const codeEl = btn.parentElement.querySelector('code'); const codeText = codeEl ? (codeEl.innerText || codeEl.textContent) : ""; let success = false, error = null; try { const textarea = document.createElement("textarea"); textarea.value = codeText; textarea.setAttribute('readonly', ''); textarea.style.position = "absolute"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); textarea.select(); success = document.execCommand('copy'); document.body.removeChild(textarea); if (success) { btn.innerText = '已复制!'; setTimeout(() => btn.innerText = '复制', 1200); } else { btn.innerText = '失败'; setTimeout(() => btn.innerText = '复制', 1200); } } catch (err) { error = err; btn.innerText = '失败'; setTimeout(() => btn.innerText = '复制', 1200); } // 必然有日志 if (success) { log("[复制代码] 成功,内容:", codeText); } else { console.error("[复制代码] 失败", error, "内容:", codeText); alert("复制失败: " + (error ? error : "未知错误")); } }); }); } // fetchLLMAnswer,支持流式markdown、代码块复制按钮 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; let collectedSources = []; 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); // 收集 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); } } } 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; // 直接累计 markdown 原文 } // 渲染 markdown,代码块加复制按钮 let html = ""; try { html = window.marked ? marked.parse(answerStr) : answerStr.replace(/\n/g, "
"); } catch (e) { html = answerStr.replace(/\n/g, "
"); } msgBox.innerHTML = (thinkStr ? `
${thinkStr}
` : "") + `智能助手:
${wrapHtmlWithCopyBtn(html)}
`; // 【关键】渲染后绑定复制事件 bindAllCopyButtons(msgBox); contentDiv.scrollTop = contentDiv.scrollHeight; } } } } if (collectedSources.length > 0) { 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); } } catch (e) { msgBox.innerHTML += `LLM异常:${e}`; } } async function renderSources(sourceList, sources) { const titles = sources.map(s => s.title).filter(Boolean); const jumpMap = await resolveJumpLinks(titles); sourceList.style.marginTop = "12px"; sourceList.style.borderTop = "1px solid #dbe3f3"; sourceList.style.paddingTop = "7px"; const titleBar = document.createElement("div"); titleBar.textContent = "📎 参考资料"; titleBar.style.fontWeight = "bold"; titleBar.style.marginBottom = "6px"; titleBar.style.fontSize = "13.5px"; titleBar.style.color = "#0d2fec"; sourceList.appendChild(titleBar); // 新增ul包裹 const ul = document.createElement("ul"); ul.style.paddingLeft = "1.2em"; ul.style.margin = "0"; ul.style.listStyle = "disc"; ul.style.fontSize = "14px"; for (const src of sources) { const url = jumpMap[src.title]; const li = document.createElement("li"); li.style.marginBottom = "3px"; li.style.lineHeight = "1.7"; // 分值 let scoreText = ""; if (typeof src.score === "number") { scoreText = `(相似度:${(src.score * 100).toFixed(2)}%)`; } if (url) { const link = document.createElement("a"); link.href = url; link.textContent = src.title || "未命名文档"; link.target = "_blank"; link.style.color = "#2560de"; link.style.textDecoration = "none"; link.onmouseenter = () => (link.style.textDecoration = "underline"); link.onmouseleave = () => (link.style.textDecoration = "none"); li.appendChild(link); if (scoreText) { const scoreSpan = document.createElement("span"); scoreSpan.style.color = "#8990a1"; scoreSpan.style.fontSize = "12.5px"; scoreSpan.style.marginLeft = "6px"; scoreSpan.textContent = scoreText; li.appendChild(scoreSpan); } } else { li.textContent = (src.title || "未命名文档") + (scoreText ? " " + scoreText : ""); li.style.color = "#666"; } ul.appendChild(li); } sourceList.appendChild(ul); }