feat(knowledge-base): 增加全局知识库管理

This commit is contained in:
2026-06-08 21:37:32 +08:00
parent e6fa738fd5
commit 5ecf78c5d6
12 changed files with 1425 additions and 2 deletions

238
static/js/knowledge_base.js Normal file
View File

@@ -0,0 +1,238 @@
(function () {
var page = document.querySelector(".knowledge-page");
if (!page) {
return;
}
var documentForm = document.getElementById("knowledgeDocumentForm");
var documentStatus = document.getElementById("knowledgeDocumentStatus");
var documentTable = document.getElementById("knowledgeDocumentTable");
var documentSearch = document.getElementById("knowledgeDocumentSearch");
var searchForm = document.getElementById("knowledgeSearchForm");
var queryInput = document.getElementById("knowledgeSearchQuery");
var results = document.getElementById("knowledgeSearchResults");
var sourceSearch = document.getElementById("knowledgeSourceSearch");
var sourceTable = document.getElementById("knowledgeSourceTable");
var documentFileInput = document.getElementById("knowledgeDocumentFile");
var uploadDropzone = document.getElementById("knowledgeUploadDropzone");
function csrfToken() {
var cookie = document.cookie.split("; ").find(function (item) {
return item.indexOf("csrftoken=") === 0;
});
return cookie ? decodeURIComponent(cookie.split("=")[1]) : "";
}
function escapeHtml(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async function patchDocument(row, payload) {
var response = await fetch(row.getAttribute("data-detail-url"), {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("知识库材料更新失败。");
}
return response.json();
}
async function deleteDocument(row) {
var response = await fetch(row.getAttribute("data-detail-url"), {
method: "DELETE",
headers: { "X-CSRFToken": csrfToken() },
});
if (!response.ok) {
throw new Error("知识库材料删除失败。");
}
}
async function indexDocument(row) {
var response = await fetch(row.getAttribute("data-index-url"), {
method: "POST",
headers: { "X-CSRFToken": csrfToken() },
});
if (!response.ok) {
throw new Error("知识库材料解析入库失败。");
}
return response.json();
}
function renderResults(payload) {
if (!results) {
return;
}
if (payload.error_message) {
results.innerHTML = '<p class="knowledge-search-error">' + escapeHtml(payload.error_message) + "</p>";
return;
}
if (!payload.results || !payload.results.length) {
results.innerHTML = '<p class="panel-empty">未检索到依据片段。</p>';
return;
}
results.innerHTML = payload.results
.map(function (item, index) {
return [
'<article class="knowledge-result">',
"<header><strong>结果 " + (index + 1) + "</strong><span>" + escapeHtml(item.source || "法规材料") + "</span></header>",
"<p>" + escapeHtml(item.text || "").slice(0, 600) + "</p>",
item.score === null || item.score === undefined ? "" : "<em>score: " + escapeHtml(item.score) + "</em>",
"</article>",
].join("");
})
.join("");
}
if (documentForm) {
documentForm.addEventListener("submit", async function (event) {
event.preventDefault();
var formData = new FormData(documentForm);
if (documentStatus) {
documentStatus.textContent = "上传并解析入库中...";
}
try {
var response = await fetch(page.getAttribute("data-document-url"), {
method: "POST",
headers: { "X-CSRFToken": csrfToken() },
body: formData,
});
if (!response.ok) {
throw new Error("新增材料失败。");
}
window.location.reload();
} catch (error) {
if (documentStatus) {
documentStatus.textContent = error.message || "新增材料失败。";
}
}
});
}
if (documentFileInput && documentStatus) {
documentFileInput.addEventListener("change", function () {
var file = documentFileInput.files && documentFileInput.files[0];
documentStatus.textContent = file
? "已选择:" + file.name
: "上传后会进入当前账号的全局知识库。";
});
}
if (uploadDropzone && documentFileInput) {
uploadDropzone.addEventListener("click", function () {
documentFileInput.click();
});
uploadDropzone.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
documentFileInput.click();
}
});
["dragenter", "dragover"].forEach(function (eventName) {
uploadDropzone.addEventListener(eventName, function (event) {
event.preventDefault();
uploadDropzone.classList.add("dragging");
});
});
["dragleave", "drop"].forEach(function (eventName) {
uploadDropzone.addEventListener(eventName, function (event) {
event.preventDefault();
uploadDropzone.classList.remove("dragging");
});
});
uploadDropzone.addEventListener("drop", function (event) {
var files = event.dataTransfer && event.dataTransfer.files;
if (!files || !files.length) {
return;
}
documentFileInput.files = files;
documentFileInput.dispatchEvent(new Event("change", { bubbles: true }));
});
}
if (documentTable) {
documentTable.addEventListener("click", async function (event) {
var button = event.target.closest("[data-kb-action]");
if (!button) {
return;
}
var row = button.closest("tr[data-document-id]");
if (!row) {
return;
}
var action = button.getAttribute("data-kb-action");
try {
if (action === "edit") {
var nameCell = row.querySelector(".attachment-name");
var nextName = window.prompt("请输入新的材料名称", nameCell ? nameCell.textContent.trim() : "");
if (nextName) {
await patchDocument(row, { display_name: nextName });
window.location.reload();
}
} else if (action === "toggle") {
await patchDocument(row, { is_active: button.textContent.trim() === "启用" });
window.location.reload();
} else if (action === "index") {
button.disabled = true;
button.textContent = "解析中";
await indexDocument(row);
window.location.reload();
} else if (action === "delete" && window.confirm("确认删除该知识库材料?")) {
await deleteDocument(row);
window.location.reload();
}
} catch (error) {
window.alert(error.message || "知识库材料操作失败。");
}
});
}
if (searchForm && queryInput) {
searchForm.addEventListener("submit", async function (event) {
event.preventDefault();
var query = queryInput.value.trim();
if (!query) {
renderResults({ error_message: "请输入检索问题。" });
return;
}
results.innerHTML = '<p class="panel-empty">检索中...</p>';
try {
var response = await fetch(page.getAttribute("data-search-url"), {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken(),
},
body: JSON.stringify({ query: query }),
});
renderResults(await response.json());
} catch (error) {
renderResults({ error_message: "检索失败,请稍后重试。" });
}
});
}
function bindTableSearch(input, table, selector) {
if (!input || !table) {
return;
}
input.addEventListener("input", function () {
var keyword = input.value.trim().toLowerCase();
table.querySelectorAll(selector).forEach(function (row) {
row.hidden = keyword && row.textContent.toLowerCase().indexOf(keyword) === -1;
});
});
}
bindTableSearch(documentSearch, documentTable, "tbody tr[data-document-id]");
bindTableSearch(sourceSearch, sourceTable, "tbody tr[data-source-name]");
})();