统一前端对于接口的使用

This commit is contained in:
Wenjie Zhang 2025-05-04 21:04:01 +08:00
parent 9fdc6a6b77
commit c6474f6585
22 changed files with 352 additions and 772 deletions

View File

@ -12,6 +12,12 @@
## 📝 项目概述
DEV 更新待办:
- 智能体的消息加载有问题
- 智能体的管理员的配置无法更新到用户层面
语析是一个强大的问答平台,结合了大模型 RAG 知识库与知识图谱技术,基于 Llamaindex + VueJS + FastAPI + Neo4j 构建。
**核心特点:**

View File

@ -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

View File

@ -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)
},
}
}

View File

@ -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可以继续添加到这里
// 其他需要用户认证的API可以继续添加到这里

View File

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

View File

@ -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
* - 用户管理知识库管理系统配置等
*
*
* 注意本模块已处理权限验证和请求头使用时无需再手动添加认证头
*/
*/

View File

@ -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'
import { apiRequest } from './base'

View File

@ -12,11 +12,11 @@
<slot name="header-center"></slot>
</div>
<div class="header__right">
<div class="current-agent nav-btn" @click="sayHi">
<!-- <div class="current-agent nav-btn" @click="sayHi">
<RobotOutlined />&nbsp;
<span v-if="currentAgent">{{ currentAgent.name }}</span>
<span v-else>加载中...</span>
</div>
</div> -->
<slot name="header-right"></slot>
</div>
</div>
@ -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) {

View File

@ -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;
});

View File

@ -22,7 +22,7 @@
{{ state.isFullscreen ? '退出全屏' : '全屏' }}
</a-button>
<a-tooltip :title="state.autoRefresh ? '点击停止自动刷新' : '点击开启自动刷新'">
<a-button
<a-button
:type="state.autoRefresh ? 'primary' : 'default'"
@click="toggleAutoRefresh(!state.autoRefresh)"
>
@ -72,9 +72,11 @@
<script setup>
import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive, computed } from 'vue';
import { useConfigStore } from '@/stores/config';
import { useUserStore } from '@/stores/user';
import { useThrottleFn } from '@vueuse/core';
import {
FullscreenOutlined,
import { message } from 'ant-design-vue';
import {
FullscreenOutlined,
FullscreenExitOutlined,
ReloadOutlined,
ClearOutlined,
@ -82,10 +84,21 @@ import {
SyncOutlined
} from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import { logApi } from '@/apis/admin_api';
const configStore = useConfigStore()
const userStore = useUserStore();
const config = configStore.config;
//
const checkAdminPermission = () => {
if (!userStore.isAdmin) {
message.error('需要管理员权限才能查看日志');
return false;
}
return true;
};
//
const logLevels = [
{ value: 'INFO', label: 'INFO' },
@ -153,16 +166,13 @@ const processedLogs = computed(() => {
//
const fetchLogs = async () => {
if (!checkAdminPermission()) return;
state.fetching = true;
try {
error.value = '';
const response = await fetch('/api/log');
if (!response.ok) {
throw new Error('获取日志失败');
}
const data = await response.json();
state.rawLogs = data.log.split('\n').filter(line => line.trim());
const logData = await logApi.getLogs();
state.rawLogs = logData.log.split('\n').filter(line => line.trim());
await nextTick();
const scrollToBottom = useThrottleFn(() => {
@ -180,6 +190,7 @@ const fetchLogs = async () => {
//
const clearLogs = () => {
if (!checkAdminPermission()) return;
state.rawLogs = [];
};
@ -195,6 +206,8 @@ const filterLogs = () => {
//
const toggleAutoRefresh = (value) => {
if (!checkAdminPermission()) return;
if (value) {
autoRefreshInterval = setInterval(fetchLogs, 5000);
state.autoRefresh = true;
@ -209,6 +222,8 @@ const toggleAutoRefresh = (value) => {
//
const toggleFullscreen = async () => {
if (!checkAdminPermission()) return;
try {
if (!state.isFullscreen) {
if (logViewer.value.requestFullscreen) {
@ -242,13 +257,17 @@ const handleFullscreenChange = () => {
};
onMounted(() => {
fetchLogs();
if (checkAdminPermission()) {
fetchLogs();
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
});
onActivated(() => {
if (!checkAdminPermission()) return;
if (state.autoRefresh) {
toggleAutoRefresh(true);
} else {
@ -267,6 +286,7 @@ onUnmounted(() => {
});
const printConfig = () => {
if (!checkAdminPermission()) return;
console.log('Current config:', config);
};
</script>

View File

@ -1,12 +1,15 @@
<template>
<div class="user-info-component">
<a-dropdown :trigger="['click']" v-if="userStore.isLoggedIn">
<div class="user-avatar">
<div class="user-initial">{{ userInitial }}</div>
<div class="user-role-badge" :class="userRoleClass"></div>
<div class="user-info-dropdown" :data-align="showRole ? 'left' : 'center'">
<div class="user-avatar">
<UserOutlined />
<div class="user-role-badge" :class="userRoleClass"></div>
</div>
<div v-if="showRole">{{ userStore.username }}</div>
</div>
<template #overlay>
<a-menu>
<a-menu>
<a-menu-item key="username" disabled>
<span class="user-menu-username">{{ userStore.username }}</span>
</a-menu-item>
@ -36,6 +39,13 @@ import { message } from 'ant-design-vue';
const router = useRouter();
const userStore = useUserStore();
const props = defineProps({
showRole: {
type: Boolean,
default: false
}
})
//
const userInitial = computed(() => {
if (!userStore.username) return '?';
@ -84,7 +94,22 @@ const goToLogin = () => {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
// margin-bottom: 16px;
}
.user-info-dropdown {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
&[data-align="center"] {
justify-content: center;
}
&[data-align="left"] {
justify-content: flex-start;
}
}
.user-avatar {

View File

@ -1,183 +0,0 @@
<template>
<div class="pdf2txt-container">
<div class="sidebar">
<div class="additional-params">
<h4>相关参数</h4>
<p>暂无相关参数</p>
</div>
</div>
<div class="result-container">
<div class="input-container">
<div class="upload">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:max-count="1"
:disabled="state.uploading"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
<p class="ant-upload-text">点击或者把PDF文件拖拽到这里上传</p>
<p class="ant-upload-hint">
仅支持上传PDF文件同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
<a-button type="primary" @click="convertPdfToText" :loading="state.loading">Convert PDF to Text</a-button>
</div>
<div class="output-container">
<textarea v-model="convertedText" placeholder="Converted text will appear here" readonly></textarea>
<div class="infos">
<span>字符数: {{ charCount }}</span>
<span>Token {{ estimatedTokenCount }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { message } from 'ant-design-vue';
const state = reactive({
loading: false,
uploading: false,
});
const fileList = ref([]);
const convertedText = ref('');
const charCount = computed(() => convertedText.value.length);
const estimatedTokenCount = computed(() => {
const chars = convertedText.value.split('');
let tokenCount = 0;
for (let char of chars) {
if (/[\u4e00-\u9fff]/.test(char)) {
tokenCount += 1;
} else if (/[a-zA-Z]/.test(char)) {
tokenCount += 0.25;
} else {
tokenCount += 0.5;
}
}
return Math.ceil(tokenCount);
});
const handleFileUpload = (info) => {
const { status } = info.file;
if (status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
};
const handleDrop = (e) => {
console.log(e);
};
const convertPdfToText = async () => {
if (fileList.value.length === 0) {
message.error("请上传PDF文件");
return;
}
const file = fileList.value[0].response.file_path;
try {
state.loading = true;
const response = await fetch('/api/tool/pdf2txt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(file.toString())
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
convertedText.value = data.text;
state.loading = false;
} catch (error) {
console.error('Error converting PDF to text:', error);
message.error('PDF转换失败请重试');
state.loading = false;
}
};
</script>
<style lang="less" scoped>
.pdf2txt-container {
display: flex;
border-radius: 8px;
font-family: 'Arial', sans-serif;
.sidebar {
position: sticky;
top: 0;
width: 350px;
background-color: var(--bg-sider);
border-right: 1px solid var(--main-light-3);
padding: 20px;
min-width: 250px;
flex: 1;
.additional-params {
h4 {
font-size: 1.2em;
margin-bottom: 10px;
}
}
}
.result-container {
flex: 3;
padding: 20px;
.input-container {
display: flex;
flex-direction: column;
margin-bottom: 15px;
.upload {
margin-bottom: 15px;
}
}
.output-container {
textarea {
width: 100%;
height: 300px;
resize: vertical;
padding: 1rem;
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
background-color: var(--gray-100);
&:focus {
border-color: var(--main-color);
outline: none;
}
}
.infos {
padding: 10px;
margin-top: 10px;
font-size: 1em;
color: var(--gray-800);
display: flex;
gap: 16px;
}
}
}
}
</style>

View File

@ -1,276 +0,0 @@
<template>
<div class="text-chunking-container">
<div class="sidebar">
<div class="additional-params">
<h4>相关参数</h4>
<a-form
:model="params"
name="basic"
autocomplete="off"
layout="vertical"
@finish="chunkText"
>
<a-form-item label="Chunk Size" name="chunkSize" >
<a-input v-model:value="params.chunkSize" :disabled="params.useParser && state.useFile" />
</a-form-item>
<a-form-item label="Chunk Overlap" name="chunkOverlap" >
<a-input v-model:value="params.chunkOverlap" :disabled="params.useParser && state.useFile" />
</a-form-item>
<a-form-item label="使用文件节点解析器" name="useParser" v-if="state.useFile">
<a-switch v-model:checked="params.useParser" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="state.loading">Chunk Text</a-button>
</a-form-item>
</a-form>
<!-- Future parameters can be added here -->
</div>
</div>
<div class="result-container">
<div class="input-container">
<div class="actions">
<span :class="{'active': !state.useFile}" @click="state.useFile = false">输入文本</span>
<span :class="{'active': state.useFile}" @click="state.useFile = true">上传文件</span>
</div>
<div class="upload" v-if="state.useFile">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:max-count="1"
:disabled="state.uploading"
action="/api/data/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
<p class="ant-upload-hint">
目前仅支持上传文本文件 .pdf, .txt, .md且同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
<textarea v-if="!state.useFile" v-model="text" placeholder="Enter text to chunk"></textarea>
<div class="infos" v-if="!state.useFile">
<!-- <span>字数: {{ wordCount }}</span> -->
<span>字符数: {{ charCount }}</span>
<span>Token {{ estimatedTokenCount }}</span>
</div>
</div>
<!-- <div>{{ chunks[0] }}</div> -->
<div id="result-cards" class="result-cards">
<div v-for="(chunk, index) in chunks" :key="index" class="chunk">
<p><strong>#{{ index + 1 }}</strong> {{ chunk.text }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, computed } from 'vue';
import { message } from 'ant-design-vue'
const text = ref('');
const state = reactive({
loading: false,
uploading: false,
useFile: false,
})
const params = reactive({
chunkSize: 500,
chunkOverlap: 20,
useParser: false,
})
const chunks = ref([]);
const fileList = ref([]);
const wordCount = computed(() => text.value.split(/\s+/).filter(Boolean).length);
const charCount = computed(() => text.value.length)
const estimatedTokenCount = computed(() => {
//
const chars = text.value.split('');
let tokenCount = 0;
for (let char of chars) {
if (/[\u4e00-\u9fff]/.test(char)) {
tokenCount += 1; // token
}
else if (/[a-zA-Z]/.test(char)) {
tokenCount += 0.25; // 4 token
}
else {
tokenCount += 0.5; // token
}
}
return Math.ceil(tokenCount);
})
const chunkText = async () => {
let text_or_file = ''
if (state.useFile) {
if (fileList.value.length === 0) {
message.error("请上传文件")
return
}
console.log(fileList.value)
text_or_file = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)[0]
} else {
if (text.value.length === 0) {
message.error("请输入文本")
return
}
text_or_file = text.value
}
try {
state.loading = true
const response = await fetch('/api/tool/text-chunking', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text_or_file,
params: {
chunk_size: params.chunkSize,
chunk_overlap: params.chunkOverlap,
use_parser: params.useParser
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
chunks.value = data.nodes;
state.loading = false
} catch (error) {
console.error('Error chunking text:', error);
state.loading = false
}
};
</script>
<style lang="less" scoped>
.text-chunking-container {
display: flex;
border-radius: 8px;
font-family: 'Arial', sans-serif;
.sidebar {
position: sticky;
top: 0;
width: 350px; /* 初始宽度 */
background-color: var(--bg-sider);
border-right: 1px solid var(--main-light-3);
padding: 20px;
min-width: 250px;
flex: 1;
.additional-params {
h4 {
font-size: 1.2em;
margin-bottom: 10px;
}
}
}
.result-container {
flex: 3;
padding: 20px;
.input-container {
display: flex;
flex-direction: column;
margin-bottom: 15px;
.actions {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
width: fit-content;
background-color: var(--gray-200);
padding: 4px;
border-radius: 4px;
span {
color: var(--gray-900);
cursor: pointer;
padding: 4px 10px;
border-radius: 4px;
transition: background-color 0.3s;
&.active {
background-color: white;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03),0 1px 6px -1px rgba(0, 0, 0, 0.02),0 2px 4px 0 rgba(0, 0, 0, 0.02)
}
}
}
textarea {
resize: vertical;
height: 300px;
padding: 1rem;
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
background-color: var(--gray-100);
&:focus {
border-color: var(--main-color);
outline: none;
}
}
.infos {
padding: 10px; /* Padding for spacing */
margin-top: 10px; /* Space above the infos */
font-size: 1em; /* Font size */
color: var(--gray-800); /* Text color */
display: flex;
gap: 16px;
}
}
.result-cards {
column-count: 3; /* 设置列数 */
column-gap: 10px; /* 列之间的间距 */
}
.chunk {
background-color: var(--main-5);
border: 1px solid var(--main-light-3);
border-radius: 8px;
padding: 16px;
margin-bottom: 10px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.01);
break-inside: avoid; /* 避免元素被分割到不同列 */
//
word-wrap: break-word;
word-break: break-all;
}
}
}
@media (max-width: 980px) {
#result-cards {
column-count: 1;
}
}
@media (min-width: 981px) and (max-width: 1500px) {
#result-cards {
column-count: 2;
}
}
@media (min-width: 1501px){
#result-cards {
column-count: 3;
}
}
</style>

View File

@ -92,6 +92,11 @@ const mainList = [{
path: '/chat',
icon: MessageOutlined,
activeIcon: MessageFilled,
}, {
name: '智能体',
path: '/agent',
icon: ToolOutlined,
activeIcon: ToolFilled,
}, {
name: '图谱',
path: '/graph',
@ -104,11 +109,6 @@ const mainList = [{
icon: BookOutlined,
activeIcon: BookFilled,
// hidden: !configStore.config.enable_knowledge_base,
}, {
name: '智能体',
path: '/agent',
icon: ToolOutlined,
activeIcon: ToolFilled,
}
]
</script>
@ -168,7 +168,9 @@ const mainList = [{
<div class="fill" style="flex-grow: 1;"></div>
<!-- 用户信息组件 -->
<UserInfoComponent />
<div class="nav-item user-info">
<UserInfoComponent />
</div>
<div class="github nav-item">
<a-tooltip placement="right">
@ -176,19 +178,19 @@ const mainList = [{
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank" class="github-link">
<GithubOutlined class="icon" style="color: #222;"/>
<span v-if="githubStars > 0" class="github-stars">
<span class="star-count">{{ githubStars.toFixed(1) }}</span>
<span class="star-count">{{ (githubStars / 1000).toFixed(1) }}k</span>
</span>
</a>
</a-tooltip>
</div>
<div class="nav-item api-docs">
<!-- <div class="nav-item api-docs">
<a-tooltip placement="right">
<template #title>接口文档 {{ apiDocsUrl }}</template>
<a :href="apiDocsUrl" target="_blank" class="github-link">
<ApiOutlined class="icon" style="color: #222;"/>
</a>
</a-tooltip>
</div>
</div> -->
<RouterLink class="nav-item setting" to="/setting" active-class="active">
<a-tooltip placement="right">
<template #title>设置</template>
@ -365,8 +367,7 @@ div.header, #app-router-view {
width: auto;
font-size: 20px;
color: #333;
margin-bottom: 20px;
margin-top: 10px;
margin-bottom: 8px;
padding: 16px 12px;
&:hover {

View File

@ -41,8 +41,15 @@ const router = createRouter({
{
path: '/agent',
name: 'AgentMain',
component: () => import('../views/AgentView.vue'),
meta: { keepAlive: true, requiresAuth: true, requiresAdmin: true }
component: AppLayout,
children: [
{
path: '',
name: 'AgentComp',
component: () => import('../views/AgentView.vue'),
meta: { keepAlive: true, requiresAuth: true, requiresAdmin: true }
}
]
},
{
path: '/agent/:agent_id',

View File

@ -1,26 +1,23 @@
<template>
<div class="agent-single-view">
<!-- 侧边栏 -->
<div class="sidebar" :class="{ 'collapsed': isSidebarCollapsed }">
<!-- <div class="sidebar" :class="{ 'collapsed': isSidebarCollapsed }">
<div class="sidebar-content">
<div class="user-icon">
<img :src="sidebarLeftIcon" alt="用户信息" @click="toggleUserInfo" />
<UserInfoComponent :show-role="!isSidebarCollapsed" />
</div>
<!-- 这里可以添加更多侧边栏内容 -->
<UserInfoComponent />
</div>
<div class="toggle-button" @click="toggleSidebar">
<img :src="isSidebarCollapsed ? sidebarRightIcon : sidebarLeftIcon" alt="折叠/展开" />
</div>
</div>
<!-- 用户信息组件 -->
<div class="user-info-wrapper">
<UserInfoComponent />
</div>
</div> -->
<!-- 智能体聊天界面 -->
<AgentChatComponent :agent-id="agentId" />
<AgentChatComponent :agent-id="agentId">
<template #header-right>
<UserInfoComponent />
</template>
</AgentChatComponent>
</div>
</template>
@ -56,6 +53,8 @@ const toggleUserInfo = () => {
height: 100vh;
overflow: hidden;
position: relative;
display: flex;
flex-direction: row;
}
.user-info-wrapper {
@ -67,7 +66,7 @@ const toggleUserInfo = () => {
//
.sidebar {
position: absolute;
// position: absolute;
left: 0;
top: 0;
height: 100%;
@ -90,6 +89,7 @@ const toggleUserInfo = () => {
.user-icon {
cursor: pointer;
margin-bottom: 20px;
padding-left: 4px 8px;
img {
width: 32px;

View File

@ -30,7 +30,7 @@
{{ agents[selectedAgentId]?.description }}
</p>
<!-- 添加配置按钮 -->
<!-- 添加配置按钮 TODO 这里的配置修改还无法影响到独立接口的对话-->
<div class="agent-action-buttons">
<a-button
class="action-button"
@ -48,6 +48,15 @@
访问令牌
</a-button>
<a-button
class="action-button"
@click="goToAgentPage"
v-if="selectedAgentId"
>
<template #icon><LinkOutlined /></template>
打开独立页面
</a-button>
<a-button
class="action-button primary-action"
@click="setAsDefaultAgent"
@ -58,14 +67,6 @@
{{ isDefaultAgent ? '当前为默认智能体' : '设为默认智能体' }}
</a-button>
<a-button
class="action-button"
@click="goToAgentPage"
v-if="selectedAgentId"
>
<template #icon><LinkOutlined /></template>
打开独立页面
</a-button>
</div>
<!-- 添加requirements显示部分 -->

View File

@ -397,29 +397,29 @@ const onQuery = () => {
return
}
meta.db_id = database.value.db_id
fetch('/api/data/query-test', {
method: "POST",
headers: {
"Content-Type": "application/json" // Content-Type
},
body: JSON.stringify({
try {
knowledgeBaseApi.queryTest({
query: queryText.value.trim(),
meta: meta
}),
})
.then(response => response.json())
.then(data => {
console.log(data)
queryResult.value = data
filterQueryResults()
})
.catch(error => {
})
.then(data => {
console.log(data)
queryResult.value = data
filterQueryResults()
})
.catch(error => {
console.error(error)
message.error(error.message)
})
.finally(() => {
state.searchLoading = false
})
} catch (error) {
console.error(error)
message.error(error.message)
})
.finally(() => {
state.searchLoading = false
})
}
}
const handleFileUpload = (event) => {
@ -480,27 +480,31 @@ const deleteDatabse = () => {
const openFileDetail = (record) => {
state.lock = true
fetch(`/api/data/document?db_id=${databaseId.value}&file_id=${record.file_id}`, {
method: "GET",
})
.then(response => response.json())
.then(data => {
console.log(data)
if (data.status == "failed") {
message.error(data.message)
return
}
state.lock = false
selectedFile.value = {
...record,
lines: data.lines || []
}
state.drawer = true
})
.catch(error => {
console.error(error)
message.error(error.message)
})
try {
knowledgeBaseApi.getDocumentDetail(databaseId.value, record.file_id)
.then(data => {
console.log(data)
if (data.status == "failed") {
message.error(data.message)
return
}
state.lock = false
selectedFile.value = {
...record,
lines: data.lines || []
}
state.drawer = true
})
.catch(error => {
console.error(error)
message.error(error.message)
})
} catch (error) {
console.error(error)
message.error('获取文件详情失败')
state.lock = false
}
}
const formatRelativeTime = (timestamp) => {
@ -603,17 +607,10 @@ const chunkFiles = () => {
state.loading = true
// file-to-chunk
fetch('/api/data/file-to-chunk', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
files: files,
params: chunkParams.value
}),
knowledgeBaseApi.fileToChunk({
files: files,
params: chunkParams.value
})
.then(response => response.json())
.then(data => {
console.log('文件分块信息:', data)
chunkResults.value = Object.values(data);
@ -645,17 +642,10 @@ const addToDatabase = () => {
});
// add-by-chunks
fetch('/api/data/add-by-chunks', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
db_id: databaseId.value,
file_chunks: fileChunks
}),
knowledgeBaseApi.addByChunks({
db_id: databaseId.value,
file_chunks: fileChunks
})
.then(response => response.json())
.then(data => {
console.log(data)

View File

@ -121,7 +121,7 @@ const loadDatabases = () => {
console.error('加载数据库列表失败:', error);
if (error.message.includes('权限')) {
message.error('需要管理员权限访问知识库')
}
}
})
}
@ -135,10 +135,10 @@ const createDatabase = () => {
}
knowledgeBaseApi.createDatabase({
database_name: newDatabase.name,
description: newDatabase.description,
dimension: newDatabase.dimension ? parseInt(newDatabase.dimension) : null,
})
database_name: newDatabase.name,
description: newDatabase.description,
dimension: newDatabase.dimension ? parseInt(newDatabase.dimension) : null,
})
.then(data => {
console.log(data)
loadDatabases()

View File

@ -91,7 +91,7 @@ const goToChat = async () => {
} else {
// 退chat
router.push("/chat");
}
}
}
} catch (error) {
console.error('跳转到智能体页面失败:', error);

View File

@ -379,6 +379,7 @@ import HeaderComponent from '@/components/HeaderComponent.vue';
import TableConfigComponent from '@/components/TableConfigComponent.vue';
import { notification, Button } from 'ant-design-vue';
import { modelIcons } from '@/utils/modelIcon'
import { systemConfigApi } from '@/apis/admin_api'
const configStore = useConfigStore()
@ -584,15 +585,19 @@ onUnmounted(() => {
const sendRestart = () => {
console.log('Restarting...')
message.loading({ content: '重新加载模型中', key: "restart", duration: 0 });
fetch('/api/restart', {
method: 'POST',
}).then(() => {
console.log('Restarted')
message.success({ content: '重新加载完成!', key: "restart", duration: 2 });
setTimeout(() => {
window.location.reload()
}, 200)
})
systemConfigApi.restartServer()
.then(() => {
console.log('Restarted')
message.success({ content: '重新加载完成!', key: "restart", duration: 2 });
setTimeout(() => {
window.location.reload()
}, 200)
})
.catch(error => {
console.error('重启服务失败:', error)
message.error({ content: `重启失败: ${error.message}`, key: "restart", duration: 2 });
});
}
//

View File

@ -1,111 +0,0 @@
<template>
<div class="tools-container layout-container">
<HeaderComponent
title="工具箱"
description="这里展示了各种可用的工具,重点是为了测试部分功能的页面。(注:不是大模型的工具)"
>
</HeaderComponent>
<div class="tools-grid">
<div v-for="tool in tools" :key="tool.id" class="tool-card" @click="navigateToTool(tool.url)">
<div class="tool-header">
<h3>{{ tool.title }}</h3>
</div>
<div class="tool-info">
<p>{{ tool.description }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { CalculatorOutlined, FileSearchOutlined, TranslationOutlined } from '@ant-design/icons-vue';
import HeaderComponent from '@/components/HeaderComponent.vue';
const router = useRouter();
const tools = ref([]);
const iconMap = ref({
"text-chunking": FileSearchOutlined
})
const state = reactive({
loadingTools: true,
})
const getTools = () => {
state.loadingTools = true
fetch('/api/tool/')
.then(response => response.json())
.then(data => {
tools.value = data;
state.loadingTools = false;
})
.catch(error => {
console.error('Error fetching tools:', error);
state.loadingTools = false;
});
};
const navigateToTool = (toolUrl) => {
router.push(toolUrl);
};
onMounted(() => {
getTools();
});
</script>
<style scoped lang="less">
.tools-container {
padding: 0;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
.tool-card {
display: flex;
flex-direction: column;
background-color: white;
border: 1px solid var(--gray-300);
border-radius: 8px;
padding: 20px;
transition: transform 0.1s ease, box-shadow 0.1s ease;
cursor: pointer;
&:hover {
// transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.tool-header {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 15px;
.tool-icon {
margin-right: 10px;
}
h3 {
margin: 0;
}
}
.tool-info {
flex-grow: 1;
p {
margin: 0;
color: var(--gray-800);
}
}
}
}
</style>