本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

657 lines
17 KiB

/**
* 统一 API 请求层
* 封装所有后端接口调用,组件只需调用函数处理业务逻辑
*/
import { API_BASE } from './utils.js'
// ==================== 通用请求 ====================
/**
* GET 请求,返回 JSON
*/
async function getJSON(path) {
const res = await fetch(API_BASE + path)
return res.json()
}
/**
* POST JSON 请求,返回 JSON
*/
async function postJSON(path, body) {
const res = await fetch(API_BASE + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
return res.json()
}
/**
* DELETE 请求 + JSON body,返回 JSON(用于批量删除等场景)
*/
async function deleteJSONWithBody(path, body) {
const res = await fetch(API_BASE + path, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
return res.json()
}
/**
* PUT 请求 + JSON body,返回 JSON(用于批量操作等场景)
*/
async function putJSONWithBody(path, body) {
const res = await fetch(API_BASE + path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
return res.json()
}
/**
* DELETE 请求,返回 JSON(无 body)
*/
async function deleteJSON(path) {
const res = await fetch(API_BASE + path, { method: 'DELETE' })
return res.json()
}
/**
* PUT 请求,返回 JSON(参数走 query string)
*/
async function putJSON(path, params) {
let url = API_BASE + path
if (params) {
const qs = Object.entries(params)
.filter(([, v]) => v !== undefined && v !== null && v !== '')
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
if (qs) url += (url.includes('?') ? '&' : '?') + qs
}
const res = await fetch(url, { method: 'PUT' })
return res.json()
}
/**
* POST FormData 请求,返回 JSON
*/
async function postForm(path, formData) {
const res = await fetch(API_BASE + path, {
method: 'POST',
body: formData
})
return res.json()
}
// ==================== AI 对话 ====================
/**
* 同步对话
* @param {string} message 用户消息(原样发送,不做包装)
* @param {string} chatId 会话ID
* @param {string} [roleId] 客服角色ID,命中后由服务端套用该角色人设
*/
export function chatSync(message, chatId, roleId, accountId) {
let url = API_BASE + `/ai/assistant_app/chat/sync?message=${encodeURIComponent(message)}&chatId=${encodeURIComponent(chatId)}`
if (roleId) url += `&roleId=${encodeURIComponent(roleId)}`
if (accountId) url += `&accountId=${encodeURIComponent(accountId)}`
return fetch(url).then(res => res.text())
}
/**
* RAG 同步对话
* 知识库范围由服务端依据 roleId 强制限定,前端无需(也无法)跨域指定分类。
* @param {string} message 用户消息
* @param {string} chatId 会话ID
* @param {string} strategy 查询重写策略
* @param {string} [roleId] 客服角色ID
*/
export function chatRagSync(message, chatId, strategy, roleId, accountId) {
let url = API_BASE + `/ai/assistant_app/chat/rag/sync?message=${encodeURIComponent(message)}&chatId=${encodeURIComponent(chatId)}&rewriteStrategy=${encodeURIComponent(strategy)}`
if (roleId) url += `&roleId=${encodeURIComponent(roleId)}`
if (accountId) url += `&accountId=${encodeURIComponent(accountId)}`
return fetch(url).then(res => res.text())
}
/**
* 获取普通 SSE 流式对话 URL
* @param {string} message
* @param {string} chatId
* @param {'sse'|'sse2'|'sse3'} mode
* @param {string} [roleId]
* @returns {string}
*/
export function chatSSEUrl(message, chatId, mode, roleId, accountId) {
const pathMap = {
sse: '/ai/assistant_app/chat/sse',
sse2: '/ai/assistant_app/chat/server_sent_event',
sse3: '/ai/assistant_app/chat/sse_emitter'
}
let url = API_BASE + `${pathMap[mode]}?message=${encodeURIComponent(message)}&chatId=${encodeURIComponent(chatId)}`
if (roleId) url += `&roleId=${encodeURIComponent(roleId)}`
if (accountId) url += `&accountId=${encodeURIComponent(accountId)}`
return url
}
/**
* 获取 RAG 流式对话 URL(知识库范围由服务端依据 roleId 强制限定)
* @param {string} message
* @param {string} chatId
* @param {string} strategy
* @param {string} [roleId]
* @returns {string}
*/
export function chatRagSSEUrl(message, chatId, strategy, roleId, accountId) {
let url = API_BASE + `/ai/assistant_app/chat/rag/sse?message=${encodeURIComponent(message)}&chatId=${encodeURIComponent(chatId)}&rewriteStrategy=${encodeURIComponent(strategy)}`
if (roleId) url += `&roleId=${encodeURIComponent(roleId)}`
if (accountId) url += `&accountId=${encodeURIComponent(accountId)}`
return url
}
/**
* 获取本次 RAG 回答命中的知识库片段(引用来源)
*/
export function ragSources(message, chatId, strategy, roleId, accountId) {
let path = `/ai/assistant_app/rag/sources?message=${encodeURIComponent(message)}&chatId=${encodeURIComponent(chatId)}&rewriteStrategy=${encodeURIComponent(strategy)}`
if (roleId) path += `&roleId=${encodeURIComponent(roleId)}`
if (accountId) path += `&accountId=${encodeURIComponent(accountId)}`
return getJSON(path)
}
export function getRoleList() {
return getJSON('/role/list')
}
/** 管理用:列出全部角色(含已停用) */
export function getAllRoles() {
return getJSON('/role/all')
}
/** 新增角色 */
export function createRole(role) {
return postJSON('/role', role)
}
/** 编辑角色基本信息 */
export function updateRole(roleId, role) {
return putJSONWithBody(`/role/${roleId}`, role)
}
/** 删除角色(逻辑删除) */
export function deleteRole(roleId) {
return deleteJSON(`/role/${roleId}`)
}
export function updateRoleCategories(roleId, categoryIds) {
return putJSONWithBody(`/role/${roleId}/categories`, { categoryIds })
}
export function getAccountList() {
return getJSON('/account/list')
}
export function getAllAccounts() {
return getJSON('/account/all')
}
export function createAccount(account) {
return postJSON('/account', account)
}
export function updateAccount(accountId, account) {
return putJSONWithBody(`/account/${accountId}`, account)
}
export function deleteAccount(accountId) {
return deleteJSON(`/account/${accountId}`)
}
// ==================== 文档管理 ====================
/**
* 文档列表(分页 + 过滤)
*/
export function listDocuments(page = 1, size = 10, categoryId, status) {
let path = `/document/list?page=${page}&size=${size}`
if (categoryId) path += `&categoryId=${categoryId}`
if (status) path += `&status=${status}`
return getJSON(path)
}
/**
* 文档详情
*/
export function getDocumentDetail(id) {
return getJSON(`/document/${id}`)
}
/**
* 文档分块列表
*/
export function getDocumentChunks(id) {
return getJSON(`/document/${id}/chunks`)
}
/**
* 删除文档
*/
export function deleteDocument(id) {
return deleteJSON(`/document/${id}`)
}
/**
* 更新文档元信息
*/
export function updateDocument(id, title, categoryId, tags) {
return putJSON(`/document/${id}`, { title, categoryId, tags })
}
/**
* 重新处理文档
*/
export function reprocessDocument(id) {
return putJSON(`/document/${id}/reprocess`)
}
/**
* 批量删除文档
*/
export function batchDeleteDocuments(ids) {
return postJSON('/document/batch/delete', { ids })
}
/**
* 批量重新处理文档
*/
export function batchReprocessDocuments(ids) {
return postJSON('/document/batch/reprocess', { ids })
}
/**
/**
* 语义搜索
*/
export function searchDocuments(query, topK, similarityThreshold, categoryId, categoryIds, searchMode) {
const body = { query, topK, similarityThreshold }
if (categoryIds && categoryIds.length) body.categoryIds = categoryIds
if (categoryId) body.categoryId = categoryId
if (searchMode) body.searchMode = searchMode
return postJSON('/document/search', body)
}
// ==================== 统计 ====================
/**
* 知识库统计
*/
export function getStats() {
return getJSON('/document/stats')
}
// ==================== 上传 ====================
/**
* 上传文件(带进度回调)
* 使用 XMLHttpRequest 替代 fetch,支持监听上传进度
*
* @param {string} path 请求路径
* @param {FormData} formData 表单数据
* @param {function(number): void} onProgress 进度回调,参数为 0-100 的百分比
* @returns {Promise<object>}
*/
function postFormWithProgress(path, formData, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', API_BASE + path)
// 上传进度监听
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable && onProgress) {
onProgress(Math.round((e.loaded / e.total) * 100))
}
})
xhr.addEventListener('load', () => {
try {
resolve(JSON.parse(xhr.responseText))
} catch (e) {
reject(new Error('响应解析失败'))
}
})
xhr.addEventListener('error', () => reject(new Error('网络错误')))
xhr.addEventListener('abort', () => reject(new Error('上传已取消')))
xhr.send(formData)
})
}
/**
* 上传普通文件(带进度)
*/
export function uploadFile(formData, onProgress) {
return postFormWithProgress('/upload/file', formData, onProgress)
}
/**
* 上传普通文件(无进度,兼容旧调用)
*/
export function uploadFileSimple(formData) {
return postForm('/upload/file', formData)
}
/**
* 上传文本内容
*/
export function uploadString(content, title, categoryId, tags) {
let url = `/upload/string?title=${encodeURIComponent(title)}`
if (categoryId) url += `&categoryId=${categoryId}`
if (tags) url += `&tags=${encodeURIComponent(tags)}`
return fetch(API_BASE + url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: content
}).then(res => res.json())
}
/**
* 上传 Markdown(带进度)
*/
export function uploadMarkdown(formData, onProgress) {
return postFormWithProgress('/upload/markdown', formData, onProgress)
}
/**
* 上传 JSON(基本方式,带进度)
*/
export function uploadJsonBasic(formData, onProgress) {
return postFormWithProgress('/upload/json/basic', formData, onProgress)
}
/**
* 上传 JSON(按字段提取,带进度)
*/
export function uploadJsonFields(formData, fields, onProgress) {
return postFormWithProgress(`/upload/json/fields?fields=${encodeURIComponent(fields)}`, formData, onProgress)
}
/**
* 上传 JSON(按指针拆分,带进度)
*/
export function uploadJsonPointer(formData, pointer, onProgress) {
return postFormWithProgress(`/upload/json/pointer?pointer=${encodeURIComponent(pointer)}`, formData, onProgress)
}
// ==================== 分类 ====================
/**
* 分类树
*/
export function getCategoryTree() {
return getJSON('/category/tree')
}
/**
* 分类列表
*/
export function getCategoryList() {
return getJSON('/category/list')
}
/**
* 创建分类
*/
export function createCategory(data) {
return postJSON('/category', data)
}
/**
* 更新分类
*/
export function updateCategory(id, data) {
return putJSON(`/category/${id}`, data)
}
/**
* 删除分类
*/
export function deleteCategory(id) {
return deleteJSON(`/category/${id}`)
}
// ==================== 会话管理 ====================
/**
* 会话列表(分页)
*/
export function listConversations(page = 1, size = 10, keyword, accountId, roleId) {
let path = `/conversation/list?page=${page}&size=${size}`
if (keyword) path += `&keyword=${encodeURIComponent(keyword)}`
if (accountId) path += `&accountId=${encodeURIComponent(accountId)}`
if (roleId) path += `&roleId=${encodeURIComponent(roleId)}`
return getJSON(path)
}
/**
* 会话详情
*/
export function getConversationDetail(conversationId) {
return getJSON(`/conversation/${conversationId}`)
}
/**
* 会话消息列表
*/
export function getConversationMessages(conversationId) {
return getJSON(`/conversation/${conversationId}/messages`)
}
/**
* 删除会话
*/
export function deleteConversation(conversationId) {
return deleteJSON(`/conversation/${conversationId}`)
}
/**
* 导出会话记录
*/
export function exportConversation(conversationId) {
return fetch(API_BASE + `/conversation/${conversationId}/export`)
.then(res => res.text())
}
/**
* 会话统计
*/
export function getConversationStats() {
return getJSON('/conversation/stats')
}
// ==================== Model config management ====================
export function listModelConfigs(page = 1, size = 10, appType) {
let path = `/model-config/list?page=${page}&size=${size}`
if (appType) path += `&appType=${encodeURIComponent(appType)}`
return getJSON(path)
}
export function getModelConfigDetail(id) {
return getJSON(`/model-config/${id}`)
}
export function getActiveModelConfig(appType) {
return getJSON(`/model-config/active/${encodeURIComponent(appType)}`)
}
export function createModelConfig(data) {
return postJSON('/model-config', data)
}
export function updateModelConfig(id, data) {
return putJSONWithBody(`/model-config/${id}`, data)
}
export function activateModelConfig(id) {
return putJSON(`/model-config/${id}/activate`)
}
export function deleteModelConfig(id) {
return deleteJSON(`/model-config/${id}`)
}
/**
* Truncate a conversation from the given user turn onward.
*/
export function truncateConversation(conversationId, userTurn) {
return postJSON(`/conversation/${conversationId}/truncate`, { userTurn })
}
// ==================== P0-002: 用户反馈 ====================
/**
* 提交/修改消息反馈(upsert 语义)
* @param {Object} feedback - { messageId, conversationId, feedbackType, reasonCategory, reasonComment }
*/
export function submitFeedback(feedback) {
return postJSON('/feedback', feedback)
}
/**
* 获取反馈统计
*/
export function getFeedbackStats(startDate, endDate) {
let path = '/feedback/stats'
const params = []
if (startDate) params.push(`startDate=${encodeURIComponent(startDate)}`)
if (endDate) params.push(`endDate=${encodeURIComponent(endDate)}`)
if (params.length) path += '?' + params.join('&')
return getJSON(path)
}
/**
* 按会话查询反馈
*/
export function getFeedbackByConversation(conversationId) {
return getJSON(`/feedback/by-conversation/${encodeURIComponent(conversationId)}`)
}
/**
* 批量查询消息反馈状态(供回显)
*/
export function getFeedbackBatch(messageIds) {
return getJSON(`/feedback/batch?messageIds=${encodeURIComponent(messageIds.join(','))}`)
}
// ==================== P0-004: 敏感词管理 ====================
/**
* 敏感词分页列表
*/
export function listSensitiveWords(page = 1, size = 20, keyword, category) {
let path = `/sensitive-word/list?page=${page}&size=${size}`
if (keyword) path += `&keyword=${encodeURIComponent(keyword)}`
if (category) path += `&category=${encodeURIComponent(category)}`
return getJSON(path)
}
/**
* 新增敏感词
*/
export function createSensitiveWord(data) {
return postJSON('/sensitive-word', data)
}
/**
* 修改敏感词
*/
export function updateSensitiveWord(id, data) {
return putJSONWithBody(`/sensitive-word/${id}`, data)
}
/**
* 删除敏感词
*/
export function deleteSensitiveWord(id) {
return deleteJSON(`/sensitive-word/${id}`)
}
/**
* 批量导入敏感词
*/
export function batchImportSensitiveWords(data) {
return postJSON('/sensitive-word/batch-import', data)
}
/**
* 查询审计日志
*/
export function listAuditLogs(page = 1, size = 20, sessionId) {
let path = `/sensitive-word/audit-log?page=${page}&size=${size}`
if (sessionId) path += `&sessionId=${encodeURIComponent(sessionId)}`
return getJSON(path)
}
// ==================== P0-003: FAQ 管理 ====================
/**
* FAQ 分页列表
*/
export function listFaqs(page = 1, size = 20, keyword, category, status) {
let path = `/faq/list?page=${page}&size=${size}`
if (keyword) path += `&keyword=${encodeURIComponent(keyword)}`
if (category) path += `&category=${encodeURIComponent(category)}`
if (status) path += `&status=${encodeURIComponent(status)}`
return getJSON(path)
}
/**
* 新增 FAQ
*/
export function createFaq(data) {
return postJSON('/faq', data)
}
/**
* 修改 FAQ
*/
export function updateFaq(id, data) {
return putJSONWithBody(`/faq/${id}`, data)
}
/**
* 删除 FAQ
*/
export function deleteFaq(id) {
return deleteJSON(`/faq/${id}`)
}
/**
* 启用/禁用 FAQ
*/
export function toggleFaqStatus(id, status) {
return putJSON(`/faq/${id}/toggle`, { status })
}
/**
* 批量导入 FAQ
*/
export function batchImportFaqs(data) {
return postJSON('/faq/batch-import', data)
}
/**
* 导出所有 FAQ
*/
export function exportFaqs() {
return getJSON('/faq/export')
}
/**
* FAQ 匹配统计
*/
export function getFaqStats() {
return getJSON('/faq/stats')
}