/** * 统一 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} */ 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') }