继续优化接口,移除无关模块:(1)移除原本的 Token 管理模块;(2)所有 API 请求使用 /api 前缀;(3)移除重复的中间件验证;(4)前后端都移除工具模块;(5)移除原本的 safe_config 模块;(6)其他小细节优化

This commit is contained in:
Wenjie Zhang 2025-05-05 16:31:35 +08:00
parent 0fd83d179f
commit 7b6a1b4458
20 changed files with 72 additions and 721 deletions

View File

@ -1,12 +1,10 @@
import os import os
import sqlite3
import pathlib import pathlib
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from server.models.token_model import Base, AgentToken from server.models import Base
from server.models.user_model import User, OperationLog from server.models.user_model import User
class DBManager: class DBManager:
"""数据库管理器""" """数据库管理器"""

View File

@ -6,13 +6,12 @@ from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from server.routers import router from server.routers import router
from server.utils.auth_middleware import get_current_user, is_public_path, is_admin_path from server.utils.auth_middleware import is_public_path
from server.models.user_model import User
from src.utils.logging_config import logger from src.utils.logging_config import logger
app = FastAPI() app = FastAPI()
app.include_router(router) app.include_router(router, prefix="/api")
# CORS 设置 # CORS 设置
app.add_middleware( app.add_middleware(
@ -33,85 +32,24 @@ class AuthMiddleware(BaseHTTPMiddleware):
if is_public_path(path): if is_public_path(path):
return await call_next(request) return await call_next(request)
# 注意:前端代理已经去掉了 /api 前缀,例如 /api/chat 变成了 /chat if not path.startswith("/api"):
# 判断是否需要验证的API请求包括聊天、数据、工具等
is_api_path = (
path.startswith("/chat") or
path.startswith("/data") or
path.startswith("/admin") or
path.startswith("/auth") and not is_public_path(path)
)
if not is_api_path:
# 非API路径可能是前端路由或静态资源 # 非API路径可能是前端路由或静态资源
return await call_next(request) return await call_next(request)
# 提取Authorization头 # # 提取Authorization头
auth_header = request.headers.get("Authorization") # auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "): # if not auth_header or not auth_header.startswith("Bearer "):
return JSONResponse( # return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, # status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "请先登录"}, # content={"detail": f"请先登录。Path: {path}"},
headers={"WWW-Authenticate": "Bearer"} # headers={"WWW-Authenticate": "Bearer"}
) # )
# 获取token # # 获取token
token = auth_header.split("Bearer ")[1] # token = auth_header.split("Bearer ")[1]
# 添加token到请求状态后续路由可以直接使用 # # 添加token到请求状态后续路由可以直接使用
request.state.token = token # request.state.token = token
# 检查是否需要管理员权限
if is_admin_path(path):
# 尝试获取数据库会话
try:
from server.db_manager import db_manager
from server.utils.auth_utils import AuthUtils
db = db_manager.get_session()
try:
# 验证token并获取用户信息
payload = AuthUtils.verify_access_token(token)
user_id = payload.get("sub")
if not user_id:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "无效的用户标识"},
headers={"WWW-Authenticate": "Bearer"}
)
# 查询用户信息
from server.models.user_model import User
user = db.query(User).filter(User.id == user_id).first()
if not user:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "用户不存在"},
headers={"WWW-Authenticate": "Bearer"}
)
# 检查管理员权限
if user.role not in ["admin", "superadmin"]:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": "需要管理员权限"}
)
# 将用户信息添加到请求状态
request.state.user = user
finally:
db.close()
except Exception as e:
logger.error(f"验证管理员权限出错: {e}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "验证用户权限出错"},
headers={"WWW-Authenticate": "Bearer"}
)
# 继续处理请求 # 继续处理请求
return await call_next(request) return await call_next(request)

View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

View File

@ -1,24 +0,0 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func
Base = declarative_base()
class AgentToken(Base):
"""智能体访问令牌模型"""
__tablename__ = 'agent_tokens'
id = Column(Integer, primary_key=True, autoincrement=True)
agent_id = Column(String, nullable=False, index=True) # 智能体ID
name = Column(String, nullable=False) # 令牌名称
token = Column(String, nullable=False, unique=True) # 令牌值
created_at = Column(DateTime, default=func.now()) # 创建时间
def to_dict(self):
return {
"id": self.id,
"agent_id": self.agent_id,
"name": self.name,
"token": self.token,
"created_at": self.created_at.isoformat() if self.created_at else None
}

View File

@ -1,9 +1,8 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from server.models.token_model import Base from server.models import Base
class User(Base): class User(Base):
"""用户模型""" """用户模型"""

View File

@ -2,12 +2,10 @@ from fastapi import APIRouter
from server.routers.chat_router import chat from server.routers.chat_router import chat
from server.routers.data_router import data from server.routers.data_router import data
from server.routers.base_router import base from server.routers.base_router import base
from server.routers.admin_router import admin
from server.routers.auth_router import auth from server.routers.auth_router import auth
router = APIRouter() router = APIRouter()
router.include_router(base) router.include_router(base)
router.include_router(chat) router.include_router(chat)
router.include_router(data) router.include_router(data)
router.include_router(admin)
router.include_router(auth) router.include_router(auth)

View File

@ -1,126 +0,0 @@
import secrets
import string
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy.orm import Session
from server.db_manager import db_manager
from server.models.token_model import AgentToken
from server.models.user_model import User, OperationLog
from server.utils.auth_middleware import get_db, get_current_user, get_admin_user, oauth2_scheme
from server.routers.auth_router import log_operation
admin = APIRouter(prefix="/admin", tags=["admin"])
# 请求和响应模型
class TokenCreate(BaseModel):
agent_id: str
name: str
class TokenVerify(BaseModel):
agent_id: str
token: str
class TokenResponse(BaseModel):
id: int
agent_id: str
name: str
token: str
created_at: str
# 生成随机token
def generate_token(length=32):
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
@admin.get("/tokens", response_model=List[TokenResponse])
async def get_agent_tokens(
agent_id: Optional[str] = Query(None),
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""获取智能体的token列表需要管理员权限"""
query = db.query(AgentToken)
if agent_id:
query = query.filter(AgentToken.agent_id == agent_id)
tokens = query.all()
return [token.to_dict() for token in tokens]
@admin.post("/tokens", response_model=TokenResponse)
async def create_token(
token_data: TokenCreate,
request: Request,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""创建新的token需要管理员权限"""
# 生成随机token
token_value = generate_token()
# 创建token记录
new_token = AgentToken(
agent_id=token_data.agent_id,
name=token_data.name,
token=token_value
)
# 保存到数据库
db.add(new_token)
db.commit()
db.refresh(new_token)
# 记录操作
log_operation(
db,
current_user.id,
"创建令牌",
f"为智能体 {token_data.agent_id} 创建访问令牌: {token_data.name}",
request
)
return new_token.to_dict()
@admin.delete("/tokens/{token_id}", response_model=dict)
async def delete_token(
token_id: int,
request: Request,
current_user: User = Depends(get_admin_user),
db: Session = Depends(get_db)
):
"""删除token需要管理员权限"""
token = db.query(AgentToken).filter(AgentToken.id == token_id).first()
if not token:
raise HTTPException(status_code=404, detail="Token not found")
# 记录操作信息
log_operation(
db,
current_user.id,
"删除令牌",
f"删除令牌ID: {token_id}, 智能体: {token.agent_id}, 名称: {token.name}",
request
)
db.delete(token)
db.commit()
return {"success": True, "message": "Token deleted"}
@admin.post("/verify_token")
async def verify_agent_token(
token_data: TokenVerify,
token: Optional[str] = Depends(oauth2_scheme),
db: Session = Depends(get_db)
):
"""验证智能体访问令牌(所有用户都可访问)"""
# 查找令牌
agent_token = db.query(AgentToken).filter(
AgentToken.agent_id == token_data.agent_id,
AgentToken.token == token_data.token
).first()
if not agent_token:
raise HTTPException(status_code=401, detail="Invalid token")
return {"success": True, "message": "Token verified"}

View File

@ -1,4 +1,4 @@
from fastapi import Request, Body from fastapi import Request, Body, Depends
from fastapi import APIRouter from fastapi import APIRouter
from fastapi import Request, Body from fastapi import Request, Body
@ -6,6 +6,8 @@ base = APIRouter()
from src import config, retriever, knowledge_base, graph_base from src import config, retriever, knowledge_base, graph_base
from src.utils import logger from src.utils import logger
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
from server.models.user_model import User
@base.get("/") @base.get("/")
@ -13,27 +15,28 @@ async def route_index():
return {"message": "You Got It!"} return {"message": "You Got It!"}
@base.get("/config") @base.get("/config")
def get_config(): def get_config(current_user: User = Depends(get_admin_user)):
return config.get_safe_config() return config.dump_config()
@base.post("/config") @base.post("/config")
async def update_config(key = Body(...), value = Body(...)): async def update_config(
if key == "custom_models": key = Body(...),
value = config.compare_custom_models(value) value = Body(...),
current_user: User = Depends(get_admin_user)
) -> dict:
config[key] = value config[key] = value
config.save() config.save()
return config.get_safe_config() return config.dump_config()
@base.post("/restart") @base.post("/restart")
async def restart(): async def restart(current_user: User = Depends(get_superadmin_user)):
knowledge_base.restart() knowledge_base.restart()
graph_base.start() graph_base.start()
retriever.restart() retriever.restart()
return {"message": "Restarted!"} return {"message": "Restarted!"}
@base.get("/log") @base.get("/log")
def get_log(): def get_log(current_user: User = Depends(get_admin_user)):
from src.utils.logging_config import LOG_FILE from src.utils.logging_config import LOG_FILE
from collections import deque from collections import deque

View File

@ -1,70 +0,0 @@
import os
from fastapi import APIRouter, Body
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from src.agents import agent_manager
tool = APIRouter(prefix="/tool")
class Tool(BaseModel):
name: str
title: str
description: str
url: str
method: Optional[str] = "POST"
params: Optional[Dict[str, Any]] = None
metadata: Optional[Dict[str, Any]] = None
@tool.get("/", response_model=List[Tool])
async def route_index():
tools = [
Tool(
name="text-chunking",
title="文本分块",
description="将文本分块以更好地理解。可以输入文本或者上传文件。",
url="/tools/text-chunking",
method="POST",
),
Tool(
name="pdf2txt",
title="PDF转文本",
description="将PDF文件转换为文本文件。",
url="/tools/pdf2txt",
method="POST",
),
Tool(
name="agent",
title="智能体Dev",
description="智能体演练平台,现在还处于开发预览状态,欢迎提 Issue但先不要用于正式场景。",
url="/tools/agent",
)
]
for agent in agent_manager.agents.values():
tools.append(
Tool(
name=agent.name,
title=agent.name,
description=agent.description,
url=f"/agent/{agent.name}",
method="POST",
metadata=agent.config_schema.to_dict(),
)
)
return tools
@tool.post("/text-chunking")
async def text_chunking(text: str = Body(...), params: Dict[str, Any] = Body(...)):
from src.core.indexing import chunk
nodes = chunk(text, params=params)
return {"nodes": [node.to_dict() for node in nodes]}
@tool.post("/pdf2txt")
async def handle_pdf2txt(file: str = Body(...)):
from src.plugins import ocr
text = ocr.process_pdf(file)
return {"text": text}

View File

@ -1,5 +1,5 @@
from typing import Optional, List from typing import Optional, List, Callable
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status, APIRouter
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from jose import JWTError, jwt from jose import JWTError, jwt
@ -14,21 +14,11 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=Fals
# 公开路径列表,无需登录即可访问 # 公开路径列表,无需登录即可访问
PUBLIC_PATHS = [ PUBLIC_PATHS = [
r"^/auth/token$", # 登录 r"^/api/auth/token$", # 登录
r"^/auth/check-first-run$", # 检查是否首次运行 r"^/api/auth/check-first-run$", # 检查是否首次运行
r"^/auth/initialize$", # 初始化系统 r"^/api/auth/initialize$", # 初始化系统
r"^/docs$", r"^/redoc$", r"^/openapi.json$", # API文档 r"^/api$", # Health Check
r"^/static/.*$", # 静态资源 r"^/api/login$", # 登录页面
r"^/assets/.*$", # 前端资源文件
r"^/$", # 根路径(登录页)
r"^/login$", # 登录页面
r"^/home$", # 首页
r"^/home/.*$", # 首页下的所有路径
r"^/favicon\.ico$", # 网站图标
r"^/_nuxt/.*$", # Nuxt.js生成的资源文件
r"^/js/.*$", # JavaScript文件
r"^/css/.*$", # CSS文件
r"^/img/.*$" # 图片文件
] ]
# 获取数据库会话 # 获取数据库会话
@ -110,35 +100,3 @@ def is_public_path(path: str) -> bool:
if re.match(pattern, path): if re.match(pattern, path):
return True return True
return False return False
# 路径是否需要管理员权限
ADMIN_PATHS = [
r"^/admin/.*$", # 管理员接口
r"^/data/.*$", # 数据操作接口,所有数据操作都需要管理员权限
r"^/chat/set_default_agent$", # 设置默认智能体
r"^/chat/models/update$", # 更新模型列表
r"^/auth/users.*$", # 用户管理相关API
r"^/config$" # 系统配置API
]
# 路径是否需要超级管理员权限
SUPERADMIN_PATHS = [
r"^/restart$", # 系统重启
r"^/auth/users/\d+/role$" # 修改用户角色(仅超级管理员可操作)
]
# 检查路径是否需要管理员权限
def is_admin_path(path: str) -> bool:
path = path.rstrip('/') # 去除尾部斜杠以便于匹配
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

@ -214,18 +214,8 @@ class Config(SimpleConfig):
logger.info(f"Config file {self.filename} saved") logger.info(f"Config file {self.filename} saved")
def get_safe_config(self): def dump_config(self):
""" return json.loads(str(self))
获取安全的配置即过滤掉 api_key
"""
config = json.loads(str(self))
# 过滤掉 api_key
for model in config.get("custom_models", []):
model["api_key"] = DEFAULT_MOCK_API if model.get("api_key") else ""
return config
def compare_custom_models(self, value): def compare_custom_models(self, value):
""" """

View File

@ -70,40 +70,6 @@ export const userManagementApi = {
}, },
} }
// 令牌管理API
export const tokenApi = {
/**
* 获取令牌列表
* @param {string} agentId - 智能体ID
* @returns {Promise} - 令牌列表
*/
getTokens: async (agentId) => {
checkAdminPermission()
return apiGet(`/api/admin/tokens?agent_id=${agentId}`, {}, true)
},
/**
* 创建新令牌
* @param {string} agentId - 智能体ID
* @param {string} name - 令牌名称
* @returns {Promise} - 创建结果
*/
createToken: async (agentId, name) => {
checkAdminPermission()
return apiPost('/api/admin/tokens', { agent_id: agentId, name }, {}, true)
},
/**
* 删除令牌
* @param {string} tokenId - 令牌ID
* @returns {Promise} - 删除结果
*/
deleteToken: async (tokenId) => {
checkAdminPermission()
return apiDelete(`/api/admin/tokens/${tokenId}`, {}, true)
},
}
// 知识库管理API // 知识库管理API
export const knowledgeBaseApi = { export const knowledgeBaseApi = {
/** /**

View File

@ -42,7 +42,7 @@ export async function apiRequest(url, options = {}, requiresAuth = false) {
// 尝试解析错误信息 // 尝试解析错误信息
let errorMessage = `请求失败: ${response.status}` let errorMessage = `请求失败: ${response.status}`
let errorData = null let errorData = null
try { try {
errorData = await response.json() errorData = await response.json()
errorMessage = errorData.detail || errorData.message || errorMessage errorMessage = errorData.detail || errorData.message || errorMessage
@ -56,15 +56,15 @@ export async function apiRequest(url, options = {}, requiresAuth = false) {
const userStore = useUserStore() const userStore = useUserStore()
if (userStore.isLoggedIn) { if (userStore.isLoggedIn) {
// 如果用户认为自己已登录但收到401则可能是令牌过期 // 如果用户认为自己已登录但收到401则可能是令牌过期
const isTokenExpired = errorData && const isTokenExpired = errorData &&
(errorData.detail?.includes('令牌已过期') || (errorData.detail?.includes('令牌已过期') ||
errorData.detail?.includes('token expired') || errorData.detail?.includes('token expired') ||
errorMessage?.includes('令牌已过期') || errorMessage?.includes('令牌已过期') ||
errorMessage?.includes('token expired')) errorMessage?.includes('token expired'))
message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录') message.error(isTokenExpired ? '登录已过期,请重新登录' : '认证失败,请重新登录')
userStore.logout() userStore.logout()
// 使用setTimeout确保消息显示后再跳转 // 使用setTimeout确保消息显示后再跳转
setTimeout(() => { setTimeout(() => {
window.location.href = '/login' window.location.href = '/login'

View File

@ -1,232 +0,0 @@
<template>
<div class="token-manager">
<div class="token-tools">
<a-button type="primary" size="small" @click="showAddTokenModal">
<PlusOutlined /> 创建 Token
</a-button>
</div>
<!-- 令牌列表 -->
<div class="token-list" v-if="tokens.length > 0">
<a-spin :spinning="loading">
<a-list size="small">
<a-list-item v-for="token in tokens" :key="token.id">
<div class="token-item">
<div class="token-info">
<div class="token-name">{{ token.name }}</div>
<div class="token-value">
<code>{{ token.token }}</code>
<a-button type="link" size="small" @click="copyToken(token.token)">
<CopyOutlined />
</a-button>
</div>
<div class="token-time">创建时间: {{ formatDate(token.created_at) }}</div>
</div>
<div class="token-actions">
<a-popconfirm
title="确定要删除这个令牌吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteToken(token.id)"
>
<a-button type="text" danger size="small">
<DeleteOutlined />
</a-button>
</a-popconfirm>
</div>
</div>
</a-list-item>
</a-list>
</a-spin>
</div>
<a-empty v-else description="暂无访问令牌" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<!-- 添加令牌弹窗 -->
<a-modal
v-model:open="addTokenModalVisible"
title="添加访问令牌"
ok-text="创建"
cancel-text="取消"
@ok="createToken"
>
<a-form :model="newToken" layout="vertical">
<a-form-item label="令牌名称" name="name">
<a-input v-model:value="newToken.name" placeholder="请输入令牌名称" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { message, Empty } from 'ant-design-vue';
import { PlusOutlined, DeleteOutlined, CopyOutlined } from '@ant-design/icons-vue';
import { tokenApi } from '@/apis/admin_api';
const props = defineProps({
agentId: {
type: String,
required: true
}
});
//
const tokens = ref([]);
const loading = ref(false);
const addTokenModalVisible = ref(false);
const newToken = ref({
name: ''
});
//
const fetchTokens = async () => {
loading.value = true;
try {
const data = await tokenApi.getTokens(props.agentId);
tokens.value = data;
} catch (error) {
console.error('获取令牌列表出错:', error);
message.error(error.message || '获取令牌列表出错');
} finally {
loading.value = false;
}
};
//
const createToken = async () => {
if (!newToken.value.name.trim()) {
message.warning('请输入令牌名称');
return;
}
try {
const data = await tokenApi.createToken(props.agentId, newToken.value.name);
tokens.value.push(data);
message.success('令牌创建成功');
addTokenModalVisible.value = false;
newToken.value.name = '';
} catch (error) {
console.error('创建令牌出错:', error);
message.error(error.message || '创建令牌出错');
}
};
//
const deleteToken = async (tokenId) => {
try {
await tokenApi.deleteToken(tokenId);
tokens.value = tokens.value.filter(token => token.id !== tokenId);
message.success('令牌已删除');
} catch (error) {
console.error('删除令牌出错:', error);
message.error(error.message || '删除令牌出错');
}
};
//
const copyToken = (token) => {
navigator.clipboard.writeText(token).then(() => {
message.success('令牌已复制到剪贴板');
});
};
//
const showAddTokenModal = () => {
newToken.value.name = '';
addTokenModalVisible.value = true;
};
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString();
};
// agentId
watch(() => props.agentId, (newAgentId) => {
if (newAgentId) {
fetchTokens();
} else {
tokens.value = [];
}
}, { immediate: true });
//
onMounted(() => {
if (props.agentId) {
fetchTokens();
}
});
</script>
<style lang="less" scoped>
.token-manager {
margin-top: 1rem;
// padding: 0 0.5rem;
}
.manager-title {
font-size: 1rem;
margin-bottom: 1rem;
}
.token-tools {
margin-bottom: 1rem;
display: flex;
justify-content: flex-end;
}
.token-list {
max-height: calc(100vh - 400px);
overflow-y: auto;
li.ant-list-item {
background-color: var(--gray-100);
border-radius: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
}
.token-item {
display: flex;
justify-content: space-between;
width: 100%;
}
.token-info {
flex: 1;
}
.token-name {
font-weight: 500;
margin-bottom: 0.25rem;
}
.token-value {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
background-color: var(--main-light-4);
padding: 0.25rem 0.5rem;
border-radius: 4px;
margin-bottom: 0.25rem;
overflow-x: auto;
user-select: all;
code {
flex: 1;
}
}
.token-time {
font-size: 0.75rem;
color: var(--gray-500);
}
.token-actions {
display: flex;
align-items: center;
}
</style>

View File

@ -4,7 +4,7 @@
<div class="user-info-dropdown" :data-align="showRole ? 'left' : 'center'"> <div class="user-info-dropdown" :data-align="showRole ? 'left' : 'center'">
<div class="user-avatar"> <div class="user-avatar">
<UserOutlined /> <UserOutlined />
<div class="user-role-badge" :class="userRoleClass"></div> <!-- <div class="user-role-badge" :class="userRoleClass"></div> -->
</div> </div>
<div v-if="showRole">{{ userStore.username }}</div> <div v-if="showRole">{{ userStore.username }}</div>
</div> </div>
@ -116,8 +116,6 @@ const goToLogin = () => {
width: 30px; width: 30px;
height: 30px; height: 30px;
border-radius: 50%; border-radius: 50%;
background-color: var(--main-color);
color: white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -30,7 +30,6 @@ import { useConfigStore } from '@/stores/config'
import { useDatabaseStore } from '@/stores/database' import { useDatabaseStore } from '@/stores/database'
import DebugComponent from '@/components/DebugComponent.vue' import DebugComponent from '@/components/DebugComponent.vue'
import UserInfoComponent from '@/components/UserInfoComponent.vue' import UserInfoComponent from '@/components/UserInfoComponent.vue'
import { configApi } from '@/apis/public_api'
const configStore = useConfigStore() const configStore = useConfigStore()
const databaseStore = useDatabaseStore() const databaseStore = useDatabaseStore()
@ -80,12 +79,6 @@ onMounted(() => {
const route = useRoute() const route = useRoute()
console.log(route) console.log(route)
const apiDocsUrl = computed(() => {
// return `${import.meta.env.VITE_API_URL || `http://${window.location.hostname}:${window.location.port}`}/docs`
return `http://localhost:5050/docs`
})
// //
const mainList = [{ const mainList = [{
name: '对话', name: '对话',
@ -167,10 +160,6 @@ const mainList = [{
</div> </div>
<div class="fill" style="flex-grow: 1;"></div> <div class="fill" style="flex-grow: 1;"></div>
<!-- 用户信息组件 -->
<div class="nav-item user-info">
<UserInfoComponent />
</div>
<div class="github nav-item"> <div class="github nav-item">
<a-tooltip placement="right"> <a-tooltip placement="right">
@ -191,6 +180,15 @@ const mainList = [{
</a> </a>
</a-tooltip> </a-tooltip>
</div> --> </div> -->
<!-- 用户信息组件 -->
<div class="nav-item user-info">
<a-tooltip placement="right">
<template #title>用户信息</template>
<UserInfoComponent />
</a-tooltip>
</div>
<RouterLink class="nav-item setting" to="/setting" active-class="active"> <RouterLink class="nav-item setting" to="/setting" active-class="active">
<a-tooltip placement="right"> <a-tooltip placement="right">
<template #title>设置</template> <template #title>设置</template>

View File

@ -1,5 +1,6 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { systemConfigApi } from '@/apis/admin_api'
export const useCounterStore = defineStore('counter', () => { export const useCounterStore = defineStore('counter', () => {
const count = ref(0) const count = ref(0)
@ -20,27 +21,19 @@ export const useConfigStore = defineStore('config', () => {
function setConfigValue(key, value) { function setConfigValue(key, value) {
config.value[key] = value config.value[key] = value
fetch('/api/config', { systemConfigApi.updateSystemConfig({ key, value })
method: 'POST', .then(data => {
body: JSON.stringify({ key, value }), console.debug('Success:', data)
headers: { setConfig(data)
'Content-Type': 'application/json' })
}
})
.then(response => response.json())
.then(data => {
console.debug('Success:', data)
setConfig(data)
})
} }
function refreshConfig() { function refreshConfig() {
fetch('/api/config') systemConfigApi.getSystemConfig()
.then(response => response.json()) .then(data => {
.then(data => { console.log("config", data)
console.log("config", data) setConfig(data)
setConfig(data) })
})
} }
return { config, setConfig, setConfigValue, refreshConfig } return { config, setConfig, setConfigValue, refreshConfig }

View File

@ -40,14 +40,6 @@
智能体配置 智能体配置
</a-button> </a-button>
<a-button
class="action-button"
@click="openTokenModal"
>
<template #icon><KeyOutlined /></template>
访问令牌
</a-button>
<a-button <a-button
class="action-button" class="action-button"
@click="goToAgentPage" @click="goToAgentPage"
@ -204,16 +196,6 @@
</div> </div>
</a-modal> </a-modal>
<!-- 令牌管理弹窗 -->
<a-modal
v-model:open="state.tokenModalVisible"
title="访问令牌管理"
width="650px"
:footer="null"
@cancel="closeTokenModal"
>
<TokenManagerComponent v-if="selectedAgentId" :agent-id="selectedAgentId" />
</a-modal>
</div> </div>
</template> </template>
@ -233,8 +215,6 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue'; import AgentChatComponent from '@/components/AgentChatComponent.vue';
import TokenManagerComponent from '@/components/TokenManagerComponent.vue';
import UserInfoComponent from '@/components/UserInfoComponent.vue';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { chatApi } from '@/apis/auth_api'; import { chatApi } from '@/apis/auth_api';
import { systemConfigApi } from '@/apis/admin_api'; import { systemConfigApi } from '@/apis/admin_api';
@ -253,7 +233,6 @@ const state = reactive({
isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'), isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'),
isConfigSidebarOpen: false, isConfigSidebarOpen: false,
configModalVisible: false, configModalVisible: false,
tokenModalVisible: false,
isEmptyConfig: computed(() => isEmptyConfig: computed(() =>
!selectedAgentId.value || !selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0 Object.keys(configurableItems.value).length === 0
@ -521,16 +500,6 @@ const openConfigModal = () => {
const closeConfigModal = () => { const closeConfigModal = () => {
state.configModalVisible = false; state.configModalVisible = false;
}; };
//
const openTokenModal = () => {
state.tokenModalVisible = true;
};
//
const closeTokenModal = () => {
state.tokenModalVisible = false;
};
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -580,12 +549,6 @@ const closeTokenModal = () => {
min-width: calc(var(--config-sidebar-width) - 16px); min-width: calc(var(--config-sidebar-width) - 16px);
overflow-y: auto; overflow-y: auto;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
.token-section {
margin-top: 1.5rem;
border-top: 1px solid var(--main-light-3);
padding-top: 1rem;
}
} }
.no-agent-selected { .no-agent-selected {

View File

@ -286,7 +286,6 @@ import { onMounted, reactive, ref, watch, toRaw, onUnmounted, computed } from 'v
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useConfigStore } from '@/stores/config' import { useConfigStore } from '@/stores/config'
import { useUserStore } from '@/stores/user'
import { knowledgeBaseApi } from '@/apis/admin_api' import { knowledgeBaseApi } from '@/apis/admin_api'
import HeaderComponent from '@/components/HeaderComponent.vue'; import HeaderComponent from '@/components/HeaderComponent.vue';
import { import {
@ -300,7 +299,6 @@ import {
CloudUploadOutlined, CloudUploadOutlined,
SearchOutlined, SearchOutlined,
LoadingOutlined, LoadingOutlined,
CaretUpOutlined
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
@ -397,7 +395,7 @@ const onQuery = () => {
return return
} }
meta.db_id = database.value.db_id meta.db_id = database.value.db_id
try { try {
knowledgeBaseApi.queryTest({ knowledgeBaseApi.queryTest({
query: queryText.value.trim(), query: queryText.value.trim(),
@ -480,7 +478,7 @@ const deleteDatabse = () => {
const openFileDetail = (record) => { const openFileDetail = (record) => {
state.lock = true state.lock = true
try { try {
knowledgeBaseApi.getDocumentDetail(databaseId.value, record.file_id) knowledgeBaseApi.getDocumentDetail(databaseId.value, record.file_id)
.then(data => { .then(data => {

View File

@ -16,7 +16,7 @@ export default defineConfig(({ mode }) => {
'^/api': { '^/api': {
target: env.VITE_API_URL || 'http://127.0.0.1:5050', target: env.VITE_API_URL || 'http://127.0.0.1:5050',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '/api')
} }
}, },
watch: { watch: {