diff --git a/README.md b/README.md index cf398446..53784b01 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ ## 📝 项目概述 +DEV 更新待办: + +- 智能体的消息加载有问题 +- 智能体的管理员的配置无法更新到用户层面 + + 语析是一个强大的问答平台,结合了大模型 RAG 知识库与知识图谱技术,基于 Llamaindex + VueJS + FastAPI + Neo4j 构建。 **核心特点:** diff --git a/server/utils/auth_middleware.py b/server/utils/auth_middleware.py index 692f872f..f78aa1d2 100644 --- a/server/utils/auth_middleware.py +++ b/server/utils/auth_middleware.py @@ -59,6 +59,14 @@ async def get_current_user(token: Optional[str] = Depends(oauth2_scheme), db: Se raise credentials_exception except JWTError: raise credentials_exception + except ValueError as e: + # 捕获AuthUtils.verify_access_token可能抛出的ValueError + # 例如令牌过期或无效 + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=str(e), # 将错误信息直接传递给客户端 + headers={"WWW-Authenticate": "Bearer"}, + ) # 查找用户 user = db.query(User).filter(User.id == user_id).first() @@ -108,7 +116,15 @@ ADMIN_PATHS = [ r"^/admin/.*$", # 管理员接口 r"^/data/.*$", # 数据操作接口,所有数据操作都需要管理员权限 r"^/chat/set_default_agent$", # 设置默认智能体 - r"^/chat/models/update$" # 更新模型列表 + r"^/chat/models/update$", # 更新模型列表 + r"^/auth/users.*$", # 用户管理相关API + r"^/config$" # 系统配置API +] + +# 路径是否需要超级管理员权限 +SUPERADMIN_PATHS = [ + r"^/restart$", # 系统重启 + r"^/auth/users/\d+/role$" # 修改用户角色(仅超级管理员可操作) ] # 检查路径是否需要管理员权限 @@ -117,4 +133,12 @@ def is_admin_path(path: str) -> bool: for pattern in ADMIN_PATHS: if re.match(pattern, path): return True + return False + +# 检查路径是否需要超级管理员权限 +def is_superadmin_path(path: str) -> bool: + path = path.rstrip('/') # 去除尾部斜杠以便于匹配 + for pattern in SUPERADMIN_PATHS: + if re.match(pattern, path): + return True return False \ No newline at end of file diff --git a/web/src/apis/admin_api.js b/web/src/apis/admin_api.js index 5b4177ce..9e298524 100644 --- a/web/src/apis/admin_api.js +++ b/web/src/apis/admin_api.js @@ -5,7 +5,7 @@ import { useUserStore } from '@/stores/user' * 管理员API模块 * 只有管理员和超级管理员可以访问的API * 权限要求: admin 或 superadmin - * + * * 注意: 请确保在使用这些API之前检查用户是否具有管理员权限 */ @@ -50,7 +50,7 @@ export const userManagementApi = { /** * 更新用户 - * @param {number} userId - 用户ID + * @param {number} userId - 用户ID * @param {Object} userData - 用户数据 * @returns {Promise} - 更新结果 */ @@ -137,7 +137,7 @@ export const knowledgeBaseApi = { /** * 删除知识库 - * @param {string} dbId - 知识库ID + * @param {string} dbId - 知识库ID * @returns {Promise} - 删除结果 */ deleteDatabase: async (dbId) => { @@ -165,7 +165,7 @@ export const knowledgeBaseApi = { /** * 删除文件 * @param {string} dbId - 知识库ID - * @param {string} fileId - 文件ID + * @param {string} fileId - 文件ID * @returns {Promise} - 删除结果 */ deleteFile: async (dbId, fileId) => { @@ -204,6 +204,17 @@ export const knowledgeBaseApi = { checkAdminPermission() return apiPost('/api/data/query-test', data, {}, true) }, + + /** + * 获取文档详情 + * @param {string} dbId - 知识库ID + * @param {string} fileId - 文件ID + * @returns {Promise} - 文档详情 + */ + getDocumentDetail: async (dbId, fileId) => { + checkAdminPermission() + return apiGet(`/api/data/document?db_id=${dbId}&file_id=${fileId}`, {}, true) + } } // 图数据库管理API @@ -309,6 +320,6 @@ export const logApi = { */ getLogs: async (params = {}) => { checkAdminPermission() - return apiGet('/api/admin/logs', { params }, true) + return apiGet('/api/log', { params }, true) }, -} \ No newline at end of file +} \ No newline at end of file diff --git a/web/src/apis/auth_api.js b/web/src/apis/auth_api.js index f971d5c7..92010820 100644 --- a/web/src/apis/auth_api.js +++ b/web/src/apis/auth_api.js @@ -1,4 +1,5 @@ import { apiGet, apiPost, apiDelete } from './base' +import { useUserStore } from '@/stores/user' /** * 需要用户认证的API模块 @@ -10,7 +11,7 @@ import { apiGet, apiPost, apiDelete } from './base' export const chatApi = { /** * 发送聊天消息 - * @param {Object} params - 聊天参数 + * @param {Object} params - 聊天参数 * @returns {Promise} - 聊天响应流 */ sendMessage: (params) => { @@ -18,11 +19,47 @@ export const chatApi = { method: 'POST', headers: { 'Content-Type': 'application/json', + ...useUserStore().getAuthHeaders() }, body: JSON.stringify(params), }) }, + /** + * 发送可中断的聊天消息 + * @param {Object} params - 聊天参数 + * @param {AbortSignal} signal - 用于中断请求的信号控制器 + * @returns {Promise} - 聊天响应流 + */ + sendMessageWithAbort: (params, signal) => { + return fetch('/api/chat/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...useUserStore().getAuthHeaders() + }, + body: JSON.stringify(params), + signal // 添加 signal 用于中断请求 + }) + }, + + /** + * 发送聊天消息到指定智能体(流式响应) + * @param {string} agentId - 智能体ID + * @param {Object} data - 聊天数据 + * @returns {Promise} - 聊天响应流 + */ + sendAgentMessage: (agentId, data) => { + return fetch(`/api/chat/agent/${agentId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...useUserStore().getAuthHeaders() + }, + body: JSON.stringify(data) + }) + }, + /** * 简单聊天调用(非流式) * @param {string} query - 查询内容 @@ -66,7 +103,7 @@ export const chatApi = { * @param {string} conversationId - 对话ID * @returns {Promise} - 对话详情 */ - getConversation: (conversationId) => + getConversation: (conversationId) => apiGet(`/api/chat/conversations/${conversationId}`, {}, true), /** @@ -74,7 +111,7 @@ export const chatApi = { * @param {string} conversationId - 对话ID * @returns {Promise} - 删除结果 */ - deleteConversation: (conversationId) => + deleteConversation: (conversationId) => apiDelete(`/api/chat/conversations/${conversationId}`, {}, true), /** @@ -83,7 +120,7 @@ export const chatApi = { * @param {string} title - 新标题 * @returns {Promise} - 更新结果 */ - updateConversationTitle: (conversationId, title) => + updateConversationTitle: (conversationId, title) => apiPost(`/api/chat/conversations/${conversationId}/title`, { title }, {}, true), } @@ -94,7 +131,7 @@ export const userSettingsApi = { * @returns {Promise} - 用户设置 */ getSettings: () => apiGet('/api/user/settings', {}, true), - + /** * 更新用户设置 * @param {Object} settings - 新设置 @@ -103,4 +140,4 @@ export const userSettingsApi = { updateSettings: (settings) => apiPost('/api/user/settings', settings, {}, true), } -// 其他需要用户认证的API可以继续添加到这里 \ No newline at end of file +// 其他需要用户认证的API可以继续添加到这里 \ No newline at end of file diff --git a/web/src/apis/base.js b/web/src/apis/base.js index 78e4bdf8..5b403d8a 100644 --- a/web/src/apis/base.js +++ b/web/src/apis/base.js @@ -30,19 +30,21 @@ export async function apiRequest(url, options = {}, requiresAuth = false) { if (!userStore.isLoggedIn) { throw new Error('用户未登录') } - + Object.assign(requestOptions.headers, userStore.getAuthHeaders()) } // 发送请求 const response = await fetch(url, requestOptions) - + // 处理API返回的错误 if (!response.ok) { // 尝试解析错误信息 let errorMessage = `请求失败: ${response.status}` + let errorData = null + try { - const errorData = await response.json() + errorData = await response.json() errorMessage = errorData.detail || errorData.message || errorMessage } catch (e) { // 如果无法解析JSON,使用默认错误信息 @@ -54,9 +56,19 @@ export async function apiRequest(url, options = {}, requiresAuth = false) { const userStore = useUserStore() if (userStore.isLoggedIn) { // 如果用户认为自己已登录,但收到401,则可能是令牌过期 - message.error('登录已过期,请重新登录') + const isTokenExpired = errorData && + (errorData.detail?.includes('令牌已过期') || + errorData.detail?.includes('token expired') || + errorMessage?.includes('令牌已过期') || + errorMessage?.includes('token expired')) + + message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录') userStore.logout() - window.location.href = '/login' + + // 使用setTimeout确保消息显示后再跳转 + setTimeout(() => { + window.location.href = '/login' + }, 1500) } throw new Error('未授权,请先登录') } else if (response.status === 403) { @@ -81,7 +93,7 @@ export async function apiRequest(url, options = {}, requiresAuth = false) { /** * 发送GET请求 - * @param {string} url - API端点 + * @param {string} url - API端点 * @param {Object} options - 请求选项 * @param {boolean} requiresAuth - 是否需要认证 * @returns {Promise} - 请求结果 @@ -100,12 +112,12 @@ export function apiGet(url, options = {}, requiresAuth = false) { */ export function apiPost(url, data = {}, options = {}, requiresAuth = false) { return apiRequest( - url, - { - method: 'POST', + url, + { + method: 'POST', body: JSON.stringify(data), - ...options - }, + ...options + }, requiresAuth ) } @@ -120,12 +132,12 @@ export function apiPost(url, data = {}, options = {}, requiresAuth = false) { */ export function apiPut(url, data = {}, options = {}, requiresAuth = false) { return apiRequest( - url, - { - method: 'PUT', + url, + { + method: 'PUT', body: JSON.stringify(data), - ...options - }, + ...options + }, requiresAuth ) } @@ -139,4 +151,4 @@ export function apiPut(url, data = {}, options = {}, requiresAuth = false) { */ export function apiDelete(url, options = {}, requiresAuth = false) { return apiRequest(url, { method: 'DELETE', ...options }, requiresAuth) -} \ No newline at end of file +} \ No newline at end of file diff --git a/web/src/apis/index.js b/web/src/apis/index.js index ffa124cc..6acfb3fc 100644 --- a/web/src/apis/index.js +++ b/web/src/apis/index.js @@ -17,17 +17,17 @@ export { apiRequest, apiGet, apiPost, apiPut, apiDelete } from './base' /** * 权限说明: - * + * * 1. public_api.js: 不需要认证就可以访问的API * - 登录、初始化管理员、获取公共配置等 - * + * * 2. auth_api.js: 需要用户认证才能访问的API * - 权限要求: 任何已登录用户(普通用户、管理员、超级管理员) * - 聊天功能、个人设置等 - * + * * 3. admin_api.js: 需要管理员权限才能访问的API * - 权限要求: admin 或 superadmin * - 用户管理、知识库管理、系统配置等 - * + * * 注意:本模块已处理权限验证和请求头,使用时无需再手动添加认证头 - */ \ No newline at end of file + */ \ No newline at end of file diff --git a/web/src/apis/public_api.js b/web/src/apis/public_api.js index 88d759de..66cb0412 100644 --- a/web/src/apis/public_api.js +++ b/web/src/apis/public_api.js @@ -16,7 +16,7 @@ export const authApi = { const formData = new FormData() formData.append('username', credentials.username) formData.append('password', credentials.password) - + return apiRequest('/api/auth/token', { method: 'POST', body: formData @@ -56,4 +56,4 @@ export const healthApi = { } // 从base.js导入apiRequest以支持FormData -import { apiRequest } from './base' \ No newline at end of file +import { apiRequest } from './base' \ No newline at end of file diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 6ca3c1b0..df496bd2 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -12,11 +12,11 @@
-
@@ -359,14 +359,7 @@ const sendMessageWithText = async (text) => { }; // 发送请求 - const response = await fetch(`/api/chat/agent/${currentAgent.value.name}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...userStore.getAuthHeaders() - }, - body: JSON.stringify(requestData) - }); + const response = await chatApi.sendAgentMessage(currentAgent.value.name, requestData); // console.log("requestData", requestData); if (!response.ok) { diff --git a/web/src/components/ChatComponent.vue b/web/src/components/ChatComponent.vue index b314cfc4..546ce5dd 100644 --- a/web/src/components/ChatComponent.vue +++ b/web/src/components/ChatComponent.vue @@ -190,6 +190,7 @@ import MessageInputComponent from '@/components/MessageInputComponent.vue' import MessageComponent from '@/components/MessageComponent.vue' import RefsSidebar from '@/components/RefsSidebar.vue' import { chatApi } from '@/apis/auth_api' +import { knowledgeBaseApi } from '@/apis/admin_api' const props = defineProps({ conv: Object, @@ -479,18 +480,24 @@ const groupRefs = (id) => { } const loadDatabases = () => { - fetch('/api/data/', { - method: "GET", - headers: userStore.getAuthHeaders() - }) - .then(response => response.json()) - .then(data => { - console.log(data) - opts.databases = data.databases - }) - .catch(error => { - console.error('加载数据库列表失败:', error) - }) + // 由于这是管理功能,需要检查用户是否有管理权限 + if (!userStore.isAdmin) { + console.log('非管理员用户,跳过加载数据库列表'); + return; + } + + try { + knowledgeBaseApi.getDatabases() + .then(data => { + console.log(data) + opts.databases = data.databases + }) + .catch(error => { + console.error('加载数据库列表失败:', error) + }) + } catch (error) { + console.error('获取数据库列表失败:', error); + } } const simpleCall = (msg) => { @@ -514,20 +521,27 @@ const fetchChatResponse = (user_input, cur_res_id) => { } console.log(params) - // 使用fetch带上认证头和信号控制 - fetch('/api/chat/', { - method: 'POST', - body: JSON.stringify(params), - headers: { - 'Content-Type': 'application/json', - ...userStore.getAuthHeaders() - }, - signal // 添加 signal 用于中断请求 - }) + // 使用API函数发送请求 + chatApi.sendMessageWithAbort(params, signal) .then((response) => { if (!response.ok) { - throw new Error(`请求失败: ${response.status} ${response.statusText}`) + // 检查是否是401错误(令牌过期) + if (response.status === 401) { + const userStore = useUserStore(); + if (userStore.isLoggedIn) { + message.error('登录已过期,请重新登录'); + userStore.logout(); + + // 使用setTimeout确保消息显示后再跳转 + setTimeout(() => { + window.location.href = '/login'; + }, 1500); + } + throw new Error('未授权,请先登录'); + } + throw new Error(`请求失败: ${response.status} ${response.statusText}`); } + if (!response.body) throw new Error("ReadableStream not supported."); const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); @@ -568,8 +582,6 @@ const fetchChatResponse = (user_input, cur_res_id) => { meta: data.meta, ...data, }); - // console.log("Last message", conv.value.messages[conv.value.messages.length - 1].content) - // console.log("Last message", conv.value.messages[conv.value.messages.length - 1].status) if (data.history) { conv.value.history = data.history; @@ -592,12 +604,18 @@ const fetchChatResponse = (user_input, cur_res_id) => { if (error.name === 'AbortError') { console.log('Fetch aborted'); } else { - console.error(error); - updateMessage({ - id: cur_res_id, - status: "error", - message: error.message || '请求失败', - }); + console.error('聊天请求错误:', error); + + // 检查是否是认证错误 + if (error.message.includes('未授权') || error.message.includes('令牌已过期')) { + // 已在上面处理,这里不需要重复处理 + } else { + updateMessage({ + id: cur_res_id, + status: "error", + message: error.message || '请求失败', + }); + } } isStreaming.value = false; }); diff --git a/web/src/components/DebugComponent.vue b/web/src/components/DebugComponent.vue index 07b0d9ee..d7bb3640 100644 --- a/web/src/components/DebugComponent.vue +++ b/web/src/components/DebugComponent.vue @@ -22,7 +22,7 @@ {{ state.isFullscreen ? '退出全屏' : '全屏' }} - @@ -72,9 +72,11 @@ diff --git a/web/src/components/UserInfoComponent.vue b/web/src/components/UserInfoComponent.vue index 9afe0d94..5f556589 100644 --- a/web/src/components/UserInfoComponent.vue +++ b/web/src/components/UserInfoComponent.vue @@ -1,12 +1,15 @@