fix
This commit is contained in:
parent
ddd94b3681
commit
dd6aebf566
@ -61,19 +61,11 @@ const defaultConfig = {
|
|||||||
{ label: "Token 字段", field: "token" }
|
{ label: "Token 字段", field: "token" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
workspaces: {
|
|
||||||
name: "获取工作区接口",
|
|
||||||
url: "http://llm.wisdompulse.cn/api/workspaces",
|
|
||||||
requestParams: [],
|
|
||||||
responseParams: [
|
|
||||||
{ label: "工作区数组字段", field: "workspaces" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
llmChat: {
|
llmChat: {
|
||||||
name: "指定工作区对话接口",
|
name: "指定工作区对话接口",
|
||||||
url: "http://llm.wisdompulse.cn/api/workspace/${workId}/stream-chat",
|
url: "http://app-test.wisdompulse.cn/api/v1/llm/ask",
|
||||||
requestParams: [
|
requestParams: [
|
||||||
{ label: "问题字段", field: "message" },
|
{ label: "问题字段", field: "question" },
|
||||||
{ label: "历史字段", field: "attachments" }
|
{ label: "历史字段", field: "attachments" }
|
||||||
],
|
],
|
||||||
responseParams: []
|
responseParams: []
|
||||||
|
|||||||
114
scripts/llm.js
114
scripts/llm.js
@ -226,7 +226,7 @@ function insertCopyFullAnswerBtn(msgBox, answerStr) {
|
|||||||
function promptLLMPasswordModal() {
|
function promptLLMPasswordModal() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// 避免重复弹窗
|
// 避免重复弹窗
|
||||||
if (document.getElementById("llm-password-modal")) return;
|
if (document.getElementById("llm-password-modal")) return resolve("");
|
||||||
|
|
||||||
// 创建遮罩层
|
// 创建遮罩层
|
||||||
const mask = document.createElement("div");
|
const mask = document.createElement("div");
|
||||||
@ -275,45 +275,53 @@ function promptLLMPasswordModal() {
|
|||||||
async function getLLMToken(forceRefresh = false) {
|
async function getLLMToken(forceRefresh = false) {
|
||||||
const config = await loadMergedConfig();
|
const config = await loadMergedConfig();
|
||||||
const tokenCfg = config.apiConfig.llmToken;
|
const tokenCfg = config.apiConfig.llmToken;
|
||||||
|
const expireMs = config.llmTokenExpireHours * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => {
|
chrome.storage.local.get(["llmToken", "llmTokenAt"], async (res) => {
|
||||||
const now = Date.now();
|
try {
|
||||||
const mergedConfig = await loadMergedConfig();
|
const now = Date.now();
|
||||||
const expireMs = mergedConfig.llmTokenExpireHours * 60 * 60 * 1000;
|
if (!forceRefresh && res.llmToken && res.llmTokenAt && now - res.llmTokenAt < expireMs) {
|
||||||
if (!forceRefresh && res.llmToken && res.llmTokenAt && now - res.llmTokenAt < expireMs) {
|
return resolve(res.llmToken);
|
||||||
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接口异常");
|
|
||||||
}
|
}
|
||||||
|
// 弹窗输入密码
|
||||||
|
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) {
|
async function fetchLLMAnswer(question, container, contentDiv) {
|
||||||
if (!question || !question.trim()) return;
|
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 config = await loadMergedConfig();
|
||||||
const llmToken = await getLLMToken();
|
|
||||||
const auth = await loadUserToken();
|
const auth = await loadUserToken();
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return handleLogout(auth);
|
handleLogout(auth);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const userToken = auth.token;
|
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");
|
const userItem = document.createElement("div");
|
||||||
userItem.className = "llm-msg-item llm-user";
|
userItem.className = "llm-msg-item llm-user";
|
||||||
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${question}`;
|
userItem.innerHTML = `<span style="color:#2a63c8;font-weight:bold;">我:</span> ${question}`;
|
||||||
@ -344,6 +362,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
|
|
||||||
let collectedSources = [];
|
let collectedSources = [];
|
||||||
let jumpMap = {};
|
let jumpMap = {};
|
||||||
|
// LLM 问答接口流式处理
|
||||||
try {
|
try {
|
||||||
const body = { llmToken, question };
|
const body = { llmToken, question };
|
||||||
|
|
||||||
@ -351,6 +370,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `${userToken}`,
|
"Authorization": `${userToken}`,
|
||||||
|
"Accept": "text/event-stream",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
@ -372,7 +392,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
if (value) {
|
if (value) {
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = buffer.split("\n\n");
|
const lines = buffer.split("\n\n");
|
||||||
buffer = lines.pop(); // 留给下次读未完整数据
|
buffer = lines.pop(); // 留给下次未完整数据
|
||||||
|
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
@ -380,7 +400,6 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
|
|
||||||
const chunkStr = line.slice(5).trim();
|
const chunkStr = line.slice(5).trim();
|
||||||
if (!chunkStr) continue;
|
if (!chunkStr) continue;
|
||||||
|
|
||||||
let chunk;
|
let chunk;
|
||||||
try {
|
try {
|
||||||
chunk = JSON.parse(chunkStr);
|
chunk = JSON.parse(chunkStr);
|
||||||
@ -389,13 +408,13 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集 sources(只更新,不渲染)
|
// 收集 sources
|
||||||
if (chunk.sources) {
|
if (chunk.sources) {
|
||||||
collectedSources = chunk.sources;
|
collectedSources = chunk.sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 累积内容
|
// 只要有 textResponse 就累加
|
||||||
let content = chunk.textResponse || "";
|
let content = chunk.textResponse ?? "";
|
||||||
if (content.includes("<think>")) {
|
if (content.includes("<think>")) {
|
||||||
inThink = true;
|
inThink = true;
|
||||||
content = content.replace("<think>", "");
|
content = content.replace("<think>", "");
|
||||||
@ -411,7 +430,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
answerStr += content;
|
answerStr += content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染回答(markdown 支持)
|
// 每次都要渲染(不管内容是否为空)
|
||||||
let html = "";
|
let html = "";
|
||||||
try {
|
try {
|
||||||
html = window.marked ? wrapHtmlWithCopyBtn(marked.parse(answerStr)) : answerStr.replace(/\n/g, "<br>");
|
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);
|
insertCopyFullAnswerBtn(msgBox, answerStr);
|
||||||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||||||
|
|
||||||
// 关闭信号触发最终引用渲染
|
// 收尾渲染引用
|
||||||
if (chunk.close === true) {
|
if (chunk.close === true) {
|
||||||
if (collectedSources.length > 0) {
|
if (collectedSources.length > 0) {
|
||||||
const titles = collectedSources.map(s => s.title).filter(Boolean);
|
const titles = collectedSources.map(s => s.title).filter(Boolean);
|
||||||
@ -446,6 +465,7 @@ async function fetchLLMAnswer(question, container, contentDiv) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
msgBox.innerHTML += `<span style='color:red;'>LLM异常:${e}</span>`;
|
||||||
|
console.error("LLM问答流式接口异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user