Defeng fe8f0a875a
Some checks failed
CI / Frontend (Vite) (push) Successful in 24s
CI / Docker Release Tar (push) Has been skipped
CI / Backend (Rust) (push) Failing after 10m15s
Initial project import
2026-07-24 10:28:31 +08:00

735 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const API_BASE = '/api/v1/knowledge'
// 用户认证信息(实际应用中应该从登录获取)
const USER_ID = 'user1'
const USER_NAME = '42tr'
const ROLE = 'admin'
const getHeaders = (contentType = true) => {
const headers = {
'x-user-id': USER_ID,
'x-user-name': USER_NAME,
'x-role': ROLE,
}
if (contentType) {
headers['Content-Type'] = 'application/json'
}
return headers
}
const readErrorMessage = async (response, fallback) => {
try {
const payload = await response.json()
if (payload?.error) return payload.error
if (payload?.message) return payload.message
} catch (_) {
// ignore json parse error
}
return fallback
}
export const api = {
// 搜索
async search(query, kbId = null, fileId = null, options = {}) {
const { advanced = false } = options
let url = `${API_BASE}/search/?query=${encodeURIComponent(query)}`
if (kbId) {
url += `&kb_id=${kbId}`
}
if (fileId) {
url += `&file_id=${fileId}`
}
if (advanced) {
url += '&advanced=true'
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('搜索失败')
const data = await response.json()
return data.results || []
},
advancedSearchStream(params, handlers = {}) {
const {
query,
kbId = null,
fileId = null,
maxSubQueries = 3,
perQueryLimit = 10,
contextChars = 2000,
debug = false,
} = params
const searchParams = new URLSearchParams()
searchParams.set('query', query)
searchParams.set('max_sub_queries', maxSubQueries)
searchParams.set('per_query_limit', perQueryLimit)
searchParams.set('context_chars', contextChars)
if (debug) searchParams.set('debug', 'true')
if (kbId) searchParams.set('kb_id', kbId)
if (fileId) searchParams.set('file_id', fileId)
const controller = new AbortController()
const url = `${API_BASE}/search/advanced/stream?${searchParams.toString()}`
const headers = getHeaders(false)
const decoder = new TextDecoder('utf-8')
const parseEvent = (chunk) => {
const lines = chunk.split('\n')
let eventType = 'message'
let dataLines = []
for (const line of lines) {
const trimmed = line.trim()
if (trimmed.startsWith('event:')) {
eventType = trimmed.slice(6).trim()
} else if (trimmed.startsWith('data:')) {
dataLines.push(trimmed.slice(5).trim())
}
}
const dataStr = dataLines.join('\n')
let parsedData = null
if (dataStr) {
try {
parsedData = JSON.parse(dataStr)
} catch (err) {
parsedData = dataStr
}
}
return { eventType, data: parsedData }
}
const streamPromise = (async () => {
try {
const response = await fetch(url, {
headers,
signal: controller.signal,
})
if (!response.ok) {
throw new Error('高级搜索连接失败')
}
const reader = response.body.getReader()
let buffer = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let boundary
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
const rawEvent = buffer.slice(0, boundary).trim()
buffer = buffer.slice(boundary + 2)
if (!rawEvent) continue
const { eventType, data } = parseEvent(rawEvent)
switch (eventType) {
case 'status':
handlers.onStatus?.(data)
break
case 'plan':
handlers.onPlan?.(data)
break
case 'step':
handlers.onStep?.(data)
break
case 'candidate':
handlers.onCandidate?.(data)
break
case 'filtered':
handlers.onFiltered?.(data)
break
case 'result':
handlers.onResult?.(data)
break
case 'error':
handlers.onErrorEvent?.(data)
break
case 'done':
handlers.onDone?.()
break
default:
handlers.onMessage?.({ eventType, data })
}
}
}
handlers.onComplete?.()
} catch (err) {
if (controller.signal.aborted) {
handlers.onAbort?.()
} else {
handlers.onError?.(err)
}
} finally {
handlers.onFinally?.()
}
})()
return {
cancel: () => controller.abort(),
finished: streamPromise,
}
},
async searchFull(query, kbId = null, fileId = null) {
let url = `${API_BASE}/search/full?query=${encodeURIComponent(query)}`
if (kbId) {
url += `&kb_id=${kbId}`
}
if (fileId) {
url += `&file_id=${fileId}`
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('全文搜索失败')
const data = await response.json()
return data.results || []
},
async searchImage(file, text = '', kbId = null, fileId = null) {
const formData = new FormData()
formData.append('file', file)
formData.append('text', text)
let url = `${API_BASE}/search/image`
const params = []
if (kbId) {
params.push(`kb_id=${kbId}`)
}
if (fileId) {
params.push(`file_id=${fileId}`)
}
if (params.length > 0) {
url += `?${params.join('&')}`
}
const response = await fetch(url, {
method: 'POST',
headers: getHeaders(false),
body: formData,
})
if (!response.ok) throw new Error('图片搜索失败')
const data = await response.json()
return data.results || []
},
// 词表
async listLexicons(params = {}) {
const searchParams = new URLSearchParams()
if (params.q) searchParams.set('q', params.q)
if (params.enabled === true) searchParams.set('enabled', 'true')
if (params.enabled === false) searchParams.set('enabled', 'false')
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.offset) searchParams.set('offset', String(params.offset))
const query = searchParams.toString()
const url = `${API_BASE}/search/lexicons${query ? `?${query}` : ''}`
const response = await fetch(url, { headers: getHeaders() })
if (!response.ok) throw new Error(await readErrorMessage(response, '获取词表失败'))
return response.json()
},
async createLexicon(data) {
const response = await fetch(`${API_BASE}/search/lexicons`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '创建词条失败'))
return response.json()
},
async updateLexicon(id, data) {
const response = await fetch(`${API_BASE}/search/lexicons/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '更新词条失败'))
return response.json()
},
async deleteLexicon(id) {
const response = await fetch(`${API_BASE}/search/lexicons/${id}`, {
method: 'DELETE',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '删除词条失败'))
return response.json()
},
async toggleLexiconEnabled(id, enabled) {
const response = await fetch(`${API_BASE}/search/lexicons/${id}/enabled`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ enabled }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '更新词条状态失败'))
return response.json()
},
async reloadLexicon() {
const response = await fetch(`${API_BASE}/search/lexicons/reload`, {
method: 'POST',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '重载词表失败'))
return response.json()
},
async publishLexicon() {
const response = await fetch(`${API_BASE}/search/lexicons/publish`, {
method: 'POST',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '发布词表失败'))
return response.json()
},
// 同义词
async listSynonyms(params = {}) {
const searchParams = new URLSearchParams()
if (params.q) searchParams.set('q', params.q)
if (params.enabled === true) searchParams.set('enabled', 'true')
if (params.enabled === false) searchParams.set('enabled', 'false')
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.offset) searchParams.set('offset', String(params.offset))
const query = searchParams.toString()
const url = `${API_BASE}/search/synonyms${query ? `?${query}` : ''}`
const response = await fetch(url, { headers: getHeaders() })
if (!response.ok) throw new Error(await readErrorMessage(response, '获取同义词失败'))
return response.json()
},
async createSynonym(data) {
const response = await fetch(`${API_BASE}/search/synonyms`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '创建同义词失败'))
return response.json()
},
async updateSynonym(id, data) {
const response = await fetch(`${API_BASE}/search/synonyms/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '更新同义词失败'))
return response.json()
},
async deleteSynonym(id) {
const response = await fetch(`${API_BASE}/search/synonyms/${id}`, {
method: 'DELETE',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '删除同义词失败'))
return response.json()
},
async toggleSynonymEnabled(id, enabled) {
const response = await fetch(`${API_BASE}/search/synonyms/${id}/enabled`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ enabled }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '更新同义词状态失败'))
return response.json()
},
// 系统
async getIndexRebuildStatus() {
const response = await fetch(`${API_BASE}/system/index/rebuild/status`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '获取重建状态失败'))
return response.json()
},
// 知识库
async getKnowledgeBases(parentId = null) {
let url = `${API_BASE}/knowledge_base/`;
const params = new URLSearchParams();
// A null parentId fetches top-level KBs by default on the backend.
if (parentId) {
params.append('parent_id', parentId);
}
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取知识库列表失败')
return response.json()
},
async getKnowledgeBase(id) {
const response = await fetch(`${API_BASE}/knowledge_base/${id}`, {
headers: getHeaders(),
});
if (!response.ok) throw new Error('获取知识库详情失败');
return response.json();
},
async getKnowledgeBaseFiles(id, params = {}) {
const searchParams = new URLSearchParams()
if (params.page) searchParams.set('page', String(params.page))
if (params.size) searchParams.set('size', String(params.size))
if (params.filename) searchParams.set('filename', params.filename)
if (params.tag) searchParams.set('tag', params.tag)
const query = searchParams.toString()
const response = await fetch(
`${API_BASE}/knowledge_base/${id}/files${query ? `?${query}` : ''}`,
{ headers: getHeaders() }
)
if (!response.ok) throw new Error(await readErrorMessage(response, '获取知识库文件失败'))
return response.json()
},
async createKnowledgeBase(data) { // data may include { name, description, parent_id }
const response = await fetch(`${API_BASE}/knowledge_base/`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('创建知识库失败')
return response.json()
},
async updateKnowledgeBase(id, data) { // data may include { name, description, parent_id, is_public }
const response = await fetch(`${API_BASE}/knowledge_base/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('更新知识库失败')
return response.json()
},
async deleteKnowledgeBase(id) {
const response = await fetch(`${API_BASE}/knowledge_base/${id}`, {
method: 'DELETE',
headers: getHeaders(),
})
if (!response.ok) throw new Error('删除知识库失败')
return
},
async reparseKnowledgeBases() {
const response = await fetch(`${API_BASE}/knowledge_base/reparse`, {
method: 'POST',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '重新解析失败'))
return response.json()
},
async reparseKnowledgeBase(id) {
const response = await fetch(`${API_BASE}/knowledge_base/${id}/reparse`, {
method: 'POST',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '重新解析指定知识库失败'))
return response.json()
},
async exportKnowledgeBases(kbIds, includeChildren = false) {
const response = await fetch(`${API_BASE}/knowledge_base/export`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ kb_ids: kbIds, include_children: includeChildren }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '导出知识库失败'))
return response.json()
},
// 知识库权限管理
async getKbPermissions(kbId) {
const response = await fetch(`${API_BASE}/knowledge_base/${kbId}/permissions`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取权限列表失败')
return response.json()
},
async addKbPermission(kbId, userId, permission) {
const response = await fetch(`${API_BASE}/knowledge_base/${kbId}/permissions`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ user_id: userId, permission }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '添加权限失败'))
return response.json()
},
async removeKbPermission(kbId, userId) {
const response = await fetch(`${API_BASE}/knowledge_base/${kbId}/permissions/${userId}`, {
method: 'DELETE',
headers: getHeaders(),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '删除权限失败'))
return response.json()
},
// 文件
async uploadFiles(knowledgeBaseId, files, tags = [], isPublic = false, sliceType = 'smart') {
const formData = new FormData()
if (knowledgeBaseId) {
formData.append('kb_id', knowledgeBaseId)
}
if (tags.length > 0) {
formData.append('tags', JSON.stringify(tags))
}
formData.append('is_public', isPublic ? 'true' : 'false')
formData.append('slice_type', sliceType)
for (const file of files) {
formData.append('file', file)
}
const response = await fetch(`${API_BASE}/files/`, {
method: 'POST',
headers: getHeaders(false),
body: formData,
})
if (!response.ok) throw new Error(await readErrorMessage(response, '上传文件失败'))
return response.json()
},
async deleteFile(id) {
const response = await fetch(`${API_BASE}/files/${id}`, {
method: 'DELETE',
headers: getHeaders(),
})
if (!response.ok) throw new Error('删除文件失败')
return
},
async batchDeleteFiles(ids, options = {}) {
const {
strict = false,
allowProcessing = false,
} = options
const response = await fetch(`${API_BASE}/files/batch-delete`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
ids,
strict,
allow_processing: allowProcessing,
}),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '批量删除文件失败'))
return response.json()
},
async getFiles(kbId, tag = null, pagination = {}) {
let url = `${API_BASE}/files/`
const params = []
if (kbId === null) {
// 明确传递null时获取未分配知识库的文件
params.push('kb_id=null')
} else if (kbId !== undefined) {
// 传递具体的知识库ID
params.push(`kb_id=${kbId}`)
}
if (tag) {
params.push(`tag=${encodeURIComponent(tag)}`)
}
if (pagination.page) params.push(`page=${pagination.page}`)
if (pagination.size) params.push(`size=${pagination.size}`)
if (params.length > 0) {
url += `?${params.join('&')}`
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取文件列表失败')
return response.json()
},
async getFileStats(options = {}) {
const {
kbId = undefined,
includeDescendants = true,
includeUnassigned = true,
} = options
const params = new URLSearchParams()
if (kbId === null) {
params.append('kb_id', 'null')
} else if (kbId !== undefined) {
params.append('kb_id', kbId)
}
if (!includeDescendants) {
params.append('include_descendants', 'false')
}
if (!includeUnassigned) {
params.append('include_unassigned', 'false')
}
const query = params.toString()
const response = await fetch(
`${API_BASE}/files/stats${query ? `?${query}` : ''}`,
{ headers: getHeaders() }
)
if (!response.ok) throw new Error('获取文件状态统计失败')
return response.json()
},
async reparseFailedFiles(options = {}) {
const {
kbId = undefined,
includeDescendants = true,
includeUnassigned = true,
unassignedOnly = false,
} = options
const payload = {
include_descendants: includeDescendants,
include_unassigned: includeUnassigned,
unassigned_only: unassignedOnly,
}
if (kbId !== undefined) {
payload.kb_id = kbId ?? null
}
const response = await fetch(`${API_BASE}/files/reparse-failed`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(payload),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '重新解析失败文件失败'))
return response.json()
},
async getFile(id) {
const response = await fetch(`${API_BASE}/files/${id}`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取文件失败')
return response.json()
},
async downloadFile(id) {
const response = await fetch(`${API_BASE}/files/${id}/download`, {
headers: getHeaders(false),
})
if (!response.ok) throw new Error('下载文件失败')
return response.blob()
},
async updateFile(id, data) {
const response = await fetch(`${API_BASE}/files/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('更新文件失败')
return response.json()
},
async moveFile(id, targetKbId) {
const response = await fetch(`${API_BASE}/files/${id}/move`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({
target_kb_id: targetKbId ?? null,
}),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '移动文件失败'))
return response.json()
},
async getFileSlices(fileId) {
const response = await fetch(`${API_BASE}/files/${fileId}/slices`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取切片失败')
return response.json()
},
async updateSlices(fileId, slices) {
const response = await fetch(`${API_BASE}/files/${fileId}/slices`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify({ slices }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '更新切片失败'))
return response.json()
},
// 压缩文件
async getArchiveEntries(id) {
const response = await fetch(`${API_BASE}/files/${id}/archive-entries`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取压缩文件列表失败')
return response.json()
},
async extractArchive(id, password = null) {
const response = await fetch(`${API_BASE}/files/${id}/archive-extract`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ password }),
})
if (!response.ok) throw new Error(await readErrorMessage(response, '解压失败'))
return response.json()
},
async downloadArchiveEntry(id, path) {
const encodedPath = encodeURIComponent(path)
const response = await fetch(`${API_BASE}/files/${id}/archive-download?path=${encodedPath}`, {
headers: getHeaders(false),
})
if (!response.ok) throw new Error('下载文件失败')
return response.blob()
},
// 知识图谱
async searchEntities(query = null, entityType = null, kbId = null, limit = 100, fileId = null) {
let url = `${API_BASE}/graph/entities?limit=${limit}`
if (query) {
url += `&q=${encodeURIComponent(query)}`
}
if (entityType) {
url += `&entity_type=${encodeURIComponent(entityType)}`
}
if (kbId) {
url += `&kb_id=${kbId}`
}
if (fileId) {
url += `&file_id=${fileId}`
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('搜索实体失败')
return response.json()
},
async getEntity(id) {
const response = await fetch(`${API_BASE}/graph/entities/${id}`, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取实体详情失败')
return response.json()
},
async getGraphStats(kbId = null, fileId = null) {
let url = `${API_BASE}/graph/stats`
const params = []
if (kbId) {
params.push(`kb_id=${kbId}`)
}
if (fileId) {
params.push(`file_id=${fileId}`)
}
if (params.length > 0) {
url += '?' + params.join('&')
}
const response = await fetch(url, {
headers: getHeaders(),
})
if (!response.ok) throw new Error('获取图谱统计失败')
return response.json()
},
}