diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ec47d43..bad250f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "knowledage-base", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "knowledage-base", - "version": "0.0.0", + "version": "0.1.0", "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", "axios": "^1.8.4", diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts new file mode 100644 index 0000000..bc2a8d0 --- /dev/null +++ b/frontend/src/api/auth/index.ts @@ -0,0 +1,234 @@ +import { post, get, put } from '@/utils/request' + +// 用户登录接口 +export interface LoginRequest { + email: string + password: string +} + +export interface LoginResponse { + success: boolean + message?: string + user?: { + id: string + username: string + email: string + avatar?: string + tenant_id: number + is_active: boolean + created_at: string + updated_at: string + } + tenant?: { + id: number + name: string + description: string + api_key: string + status: string + business: string + storage_quota: number + storage_used: number + created_at: string + updated_at: string + } + token?: string + refresh_token?: string +} + +// 用户注册接口 +export interface RegisterRequest { + username: string + email: string + password: string +} + +export interface RegisterResponse { + success: boolean + message?: string + data?: { + user: { + id: string + username: string + email: string + } + tenant: { + id: string + name: string + api_key: string + } + } +} + +// 用户信息接口 +export interface UserInfo { + id: string + username: string + email: string + avatar?: string + tenant_id: string + created_at: string + updated_at: string +} + +// 租户信息接口 +export interface TenantInfo { + id: string + name: string + api_key: string + owner_id: string + created_at: string + updated_at: string + knowledge_bases?: KnowledgeBaseInfo[] +} + +// 知识库信息接口 +export interface KnowledgeBaseInfo { + id: string + name: string + description: string + tenant_id: string + created_at: string + updated_at: string + document_count?: number + chunk_count?: number +} + +// 模型信息接口 +export interface ModelInfo { + id: string + name: string + type: string + source: string + description?: string + is_default?: boolean + created_at: string + updated_at: string +} + +/** + * 用户登录 + */ +export async function login(data: LoginRequest): Promise { + try { + const response = await post('/api/v1/auth/login', data) + return response as unknown as LoginResponse + } catch (error: any) { + return { + success: false, + message: error.message || '登录失败' + } + } +} + +/** + * 用户注册 + */ +export async function register(data: RegisterRequest): Promise { + try { + const response = await post('/api/v1/auth/register', data) + return response as unknown as RegisterResponse + } catch (error: any) { + return { + success: false, + message: error.message || '注册失败' + } + } +} + +/** + * 获取当前用户信息 + */ +export async function getCurrentUser(): Promise<{ success: boolean; data?: UserInfo; message?: string }> { + try { + const response = await get('/api/v1/auth/me') + return response as unknown as { success: boolean; data?: UserInfo; message?: string } + } catch (error: any) { + return { + success: false, + message: error.message || '获取用户信息失败' + } + } +} + +/** + * 获取当前租户信息 + */ +export async function getCurrentTenant(): Promise<{ success: boolean; data?: TenantInfo; message?: string }> { + try { + const response = await get('/api/v1/auth/tenant') + return response as unknown as { success: boolean; data?: TenantInfo; message?: string } + } catch (error: any) { + return { + success: false, + message: error.message || '获取租户信息失败' + } + } +} + +/** + * 刷新Token + */ +export async function refreshToken(refreshToken: string): Promise<{ success: boolean; data?: { token: string; refreshToken: string }; message?: string }> { + try { + const response: any = await post('/api/v1/auth/refresh', { refreshToken }) + if (response && response.success) { + if (response.access_token || response.refresh_token) { + return { + success: true, + data: { + token: response.access_token, + refreshToken: response.refresh_token, + } + } + } + } + + // 其他情况直接返回原始消息 + return { + success: false, + message: response?.message || '刷新Token失败' + } + } catch (error: any) { + return { + success: false, + message: error.message || '刷新Token失败' + } + } +} + +/** + * 用户登出 + */ +export async function logout(): Promise<{ success: boolean; message?: string }> { + try { + await post('/api/v1/auth/logout', {}) + return { + success: true + } + } catch (error: any) { + return { + success: false, + message: error.message || '登出失败' + } + } +} + +/** + * 验证Token有效性 + */ +export async function validateToken(): Promise<{ success: boolean; valid?: boolean; message?: string }> { + try { + const response = await get('/api/v1/auth/validate') + return response as unknown as { success: boolean; valid?: boolean; message?: string } + } catch (error: any) { + return { + success: false, + valid: false, + message: error.message || 'Token验证失败' + } + } +} + + + + diff --git a/frontend/src/api/chat/index.ts b/frontend/src/api/chat/index.ts index c948eca..32f0b17 100644 --- a/frontend/src/api/chat/index.ts +++ b/frontend/src/api/chat/index.ts @@ -1,54 +1,30 @@ import { get, post, put, del, postChat } from "../../utils/request"; import { loadTestData } from "../test-data"; -// 从localStorage获取设置 -function getSettings() { - const settingsStr = localStorage.getItem("WeKnora_settings"); - if (settingsStr) { - try { - const settings = JSON.parse(settingsStr); - if (settings.apiKey && settings.endpoint) { - return settings; - } - } catch (e) { - console.error("解析设置失败:", e); - } - } - return null; -} -// 根据是否有设置决定是否需要加载测试数据 -async function ensureConfigured() { - const settings = getSettings(); - // 如果没有设置APIKey和Endpoint,则加载测试数据 - if (!settings) { - await loadTestData(); - } -} export async function createSessions(data = {}) { - await ensureConfigured(); + await loadTestData(); return post("/api/v1/sessions", data); } export async function getSessionsList(page: number, page_size: number) { - await ensureConfigured(); + await loadTestData(); return get(`/api/v1/sessions?page=${page}&page_size=${page_size}`); } export async function generateSessionsTitle(session_id: string, data: any) { - await ensureConfigured(); + await loadTestData(); return post(`/api/v1/sessions/${session_id}/generate_title`, data); } export async function knowledgeChat(data: { session_id: string; query: string; }) { - await ensureConfigured(); + await loadTestData(); return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query }); } export async function getMessageList(data: { session_id: string; limit: number, created_at: string }) { - await ensureConfigured(); - + await loadTestData(); if (data.created_at) { return get(`/api/v1/messages/${data.session_id}/load?before_time=${encodeURIComponent(data.created_at)}&limit=${data.limit}`); } else { @@ -57,6 +33,6 @@ export async function getMessageList(data: { session_id: string; limit: number, } export async function delSession(session_id: string) { - await ensureConfigured(); + await loadTestData(); return del(`/api/v1/sessions/${session_id}`); } \ No newline at end of file diff --git a/frontend/src/api/chat/streame.ts b/frontend/src/api/chat/streame.ts index 562fb16..f7d8dae 100644 --- a/frontend/src/api/chat/streame.ts +++ b/frontend/src/api/chat/streame.ts @@ -2,21 +2,9 @@ import { fetchEventSource } from '@microsoft/fetch-event-source' import { ref, type Ref, onUnmounted, nextTick } from 'vue' import { generateRandomString } from '@/utils/index'; import { getTestData } from '@/utils/request'; -import { loadTestData } from '@/api/test-data'; +import { loadTestData } from "../test-data"; + -// 从localStorage获取设置 -function getSettings() { - const settingsStr = localStorage.getItem("WeKnora_settings"); - if (settingsStr) { - try { - const settings = JSON.parse(settingsStr); - return settings; - } catch (e) { - console.error("解析设置失败:", e); - } - } - return null; -} interface StreamOptions { // 请求方法 (默认POST) @@ -49,27 +37,16 @@ export function useStream() { isStreaming.value = true; isLoading.value = true; - // 获取设置信息 - const settings = getSettings(); - let apiUrl = ''; - let apiKey = ''; - - // 如果有设置信息,优先使用设置信息 - if (settings && settings.endpoint && settings.apiKey) { - apiUrl = settings.endpoint; - apiKey = settings.apiKey; - } else { - // 否则加载测试数据 - await loadTestData(); - const testData = getTestData(); - if (!testData) { - error.value = "测试数据未初始化,无法进行聊天"; - stopStream(); - return; - } - apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080"; - apiKey = testData.tenant.api_key; + // 使用默认配置 + await loadTestData(); + const testData = getTestData(); + if (!testData) { + error.value = "测试数据未初始化,无法进行聊天"; + stopStream(); + return; } + const apiUrl = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080"; + const apiKey = testData.tenant.api_key; try { let url = diff --git a/frontend/src/api/initialization/index.ts b/frontend/src/api/initialization/index.ts index fb5718b..6d1f353 100644 --- a/frontend/src/api/initialization/index.ts +++ b/frontend/src/api/initialization/index.ts @@ -70,6 +70,11 @@ export function checkInitializationStatus(): Promise<{ initialized: boolean }> { resolve(response.data || { initialized: false }); }) .catch((error: any) => { + // 如果是401,交给全局拦截器去处理(重定向登录),这里不要把它当成未初始化 + if (error && error.status === 401) { + reject(error); + return; + } console.warn('检查初始化状态失败,假设需要初始化:', error); resolve({ initialized: false }); }); diff --git a/frontend/src/api/knowledge-base/index.ts b/frontend/src/api/knowledge-base/index.ts index abee9d7..557a209 100644 --- a/frontend/src/api/knowledge-base/index.ts +++ b/frontend/src/api/knowledge-base/index.ts @@ -1,41 +1,23 @@ import { get, post, put, del, postUpload, getDown, getTestData } from "../../utils/request"; import { loadTestData } from "../test-data"; - -// 获取知识库ID(优先从设置中获取) -async function getKnowledgeBaseID() { - // 从localStorage获取设置中的知识库ID - const settingsStr = localStorage.getItem("WeKnora_settings"); - let knowledgeBaseId = ""; - - if (settingsStr) { - try { - const settings = JSON.parse(settingsStr); - if (settings.knowledgeBaseId) { - return settings.knowledgeBaseId; - } - } catch (e) { - console.error("解析设置失败:", e); - } - } - +export async function getDefaultKnowledgeBaseId(): Promise { // 如果设置中没有知识库ID,则使用测试数据 await loadTestData(); - const testData = getTestData(); if (!testData || testData.knowledge_bases.length === 0) { - console.error("测试数据未初始化或不包含知识库"); - throw new Error("测试数据未初始化或不包含知识库"); + throw new Error('没有可用的知识库'); } + return testData.knowledge_bases[0].id; } export async function uploadKnowledgeBase(data = {}) { - const kbId = await getKnowledgeBaseID(); + const kbId = await getDefaultKnowledgeBaseId(); return postUpload(`/api/v1/knowledge-bases/${kbId}/knowledge/file`, data); } -export async function getKnowledgeBase({page, page_size}) { - const kbId = await getKnowledgeBaseID(); +export async function getKnowledgeBase({page, page_size}: {page: number, page_size: number}) { + const kbId = await getDefaultKnowledgeBaseId(); return get( `/api/v1/knowledge-bases/${kbId}/knowledge?page=${page}&page_size=${page_size}` ); @@ -57,6 +39,6 @@ export function batchQueryKnowledge(ids: any) { return get(`/api/v1/knowledge/batch?${ids}`); } -export function getKnowledgeDetailsCon(id: any, page) { +export function getKnowledgeDetailsCon(id: any, page: number) { return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`); } \ No newline at end of file diff --git a/frontend/src/api/test-data/index.ts b/frontend/src/api/test-data/index.ts index c4d43ae..ef99e45 100644 --- a/frontend/src/api/test-data/index.ts +++ b/frontend/src/api/test-data/index.ts @@ -53,3 +53,12 @@ export async function loadTestData(): Promise { return false; } } + +/** + * 重置测试数据加载状态,在重新登录或需要强制刷新时调用 + */ +export function resetTestDataLoaded() { + isTestDataLoaded = false; + // 清空已缓存的测试数据,确保下次调用会重新获取 + setTestData(null); +} diff --git a/frontend/src/assets/img/logout.svg b/frontend/src/assets/img/logout.svg new file mode 100644 index 0000000..710d939 --- /dev/null +++ b/frontend/src/assets/img/logout.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/components/menu.vue b/frontend/src/components/menu.vue index 4ebeacc..7627737 100644 --- a/frontend/src/components/menu.vue +++ b/frontend/src/components/menu.vue @@ -9,7 +9,7 @@ :class="['menu_item', item.childrenPath && item.childrenPath == currentpath ? 'menu_item_c_active' : item.path == currentpath ? 'menu_item_active' : '']"> @@ -58,11 +58,13 @@ import { onMounted, watch, computed, ref, reactive } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { getSessionsList, delSession } from "@/api/chat/index"; import { useMenuStore } from '@/stores/menu'; +import { useAuthStore } from '@/stores/auth'; import useKnowledgeBase from '@/hooks/useKnowledgeBase'; import { MessagePlugin } from "tdesign-vue-next"; let { requestMethod } = useKnowledgeBase() let uploadInput = ref(); const usemenuStore = useMenuStore(); +const authStore = useAuthStore(); const route = useRoute(); const router = useRouter(); const currentpath = ref(''); @@ -164,12 +166,14 @@ let fileAddIcon = ref('file-add-green.svg'); let knowledgeIcon = ref('zhishiku-green.svg'); let prefixIcon = ref('prefixIcon.svg'); let settingIcon = ref('setting.svg'); +let logoutIcon = ref('logout.svg'); let pathPrefix = ref(route.name) const getIcon = (path) => { fileAddIcon.value = path == 'knowledgeBase' ? 'file-add-green.svg' : 'file-add.svg'; knowledgeIcon.value = path == 'knowledgeBase' ? 'zhishiku-green.svg' : 'zhishiku.svg'; prefixIcon.value = path == 'creatChat' ? 'prefixIcon-green.svg' : path == 'knowledgeBase' ? 'prefixIcon-grey.svg' : 'prefixIcon.svg'; settingIcon.value = path == 'settings' ? 'setting-green.svg' : 'setting.svg'; + logoutIcon.value = 'logout.svg'; } getIcon(route.name) const gotopage = (path) => { @@ -177,6 +181,13 @@ const gotopage = (path) => { // 如果是系统设置,跳转到初始化配置页面 if (path === 'settings') { router.push('/initialization'); + return; + } + // 处理退出登录 + if (path === 'logout') { + authStore.logout(); + router.push('/login'); + return; } else { router.push(`/platform/${path}`); } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 6e1d7b8..9ec0e55 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,12 +1,20 @@ import { createRouter, createWebHistory } from 'vue-router' import { checkInitializationStatus } from '@/api/initialization' +import { useAuthStore } from '@/stores/auth' +import { validateToken } from '@/api/auth' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/", - redirect: "/platform", + redirect: "/platform/knowledgeBase", + }, + { + path: "/login", + name: "login", + component: () => import("../views/auth/Login.vue"), + meta: { requiresAuth: false, requiresInit: false } }, { path: "/initialization", @@ -18,71 +26,110 @@ const router = createRouter({ path: "/knowledgeBase", name: "home", component: () => import("../views/knowledge/KnowledgeBase.vue"), - meta: { requiresInit: true } + meta: { requiresInit: true, requiresAuth: true } }, { path: "/platform", name: "Platform", redirect: "/platform/knowledgeBase", component: () => import("../views/platform/index.vue"), - meta: { requiresInit: true }, + meta: { requiresInit: true, requiresAuth: true }, children: [ { path: "knowledgeBase", name: "knowledgeBase", component: () => import("../views/knowledge/KnowledgeBase.vue"), - meta: { requiresInit: true } + meta: { requiresInit: true, requiresAuth: true } }, { path: "creatChat", name: "creatChat", component: () => import("../views/creatChat/creatChat.vue"), - meta: { requiresInit: true } + meta: { requiresInit: true, requiresAuth: true } }, { path: "chat/:chatid", name: "chat", component: () => import("../views/chat/index.vue"), - meta: { requiresInit: true } + meta: { requiresInit: true, requiresAuth: true } }, { - path: "settings", - name: "settings", - component: () => import("../views/settings/Settings.vue"), + path: "settings", + name: "settings", + component: () => import("../views/settings/Settings.vue"), meta: { requiresInit: true } - }, + }, ], }, ], }); -// 路由守卫:检查系统初始化状态 +// 路由守卫:检查认证状态和系统初始化状态 router.beforeEach(async (to, from, next) => { - // 如果访问的是初始化页面,直接放行 - if (to.meta.requiresInit === false) { - next(); - return; + const authStore = useAuthStore() + + // 如果访问的是登录页面或初始化页面,直接放行 + if (to.meta.requiresAuth === false || to.meta.requiresInit === false) { + // 如果已登录用户访问登录页面,重定向到知识库列表页面 + if (to.path === '/login' && authStore.isLoggedIn) { + next('/platform/knowledgeBase') + return + } + next() + return } -1 - - try { - // 检查系统是否已初始化 - const { initialized } = await checkInitializationStatus(); - - if (initialized) { - // 系统已初始化,记录到本地存储并正常跳转 - localStorage.setItem('system_initialized', 'true'); - next(); - } else { - // 系统未初始化,跳转到初始化页面 - console.log('系统未初始化,跳转到初始化页面'); - next('/initialization'); + // 检查用户认证状态 + if (to.meta.requiresAuth !== false) { + if (!authStore.isLoggedIn) { + // 未登录,跳转到登录页面 + next('/login') + return } - } catch (error) { - console.error('检查初始化状态失败:', error); - // 如果检查失败,默认认为需要初始化 - next('/initialization'); + + // 验证Token有效性 + // try { + // const { valid } = await validateToken() + // if (!valid) { + // // Token无效,清空认证信息并跳转到登录页面 + // authStore.logout() + // next('/login') + // return + // } + // } catch (error) { + // console.error('Token验证失败:', error) + // authStore.logout() + // next('/login') + // return + // } + } + + // 检查系统初始化状态 + if (to.meta.requiresInit !== false) { + try { + const { initialized } = await checkInitializationStatus() + + if (initialized) { + // 系统已初始化,记录到本地存储并正常跳转 + localStorage.setItem('system_initialized', 'true') + next() + } else { + // 系统未初始化,跳转到初始化页面 + next('/initialization') + } + } catch (error) { + console.error('检查初始化状态失败:', error) + // 如果是401,跳转登录,不再误导去初始化 + const status = (error as any)?.status + if (status === 401) { + next('/login') + return + } + // 其他错误默认认为需要初始化 + next('/initialization') + } + } else { + next() } }); diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts new file mode 100644 index 0000000..221864a --- /dev/null +++ b/frontend/src/stores/auth.ts @@ -0,0 +1,174 @@ +import { defineStore } from 'pinia' +import { resetTestDataLoaded } from '@/api/test-data' +import { ref, computed } from 'vue' +import type { UserInfo, TenantInfo, KnowledgeBaseInfo } from '@/api/auth' + +export const useAuthStore = defineStore('auth', () => { + // 状态 + const user = ref(null) + const tenant = ref(null) + const token = ref('') + const refreshToken = ref('') + const knowledgeBases = ref([]) + const currentKnowledgeBase = ref(null) + + // 计算属性 + const isLoggedIn = computed(() => { + return !!token.value && !!user.value + }) + + const hasValidTenant = computed(() => { + return !!tenant.value && !!tenant.value.api_key + }) + + const currentTenantId = computed(() => { + return tenant.value?.id || '' + }) + + const currentUserId = computed(() => { + return user.value?.id || '' + }) + + // 操作方法 + const setUser = (userData: UserInfo) => { + user.value = userData + // 保存到localStorage + localStorage.setItem('weknora_user', JSON.stringify(userData)) + } + + const setTenant = (tenantData: TenantInfo) => { + tenant.value = tenantData + // 保存到localStorage + localStorage.setItem('weknora_tenant', JSON.stringify(tenantData)) + } + + const setToken = (tokenValue: string) => { + token.value = tokenValue + localStorage.setItem('weknora_token', tokenValue) + } + + const setRefreshToken = (refreshTokenValue: string) => { + refreshToken.value = refreshTokenValue + localStorage.setItem('weknora_refresh_token', refreshTokenValue) + } + + const setKnowledgeBases = (kbList: KnowledgeBaseInfo[]) => { + // 确保输入是数组 + knowledgeBases.value = Array.isArray(kbList) ? kbList : [] + localStorage.setItem('weknora_knowledge_bases', JSON.stringify(knowledgeBases.value)) + } + + const setCurrentKnowledgeBase = (kb: KnowledgeBaseInfo | null) => { + currentKnowledgeBase.value = kb + if (kb) { + localStorage.setItem('weknora_current_kb', JSON.stringify(kb)) + } else { + localStorage.removeItem('weknora_current_kb') + } + } + + + const logout = () => { + // 清空状态 + user.value = null + tenant.value = null + token.value = '' + refreshToken.value = '' + knowledgeBases.value = [] + currentKnowledgeBase.value = null + + // 清空localStorage + localStorage.removeItem('weknora_user') + localStorage.removeItem('weknora_tenant') + localStorage.removeItem('weknora_token') + localStorage.removeItem('weknora_refresh_token') + localStorage.removeItem('weknora_knowledge_bases') + localStorage.removeItem('weknora_current_kb') + + // 重置测试数据加载标志,确保重新登录后会重新获取KB列表 + try { + resetTestDataLoaded() + } catch {} + } + + const initFromStorage = () => { + // 从localStorage恢复状态 + const storedUser = localStorage.getItem('weknora_user') + const storedTenant = localStorage.getItem('weknora_tenant') + const storedToken = localStorage.getItem('weknora_token') + const storedRefreshToken = localStorage.getItem('weknora_refresh_token') + const storedKnowledgeBases = localStorage.getItem('weknora_knowledge_bases') + const storedCurrentKb = localStorage.getItem('weknora_current_kb') + + if (storedUser) { + try { + user.value = JSON.parse(storedUser) + } catch (e) { + console.error('解析用户信息失败:', e) + } + } + + if (storedTenant) { + try { + tenant.value = JSON.parse(storedTenant) + } catch (e) { + console.error('解析租户信息失败:', e) + } + } + + if (storedToken) { + token.value = storedToken + } + + if (storedRefreshToken) { + refreshToken.value = storedRefreshToken + } + + if (storedKnowledgeBases) { + try { + const parsed = JSON.parse(storedKnowledgeBases) + knowledgeBases.value = Array.isArray(parsed) ? parsed : [] + } catch (e) { + console.error('解析知识库列表失败:', e) + knowledgeBases.value = [] + } + } + + if (storedCurrentKb) { + try { + currentKnowledgeBase.value = JSON.parse(storedCurrentKb) + } catch (e) { + console.error('解析当前知识库失败:', e) + } + } + } + + // 初始化时从localStorage恢复状态 + initFromStorage() + + return { + // 状态 + user, + tenant, + token, + refreshToken, + knowledgeBases, + currentKnowledgeBase, + + // 计算属性 + isLoggedIn, + hasValidTenant, + currentTenantId, + currentUserId, + + // 方法 + setUser, + setTenant, + setToken, + setRefreshToken, + setKnowledgeBases, + setCurrentKnowledgeBase, + logout, + initFromStorage + } +}) \ No newline at end of file diff --git a/frontend/src/stores/menu.ts b/frontend/src/stores/menu.ts index 128dbbd..a50e0f2 100644 --- a/frontend/src/stores/menu.ts +++ b/frontend/src/stores/menu.ts @@ -13,7 +13,8 @@ export const useMenuStore = defineStore('menuStore', { childrenPath: 'chat', children: reactive([]), }, - { title: '系统设置', icon: 'setting', path: 'settings' } + { title: '系统设置', icon: 'setting', path: 'settings' }, + { title: '退出登录', icon: 'logout', path: 'logout' } ]), isFirstSession: false, firstQuery: '' diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 16c7e12..2e906be 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -2,26 +2,8 @@ import axios from "axios"; import { generateRandomString } from "./index"; -// 从localStorage获取设置 -function getSettings() { - const settingsStr = localStorage.getItem("WeKnora_settings"); - if (settingsStr) { - try { - return JSON.parse(settingsStr); - } catch (e) { - console.error("解析设置失败:", e); - } - } - return { - endpoint: import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080", - apiKey: "", - knowledgeBaseId: "", - }; -} - -// API基础URL,优先使用设置中的endpoint -const settings = getSettings(); -const BASE_URL = settings.endpoint; +// API基础URL +const BASE_URL = import.meta.env.VITE_IS_DOCKER ? "" : "http://localhost:8080"; // 测试数据 let testData: { @@ -50,13 +32,6 @@ const instance = axios.create({ // 设置测试数据 export function setTestData(data: typeof testData) { testData = data; - if (data) { - // 优先使用设置中的ApiKey,如果没有则使用测试数据中的 - const apiKey = settings.apiKey || (data?.tenant?.api_key || ""); - if (apiKey) { - instance.defaults.headers["X-API-Key"] = apiKey; - } - } } // 获取测试数据 @@ -66,25 +41,38 @@ export function getTestData() { instance.interceptors.request.use( (config) => { - // 每次请求前检查是否有更新的设置 - const currentSettings = getSettings(); - - // 更新BaseURL (如果有变化) - if (currentSettings.endpoint && config.baseURL !== currentSettings.endpoint) { - config.baseURL = currentSettings.endpoint; - } - - // 更新API Key (如果有) - if (currentSettings.apiKey) { - config.headers["X-API-Key"] = currentSettings.apiKey; + // 添加JWT token认证 + const token = localStorage.getItem('weknora_token'); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; } config.headers["X-Request-ID"] = `${generateRandomString(12)}`; return config; }, - (error) => {} + (error) => { + return Promise.reject(error); + } ); +// Token刷新标志,防止多个请求同时刷新token +let isRefreshing = false; +let failedQueue: Array<{ resolve: Function; reject: Function }> = []; +let hasRedirectedOn401 = false; + +// 处理队列中的请求 +const processQueue = (error: any, token: string | null = null) => { + failedQueue.forEach(({ resolve, reject }) => { + if (error) { + reject(error); + } else { + resolve(token); + } + }); + + failedQueue = []; +}; + instance.interceptors.response.use( (response) => { // 根据业务状态码处理逻辑 @@ -95,12 +83,98 @@ instance.interceptors.response.use( return Promise.reject(data); } }, - (error: any) => { + async (error: any) => { + const originalRequest = error.config; + if (!error.response) { return Promise.reject({ message: "网络错误,请检查您的网络连接" }); } - const { data } = error.response; - return Promise.reject(data); + + // 如果是登录接口的401,直接返回错误以便页面展示toast,不做跳转 + if (error.response.status === 401 && originalRequest?.url?.includes('/auth/login')) { + const { status, data } = error.response; + return Promise.reject({ status, message: (typeof data === 'object' ? data?.message : data) || '用户名或密码错误' }); + } + + // 如果是401错误且不是刷新token的请求,尝试刷新token + if (error.response.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/refresh')) { + if (isRefreshing) { + // 如果正在刷新token,将请求加入队列 + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }).then(token => { + originalRequest.headers['Authorization'] = 'Bearer ' + token; + return instance(originalRequest); + }).catch(err => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + const refreshToken = localStorage.getItem('weknora_refresh_token'); + + if (refreshToken) { + try { + // 动态导入refresh token API + const { refreshToken: refreshTokenAPI } = await import('../api/auth/index'); + const response = await refreshTokenAPI(refreshToken); + + if (response.success && response.data) { + const { token, refreshToken: newRefreshToken } = response.data; + + // 更新localStorage中的token + localStorage.setItem('weknora_token', token); + localStorage.setItem('weknora_refresh_token', newRefreshToken); + + // 更新请求头 + originalRequest.headers['Authorization'] = 'Bearer ' + token; + + // 处理队列中的请求 + processQueue(null, token); + + return instance(originalRequest); + } else { + throw new Error(response.message || 'Token刷新失败'); + } + } catch (refreshError) { + // 刷新失败,清除所有token并跳转到登录页 + localStorage.removeItem('weknora_token'); + localStorage.removeItem('weknora_refresh_token'); + localStorage.removeItem('weknora_user'); + localStorage.removeItem('weknora_tenant'); + + processQueue(refreshError, null); + + // 跳转到登录页 + if (!hasRedirectedOn401 && typeof window !== 'undefined') { + hasRedirectedOn401 = true; + window.location.href = '/login'; + } + + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } else { + // 没有refresh token,直接跳转到登录页 + localStorage.removeItem('weknora_token'); + localStorage.removeItem('weknora_user'); + localStorage.removeItem('weknora_tenant'); + + if (!hasRedirectedOn401 && typeof window !== 'undefined') { + hasRedirectedOn401 = true; + window.location.href = '/login'; + } + + return Promise.reject({ message: '请重新登录' }); + } + } + + const { status, data } = error.response; + // 将HTTP状态码一并抛出,方便上层判断401等场景 + return Promise.reject({ status, ...(typeof data === 'object' ? data : { message: data }) }); } ); diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue new file mode 100644 index 0000000..336c40b --- /dev/null +++ b/frontend/src/views/auth/Login.vue @@ -0,0 +1,559 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/creatChat/creatChat.vue b/frontend/src/views/creatChat/creatChat.vue index f072089..268786e 100644 --- a/frontend/src/views/creatChat/creatChat.vue +++ b/frontend/src/views/creatChat/creatChat.vue @@ -27,33 +27,7 @@ const sendMsg = (value: string) => { } async function createNewSession(value: string) { - // 从localStorage获取设置中的知识库ID - const settingsStr = localStorage.getItem("WeKnora_settings"); - let knowledgeBaseId = ""; - - if (settingsStr) { - try { - const settings = JSON.parse(settingsStr); - if (settings.knowledgeBaseId) { - knowledgeBaseId = settings.knowledgeBaseId; - createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => { - if (res.data && res.data.id) { - getTitle(res.data.id, value); - } else { - // 错误处理 - console.error("创建会话失败"); - } - }).catch(error => { - console.error("创建会话出错:", error); - }); - return; - } - } catch (e) { - console.error("解析设置失败:", e); - } - } - - // 如果设置中没有知识库ID,则使用测试数据 + // 使用测试数据获取知识库ID const testData = getTestData(); if (!testData || testData.knowledge_bases.length === 0) { console.error("测试数据未初始化或不包含知识库"); @@ -61,7 +35,7 @@ async function createNewSession(value: string) { } // 使用第一个知识库ID - knowledgeBaseId = testData.knowledge_bases[0].id; + const knowledgeBaseId = testData.knowledge_bases[0].id; createSessions({ knowledge_base_id: knowledgeBaseId }).then(res => { if (res.data && res.data.id) { diff --git a/frontend/src/views/initialization/InitializationConfig.vue b/frontend/src/views/initialization/InitializationConfig.vue index 3bc0a09..8a82a71 100644 --- a/frontend/src/views/initialization/InitializationConfig.vue +++ b/frontend/src/views/initialization/InitializationConfig.vue @@ -17,6 +17,10 @@ {{ s.label }} + + + 退出登录 +
@@ -780,8 +784,10 @@ import { listOllamaModels, testEmbeddingModel } from '@/api/initialization'; +import { useAuthStore } from '@/stores/auth'; const router = useRouter(); +const authStore = useAuthStore(); type TFormRef = { validate: (fields?: string[] | undefined) => Promise; clearValidate?: (fields?: string | string[]) => void; @@ -956,6 +962,12 @@ const goToSection = (id: string) => { } }; +// 退出登录 +const handleLogout = () => { + authStore.logout(); + router.replace('/login'); +}; + // 监听滚动,高亮当前区块 const onScroll = () => { const order = ['ollama','llm','embedding','rerank','multimodal','docsplit','submit']; @@ -2335,6 +2347,27 @@ const detectEmbeddingDimension = async () => {