feat(knowledge): add FAQ Excel import export

This commit is contained in:
mlogclub
2026-06-01 13:57:49 +08:00
parent d8692b45cd
commit 8933e963f5
15 changed files with 1133 additions and 363 deletions
+53 -1
View File
@@ -1,5 +1,5 @@
import { readSession } from "@/lib/auth"
import { request } from "@/lib/api/client"
import { request, requestBlob } from "@/lib/api/client"
import { createWebSocketBaseUrl } from "@/lib/api/websocket"
import { translateCurrentMessage } from "@/i18n/messages"
@@ -1524,6 +1524,22 @@ export type UpdateKnowledgeFAQPayload = CreateKnowledgeFAQPayload & {
id: number
}
export type KnowledgeFAQImportMode = "append" | "overwrite"
export type KnowledgeFAQImportError = {
row: number
message: string
}
export type KnowledgeFAQImportResult = {
total: number
created: number
updated: number
skipped: number
failed: number
errors: KnowledgeFAQImportError[]
}
export function fetchKnowledgeBases(
query?: Record<string, string | number | undefined>
) {
@@ -1652,6 +1668,42 @@ export function deleteKnowledgeFAQ(id: number) {
})
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
export async function downloadKnowledgeFAQImportTemplate() {
const result = await requestBlob("/api/dashboard/knowledge-faq/import_template")
downloadBlob(result.blob, result.filename || "knowledge-faq-import-template.xlsx")
}
export async function exportKnowledgeFAQs(knowledgeBaseId: number) {
const result = await requestBlob(
`/api/dashboard/knowledge-faq/export${toQueryString({ knowledgeBaseId })}`
)
downloadBlob(result.blob, result.filename || `knowledge-faq-${knowledgeBaseId}.xlsx`)
}
export function importKnowledgeFAQs(payload: {
knowledgeBaseId: number
mode: KnowledgeFAQImportMode
file: File
}) {
const formData = new FormData()
formData.set("knowledgeBaseId", String(payload.knowledgeBaseId))
formData.set("mode", payload.mode)
formData.set("file", payload.file)
return request<KnowledgeFAQImportResult>("/api/dashboard/knowledge-faq/import", {
method: "POST",
body: formData,
})
}
export function buildKnowledgeFAQIndex(faqId: number) {
return request<void>("/api/dashboard/knowledge-retrieve/build", {
method: "POST",
+65 -9
View File
@@ -18,6 +18,11 @@ type RequestOptions = RequestInit & {
onResponse?: (response: Response) => void
}
export type BlobResponse = {
blob: Blob
filename: string
}
async function parseResult<T>(response: Response) {
const payload = (await response.json()) as JsonResult<T>
if (!response.ok || !payload.success) {
@@ -31,13 +36,7 @@ async function parseResult<T>(response: Response) {
return payload.data
}
export async function request<T>(
path: string,
options: RequestOptions = {}
): Promise<T> {
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
delete (rest as RequestOptions).baseUrl
delete (rest as RequestOptions).onResponse
function buildRequestHeaders(headers: HeadersInit | undefined, skipAuth?: boolean, body?: BodyInit | null) {
const session = readSession()
const authHeaders = new Headers(headers)
@@ -46,14 +45,37 @@ export async function request<T>(
}
if (
!authHeaders.has("Content-Type") &&
rest.body &&
!(typeof FormData !== "undefined" && rest.body instanceof FormData)
body &&
!(typeof FormData !== "undefined" && body instanceof FormData)
) {
authHeaders.set("Content-Type", "application/json")
}
const locale = readStoredLocale()
authHeaders.set("Accept-Language", locale)
authHeaders.set("X-Locale", locale)
return authHeaders
}
function parseFilename(contentDisposition: string | null) {
if (!contentDisposition) {
return ""
}
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8Match?.[1]) {
return decodeURIComponent(utf8Match[1])
}
const match = contentDisposition.match(/filename="?([^";]+)"?/i)
return match?.[1] ? decodeURIComponent(match[1]) : ""
}
export async function request<T>(
path: string,
options: RequestOptions = {}
): Promise<T> {
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
delete (rest as RequestOptions).baseUrl
delete (rest as RequestOptions).onResponse
const authHeaders = buildRequestHeaders(headers, skipAuth, rest.body)
const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL
const response = await fetch(`${requestBaseUrl}${path}`, {
@@ -65,3 +87,37 @@ export async function request<T>(
return parseResult<T>(response)
}
export async function requestBlob(
path: string,
options: RequestOptions = {}
): Promise<BlobResponse> {
const { headers, skipAuth, baseUrl, onResponse, ...rest } = options
delete (rest as RequestOptions).baseUrl
delete (rest as RequestOptions).onResponse
const authHeaders = buildRequestHeaders(headers, skipAuth, rest.body)
const requestBaseUrl = baseUrl !== undefined ? baseUrl : API_BASE_URL
const response = await fetch(`${requestBaseUrl}${path}`, {
...rest,
headers: authHeaders,
cache: "no-store",
})
onResponse?.(response)
const contentType = response.headers.get("Content-Type") ?? ""
if (contentType.includes("application/json")) {
try {
await parseResult<never>(response)
} catch (error) {
throw error
}
throw new Error(translateCurrentMessage("api.requestFailed"))
}
if (!response.ok) {
throw new Error(response.statusText || translateCurrentMessage("api.requestFailed"))
}
return {
blob: await response.blob(),
filename: parseFilename(response.headers.get("Content-Disposition")),
}
}