完成配置化改造
This commit is contained in:
parent
e0b6401eab
commit
7cd51825ca
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "渠道应用开发部知识库",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.0",
|
||||
"description": "通过关键词快速搜索内部文档",
|
||||
"permissions": ["storage"],
|
||||
"host_permissions": [
|
||||
|
||||
7
readme.md
Normal file
7
readme.md
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
## 修订记录
|
||||
| 时间 | 版本 | 修订人 | 说明 |
|
||||
| ---------- | ------ | ------- | ------------------ |
|
||||
| 2025.06.02 | V2.0.0 | Luke.Ye | 完成插件配置化改造 |
|
||||
|
||||
228
scripts/llm.js
228
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 = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${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, "<br>");
|
||||
} else {
|
||||
answerStr += content; // 直接累计 markdown 原文
|
||||
answerStr += content;
|
||||
}
|
||||
|
||||
// 渲染 markdown,代码块加复制按钮
|
||||
let html = "";
|
||||
try {
|
||||
html = window.marked ? marked.parse(answerStr) : answerStr.replace(/\n/g, "<br>");
|
||||
html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(answerStr)) : answerStr.replace(/\n/g, "<br>");
|
||||
} catch (e) {
|
||||
html = answerStr.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
msgBox.innerHTML =
|
||||
(thinkStr ? `<div class="llm-think">${thinkStr}</div>` : "") +
|
||||
`<span class="assistant-label">智能助手:</span><div class="llm-md">${wrapHtmlWithCopyBtn(html)}</div>`;
|
||||
`<span class="assistant-label">智能助手:</span><div class="llm-md">${html}</div>`;
|
||||
|
||||
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 += `<span style='color:red;'>LLM异常:${e}</span>`;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user