统一前端对于接口的使用
This commit is contained in:
parent
9fdc6a6b77
commit
c6474f6585
@ -12,6 +12,12 @@
|
||||
|
||||
## 📝 项目概述
|
||||
|
||||
DEV 更新待办:
|
||||
|
||||
- 智能体的消息加载有问题
|
||||
- 智能体的管理员的配置无法更新到用户层面
|
||||
|
||||
|
||||
语析是一个强大的问答平台,结合了大模型 RAG 知识库与知识图谱技术,基于 Llamaindex + VueJS + FastAPI + Neo4j 构建。
|
||||
|
||||
**核心特点:**
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -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可以继续添加到这里
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
* - 用户管理、知识库管理、系统配置等
|
||||
*
|
||||
*
|
||||
* 注意:本模块已处理权限验证和请求头,使用时无需再手动添加认证头
|
||||
*/
|
||||
*/
|
||||
@ -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'
|
||||
@ -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 />
|
||||
<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) {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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显示部分 -->
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -91,7 +91,7 @@ const goToChat = async () => {
|
||||
} else {
|
||||
// 没有可用智能体,回退到chat页面
|
||||
router.push("/chat");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('跳转到智能体页面失败:', error);
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
// 获取模型提供商的模型列表
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user