This commit is contained in:
luke 2025-06-24 18:17:51 +08:00
parent ddd94b3681
commit dd6aebf566
2 changed files with 69 additions and 57 deletions

View File

@ -61,19 +61,11 @@ const defaultConfig = {
{ label: "Token 字段", field: "token" }
]
},
workspaces: {
name: "获取工作区接口",
url: "http://llm.wisdompulse.cn/api/workspaces",
requestParams: [],
responseParams: [
{ label: "工作区数组字段", field: "workspaces" }
]
},
llmChat: {
name: "指定工作区对话接口",
url: "http://llm.wisdompulse.cn/api/workspace/${workId}/stream-chat",
url: "http://app-test.wisdompulse.cn/api/v1/llm/ask",
requestParams: [
{ label: "问题字段", field: "message" },
{ label: "问题字段", field: "question" },
{ label: "历史字段", field: "attachments" }
],
responseParams: []

View File

@ -226,7 +226,7 @@ function insertCopyFullAnswerBtn(msgBox, answerStr) {
function promptLLMPasswordModal() {
return new Promise((resolve) => {
// 避免重复弹窗
if (document.getElementById("llm-password-modal")) return;
if (document.getElementById("llm-password-modal")) return resolve("");
// 创建遮罩层
const mask = document.createElement("div");
@ -275,45 +275,53 @@ function promptLLMPasswordModal() {
async function getLLMToken(forceRefresh = false) {
const config = await loadMergedConfig();
const tokenCfg = config.apiConfig.llmToken;
const expireMs = config.llmTokenExpireHours * 60 * 60 * 1000;
// 检查缓存
return new Promise((resolve, reject) => {
chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => {
const now = Date.now();
const mergedConfig = await loadMergedConfig();
const expireMs = mergedConfig.llmTokenExpireHours * 60 * 60 * 1000;
if (!forceRefresh && res.llmToken && res.llmTokenAt && now - res.llmTokenAt < expireMs) {
log("已从缓存中获取到llmToken. expireMs: " + expireMs);
resolve(res.llmToken);
} else {
try {
// 这里不再用 getLLMPassword(),而是直接弹窗
let pwd = await promptLLMPasswordModal();
if (!pwd) {
reject("未输入密码,无法获取 LLM Token");
return;
}
// 可选:此处不自动存储 llmPassword提升安全性
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 });
log("请求llmToken成功");
resolve(data[tokenField]);
} else {
log("LLM token获取失败");
reject("LLM token获取失败");
}
} catch (e) {
reject("LLM token接口异常");
try {
const now = Date.now();
if (!forceRefresh && res.llmToken && res.llmTokenAt && now - res.llmTokenAt < expireMs) {
return resolve(res.llmToken);
}
// 弹窗输入密码
let pwd = await promptLLMPasswordModal();
if (!pwd) return reject("未输入密码,无法获取 LLM Token");
// 构造请求
const body = {};
body[tokenCfg.requestParams[0].field] = pwd;
let response, text, data;
try {
response = await fetch(tokenCfg.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "test"
},
body: JSON.stringify(body)
});
text = await response.text();
} catch (e) {
return reject("LLM token接口网络异常: " + e);
}
try {
data = JSON.parse(text);
} catch (e) {
return reject("LLM token接口返回非JSON: " + text);
}
const tokenParam = tokenCfg.responseParams.find(p => p.label && p.label.includes("token"));
const tokenField = tokenParam ? tokenParam.field : "token";
if (data[tokenField]) {
chrome.storage.local.set({ llmToken: data[tokenField], llmTokenAt: now });
return resolve(data[tokenField]);
} else {
return reject("LLM token获取失败响应内容: " + text);
}
} catch (e) {
return reject("getLLMToken代码异常: " + e);
}
});
});
@ -322,15 +330,25 @@ async function getLLMToken(forceRefresh = false) {
async function fetchLLMAnswer(question, container, contentDiv) {
if (!question || !question.trim()) return;
// 获取Token关键是加try-catch
let llmToken;
try {
llmToken = await getLLMToken();
} catch (e) {
alert("LLM Token 获取失败:" + e);
return;
}
const config = await loadMergedConfig();
const llmToken = await getLLMToken();
const auth = await loadUserToken();
if (!auth) {
return handleLogout(auth);
handleLogout(auth);
return;
}
const userToken = auth.token;
const url = "http://app-test.wisdompulse.cn/api/v1/llm/ask";
const url = config.apiConfig.llmChat.url;
// ... 你的渲染用户输入的代码 ...
const userItem = document.createElement("div");
userItem.className = "llm-msg-item llm-user";
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${question}`;
@ -344,6 +362,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
let collectedSources = [];
let jumpMap = {};
// LLM 问答接口流式处理
try {
const body = { llmToken, question };
@ -351,6 +370,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
method: "POST",
headers: {
"Authorization": `${userToken}`,
"Accept": "text/event-stream",
"Content-Type": "application/json"
},
body: JSON.stringify(body)
@ -372,7 +392,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
if (value) {
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop(); // 留给下次未完整数据
buffer = lines.pop(); // 留给下次未完整数据
for (let line of lines) {
line = line.trim();
@ -380,7 +400,6 @@ async function fetchLLMAnswer(question, container, contentDiv) {
const chunkStr = line.slice(5).trim();
if (!chunkStr) continue;
let chunk;
try {
chunk = JSON.parse(chunkStr);
@ -389,13 +408,13 @@ async function fetchLLMAnswer(question, container, contentDiv) {
continue;
}
// 收集 sources(只更新,不渲染)
// 收集 sources
if (chunk.sources) {
collectedSources = chunk.sources;
}
// 累积内容
let content = chunk.textResponse || "";
// 只要有 textResponse 就累加
let content = chunk.textResponse ?? "";
if (content.includes("<think>")) {
inThink = true;
content = content.replace("<think>", "");
@ -411,7 +430,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
answerStr += content;
}
// 渲染回答markdown 支持
// 每次都要渲染(不管内容是否为空
let html = "";
try {
html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(answerStr)) : answerStr.replace(/\n/g, "<br>");
@ -427,7 +446,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
insertCopyFullAnswerBtn(msgBox, answerStr);
contentDiv.scrollTop = contentDiv.scrollHeight;
// 关闭信号触发最终引用渲染
// 收尾渲染引用
if (chunk.close === true) {
if (collectedSources.length > 0) {
const titles = collectedSources.map(s => s.title).filter(Boolean);
@ -446,6 +465,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
}
} catch (e) {
msgBox.innerHTML += `<span style='color:red;'>LLM异常${e}</span>`;
console.error("LLM问答流式接口异常", e);
}
}