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("
");
} 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 ? `