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
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')
|
|
}
|