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 renderSources(sourceList, sources) { const titles = sources.map(s => s.title).filter(Boolean); const jumpMap = await resolveJumpLinks(titles); sourceList.style.marginTop = "16px"; sourceList.style.borderTop = "1px solid #dbe3f3"; sourceList.style.paddingTop = "10px"; 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); for (const src of sources) { const url = jumpMap[src.title]; const line = document.createElement("div"); line.style.marginTop = "4px"; 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"); line.appendChild(link); } else { line.textContent = src.title || "未命名文档"; line.style.color = "#666"; } sourceList.appendChild(line); } } 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; 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.replace(/\n/g, "
"); } msgBox.innerHTML = (thinkStr ? `
${thinkStr}
` : "") + `智能助手:${answerStr}`; 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}`; } }