/** * 统一 API 请求层 * 封装所有后端接口调用,组件只需调用函数处理业务逻辑 * P1: 所有请求自动附加 Bearer Token,401 时触发未登录事件 */ import { API_BASE, authHeaders, clearTokens } from './utils.js' // ==================== 通用请求 ==================== /** * 处理 401 未授权响应 */ function handleUnauthorized() { clearTokens() window.dispatchEvent(new Event('auth:unauthorized')) } /** * GET 请求,返回 JSON */ async function getJSON(path) { const res = await fetch(API_BASE + path, { headers: authHeaders() }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } if (res.status === 403) throw new Error('403') 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', ...authHeaders() }, body: JSON.stringify(body) }) const json = await res.json().catch(() => ({ message: res.statusText || `HTTP ${res.status}` })) if (res.status === 401) { handleUnauthorized(); throw new Error(json.message || '未登录或登录已过期') } if (res.status === 403) throw new Error(json.message || '权限不足') if (!res.ok) throw new Error(json.message || `HTTP ${res.status}`) return json } /** * DELETE 请求 + JSON body,返回 JSON(用于批量删除等场景) */ async function deleteJSONWithBody(path, body) { const res = await fetch(API_BASE + path, { method: 'DELETE', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify(body) }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } 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', ...authHeaders() }, body: JSON.stringify(body) }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } return res.json() } /** * DELETE 请求,返回 JSON(无 body) */ async function deleteJSON(path) { const res = await fetch(API_BASE + path, { method: 'DELETE', headers: authHeaders() }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } 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', headers: authHeaders() }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } return res.json() } /** * POST FormData 请求,返回 JSON */ async function postForm(path, formData) { const res = await fetch(API_BASE + path, { method: 'POST', headers: authHeaders(), body: formData }) if (res.status === 401) { handleUnauthorized(); throw new Error('401') } 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, keyword, tag) { let path = `/document/list?page=${page}&size=${size}` if (categoryId) path += `&categoryId=${categoryId}` if (status) path += `&status=${status}` if (keyword) path += `&keyword=${encodeURIComponent(keyword)}` if (tag) path += `&tag=${encodeURIComponent(tag)}` 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 }) } /** * P1-2.1: 切换文档启用/禁用状态 */ export function toggleDocument(id) { return putJSON(`/document/${id}/toggle`) } /** * P1-2.1: 批量启用/禁用文档 */ export function batchToggleDocuments(ids, enabled) { return postJSON('/document/batch/toggle', { ids, enabled }) } /** * P1-2.2: 批量移动文档分类 */ export function batchMoveDocuments(ids, categoryId) { return postJSON('/document/batch/move', { ids, categoryId }) } /** * P1-2.3: 获取标签列表(聚合去重,含使用次数) */ export function getTagList() { return getJSON('/tag/list') } /** * P1-2.5: 更新单个分块内容 */ export function updateChunk(docId, chunkIndex, content) { return putJSONWithBody(`/document/${docId}/chunk/${chunkIndex}`, { content }) } /** * P1-2.5: 删除单个分块 */ export function deleteChunk(docId, chunkIndex) { return deleteJSON(`/document/${docId}/chunk/${chunkIndex}`) } /** * 语义搜索 */ 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) // 附加认证头 const token = localStorage.getItem('sb_token') if (token) xhr.setRequestHeader('Authorization', 'Bearer ' + token) // 上传进度监听 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable && onProgress) { onProgress(Math.round((e.loaded / e.total) * 100)) } }) xhr.addEventListener('load', () => { if (xhr.status === 401) { handleUnauthorized() reject(new Error('401')) return } 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', ...authHeaders() }, 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 putJSONWithBody(`/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`, { headers: authHeaders() }) .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}`) } // ==================== F1: 连接测试 ==================== /** * 测试模型配置连接可用性 */ export function testModelConfig(id) { return postJSON(`/model-config/${id}/test`, {}) } // ==================== F2: 配置复制 ==================== /** * 复制模型配置 */ export function duplicateModelConfig(id) { return postJSON(`/model-config/${id}/duplicate`, {}) } // ==================== F4: 健康状态 ==================== /** * 获取所有配置的健康状态 */ export function getHealthStatus() { return getJSON('/model-config/health') } /** * 手动触发单个配置的健康检查 */ export function manualHealthCheck(id) { return postJSON(`/model-config/${id}/health-check`, {}) } // ==================== F6: 优先级管理 ==================== /** * 更新配置优先级 */ export function updateModelConfigPriority(id, priority) { return putJSONWithBody(`/model-config/${id}/priority`, { priority }) } /** * 停用配置(从 Fallback 链中移除) */ export function deactivateModelConfig(id) { return putJSON(`/model-config/${id}/deactivate`) } // ==================== F7: 配置导入/导出 ==================== /** * 导出所有模型配置(API Key 始终脱敏) */ export function exportModelConfigs() { return getJSON('/model-config/export') } /** * 批量导入模型配置 */ export function importModelConfigs(configs, onConflict = 'skip') { return postJSON('/model-config/import', { configs, onConflict }) } /** * 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, categoryId, status) { let path = `/faq/list?page=${page}&size=${size}` if (keyword) path += `&keyword=${encodeURIComponent(keyword)}` if (categoryId) path += `&categoryId=${categoryId}` 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') } // ==================== P1-001: 认证管理 ==================== /** * 用户登录 */ export function login(username, password) { return postJSON('/auth/login', { username, password }) } /** * 刷新 Token */ export function refreshToken(refreshToken) { return postJSON('/auth/refresh', { refreshToken }) } /** * 获取当前登录用户信息 */ export function getCurrentUser() { return getJSON('/auth/me') } // ==================== P1-001: 系统用户管理 ==================== /** * 系统用户分页列表 */ export function listSysUsers(page = 1, size = 20, keyword, enabled) { let path = `/sys-user/list?page=${page}&size=${size}` if (keyword) path += `&keyword=${encodeURIComponent(keyword)}` if (enabled !== undefined && enabled !== null) path += `&enabled=${enabled}` return getJSON(path) } /** * 获取用户详情 */ export function getSysUserDetail(id) { return getJSON(`/sys-user/${id}`) } /** * 创建系统用户 */ export function createSysUser(data) { return postJSON('/sys-user', data) } /** * 更新系统用户 */ export function updateSysUser(id, data) { return putJSONWithBody(`/sys-user/${id}`, data) } /** * 启用/禁用系统用户 */ export function toggleSysUser(id, enabled) { return putJSON(`/sys-user/${id}/toggle`, { enabled }) } /** * 分配角色 */ export function assignSysUserRoles(id, roleIds) { return putJSONWithBody(`/sys-user/${id}/roles`, { roleIds }) } /** * 修改密码 */ export function changeSysUserPassword(id, newPassword) { return putJSONWithBody(`/sys-user/${id}/password`, { newPassword }) } /** * 获取所有角色(分配角色下拉用) */ export function getAllSysRoles() { return getJSON('/sys-user/roles') } /** * 获取系统角色列表 */ export function listSysRoles() { return getJSON('/sys-role/list') } /** * 创建系统角色 */ export function createSysRole(data) { return postJSON('/sys-role', data) } /** * 更新系统角色 */ export function updateSysRole(id, data) { return putJSONWithBody(`/sys-role/${id}`, data) } // ==================== P1-002: 运营看板 ==================== /** * 看板概览(今日实时指标) */ export function getDashboardOverview() { return getJSON('/dashboard/overview') } /** * 趋势数据(7/30天) */ export function getDashboardTrend(days = 7) { return getJSON(`/dashboard/trend?days=${days}`) } /** * 知识库分析(命中TOP-10 + 未命中问题) */ export function getDashboardKnowledge() { return getJSON('/dashboard/knowledge-analysis') } /** * 自定义时间范围数据 */ export function getDashboardCustom(startDate, endDate) { return getJSON(`/dashboard/custom?startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}`) } // ==================== P1-003: API Key 管理 ==================== /** * API Key 列表 */ export function listApiKeys(page = 1, size = 20) { return getJSON(`/api-key/list?page=${page}&size=${size}`) } /** * 创建 API Key */ export function createApiKey(data) { return postJSON('/api-key', data) } /** * 吊销 API Key */ export function revokeApiKey(id) { return putJSON(`/api-key/${id}/revoke`) } /** * 启用 API Key */ export function enableApiKey(id) { return putJSON(`/api-key/${id}/enable`) } /** * 删除 API Key */ export function deleteApiKey(id) { return deleteJSON(`/api-key/${id}`) } // ==================== P1-003: Webhook 管理 ==================== /** * Webhook 列表 */ export function listWebhooks(page = 1, size = 20) { return getJSON(`/webhook/list?page=${page}&size=${size}`) } /** * 创建 Webhook */ export function createWebhook(data) { return postJSON('/webhook', data) } /** * 更新 Webhook */ export function updateWebhook(id, data) { return putJSONWithBody(`/webhook/${id}`, data) } /** * 删除 Webhook */ export function deleteWebhook(id) { return deleteJSON(`/webhook/${id}`) } /** * 测试 Webhook 推送 */ export function testWebhook(id) { return postJSON(`/webhook/${id}/test`, {}) }