fix: 修复token验证问题,租户管理API存在401未授权问题需要进一步排查
This commit is contained in:
parent
802c57003c
commit
b5fa1b0d63
198
api/app.py
198
api/app.py
@ -1,9 +1,10 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response
|
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response, UploadFile, File
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from models import AccountCreate, AccountResponse, PasswordChange, TenantCreate, TenantResponse
|
from models import AccountCreate, AccountResponse, PasswordChange, TenantCreate, TenantResponse, ModelConfig
|
||||||
|
from model_manager import ModelManager
|
||||||
from account_manager import AccountManager as DifyAccountManager
|
from account_manager import AccountManager as DifyAccountManager
|
||||||
from backend_account_manager import BackendAccountManager
|
from backend_account_manager import BackendAccountManager
|
||||||
|
|
||||||
@ -48,11 +49,12 @@ except Exception as e:
|
|||||||
# 添加CORS中间件
|
# 添加CORS中间件
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001", "http://127.0.0.1:3001"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
expose_headers=["*"]
|
expose_headers=["*"],
|
||||||
|
max_age=600
|
||||||
)
|
)
|
||||||
api_auth = APIAuthManager()
|
api_auth = APIAuthManager()
|
||||||
op_logger = OperationLogger()
|
op_logger = OperationLogger()
|
||||||
@ -111,6 +113,9 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
# 打印接收到的token用于调试
|
||||||
|
logger.info(f"Received token: {token[:10]}...{token[-10:]}")
|
||||||
|
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
username: str = payload.get("sub")
|
username: str = payload.get("sub")
|
||||||
if username is None:
|
if username is None:
|
||||||
@ -122,11 +127,18 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
|
|||||||
if not user:
|
if not user:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
|
logger.info(f"Authenticated user: {username}")
|
||||||
return {
|
return {
|
||||||
"id": str(user.get("id")),
|
"id": str(user.get("id")),
|
||||||
"username": user.get("username"),
|
"username": user.get("username"),
|
||||||
"email": user.get("email", "")
|
"email": user.get("email", "")
|
||||||
}
|
}
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Token已过期",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
@ -176,16 +188,27 @@ async def auth_register(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@app.post("/api/auth/login")
|
@app.post("/api/auth/login")
|
||||||
async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
|
async def auth_login(request: Request):
|
||||||
"""用户登录(auth)"""
|
"""用户登录(auth)"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
username = data.get("username")
|
||||||
|
password = data.get("password")
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail="缺少用户名或密码"
|
||||||
|
)
|
||||||
|
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
user = backend_account_manager.get_user_by_username(form_data.username)
|
user = backend_account_manager.get_user_by_username(username)
|
||||||
if not user or not backend_account_manager.verify_password(form_data.password, user["password"], user["password_salt"]):
|
if not user or not backend_account_manager.verify_password(password, user["password"], user["password_salt"]):
|
||||||
op_logger.log_operation(
|
op_logger.log_operation(
|
||||||
user_id=0,
|
user_id=0,
|
||||||
operation_type="LOGIN_ATTEMPT",
|
operation_type="LOGIN_ATTEMPT",
|
||||||
endpoint="/api/auth/login",
|
endpoint="/api/auth/login",
|
||||||
parameters=f"username={form_data.username}, ip={client_ip}",
|
parameters=f"username={username}, ip={client_ip}",
|
||||||
status="FAILED"
|
status="FAILED"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -193,6 +216,7 @@ async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = De
|
|||||||
detail="用户名或密码错误",
|
detail="用户名或密码错误",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
access_token = create_access_token(
|
access_token = create_access_token(
|
||||||
data={"sub": user["username"]},
|
data={"sub": user["username"]},
|
||||||
@ -206,11 +230,17 @@ async def auth_login(request: Request, form_data: OAuth2PasswordRequestForm = De
|
|||||||
status="SUCCESS"
|
status="SUCCESS"
|
||||||
)
|
)
|
||||||
return {"access_token": access_token, "token_type": "bearer"}
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"登录失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="登录失败"
|
||||||
|
)
|
||||||
|
|
||||||
@app.post("/api/user/login")
|
@app.post("/api/user/login")
|
||||||
async def user_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
|
async def user_login(request: Request):
|
||||||
"""用户登录(user)"""
|
"""用户登录(user)"""
|
||||||
return await auth_login(request, form_data)
|
return await auth_login(request)
|
||||||
|
|
||||||
@app.post("/api/auth/refresh")
|
@app.post("/api/auth/refresh")
|
||||||
async def refresh_token(current_user: dict = Depends(get_current_user)):
|
async def refresh_token(current_user: dict = Depends(get_current_user)):
|
||||||
@ -438,6 +468,154 @@ async def get_tenant(name: str, current_user: dict = Depends(get_current_user)):
|
|||||||
)
|
)
|
||||||
raise HTTPException(status_code=400, detail="查询租户失败")
|
raise HTTPException(status_code=400, detail="查询租户失败")
|
||||||
|
|
||||||
|
# 模型管理路由组
|
||||||
|
@app.post("/api/models/upload")
|
||||||
|
async def upload_model_config(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""上传模型配置文件"""
|
||||||
|
try:
|
||||||
|
# 检查是否为admin用户
|
||||||
|
if current_user["username"] != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要admin用户权限"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 读取上传的文件内容
|
||||||
|
contents = await file.read()
|
||||||
|
config = json.loads(contents)
|
||||||
|
|
||||||
|
# 验证模型配置
|
||||||
|
required_fields = ["model_name", "provider_name", "model_type", "api_key"]
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in config:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
detail=f"模型配置缺少必要字段: {field}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"message": "模型配置上传成功", "config": config}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="无效的JSON格式"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"上传模型配置失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="上传模型配置失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/api/models/assign")
|
||||||
|
async def assign_model_to_tenant(
|
||||||
|
model_config: ModelConfig,
|
||||||
|
tenant_id: Optional[str] = None,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""分配模型给租户"""
|
||||||
|
try:
|
||||||
|
# 检查是否为admin用户
|
||||||
|
if current_user["username"] != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要admin用户权限"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果未指定租户ID,则为所有租户添加模型
|
||||||
|
if not tenant_id:
|
||||||
|
total_added = ModelManager.add_models_for_all_tenants()
|
||||||
|
return {"message": f"模型已成功分配给所有租户,共{total_added}个"}
|
||||||
|
|
||||||
|
# 为指定租户添加模型
|
||||||
|
tenant = TenantManager.get_tenant_by_id(tenant_id)
|
||||||
|
if not tenant:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="租户不存在"
|
||||||
|
)
|
||||||
|
|
||||||
|
ModelManager.add_model_for_tenant(tenant_id, tenant["encrypt_public_key"], model_config.dict())
|
||||||
|
return {"message": "模型已成功分配给租户"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"分配模型失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="分配模型失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/api/models/{tenant_id}")
|
||||||
|
async def get_tenant_models(
|
||||||
|
tenant_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""获取租户的模型列表"""
|
||||||
|
try:
|
||||||
|
models = ModelManager.get_tenant_models(tenant_id)
|
||||||
|
return {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"models": models
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取租户模型失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="获取租户模型失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.delete("/api/models/{model_id}")
|
||||||
|
async def delete_model(
|
||||||
|
model_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""删除特定模型"""
|
||||||
|
try:
|
||||||
|
# 检查是否为admin用户
|
||||||
|
if current_user["username"] != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要admin用户权限"
|
||||||
|
)
|
||||||
|
|
||||||
|
rows_affected = ModelManager.delete_model(model_id)
|
||||||
|
if rows_affected == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="模型不存在"
|
||||||
|
)
|
||||||
|
return {"message": "模型删除成功"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除模型失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="删除模型失败"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.delete("/api/models/all/{model_name}")
|
||||||
|
async def delete_model_for_all_tenants(
|
||||||
|
model_name: str,
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""删除所有租户的特定模型"""
|
||||||
|
try:
|
||||||
|
# 检查管理员权限
|
||||||
|
if current_user["username"] != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要管理员权限"
|
||||||
|
)
|
||||||
|
|
||||||
|
total_deleted = ModelManager.delete_specific_model_for_all_tenants(model_name)
|
||||||
|
return {"message": f"模型已从所有租户中删除,共{total_deleted}个"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"批量删除模型失败: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="批量删除模型失败"
|
||||||
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||||
|
|||||||
@ -14,31 +14,67 @@ class ModelManager:
|
|||||||
"""模型管理类"""
|
"""模型管理类"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_config(config_path):
|
def add_volc_models_for_tenant(tenant_name, config_path=CONFIG_PATHS['volc_model_config']):
|
||||||
"""加载模型配置文件"""
|
"""为指定租户添加火山模型"""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(config_path):
|
# 加载模型配置
|
||||||
raise FileNotFoundError(f"配置文件 {config_path} 不存在!")
|
config = ModelManager.load_config(config_path)
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
models = config.get("models", [])
|
||||||
config = json.load(f)
|
|
||||||
return config
|
# 获取租户信息
|
||||||
|
tenant = TenantManager.get_tenant_by_name(tenant_name)
|
||||||
|
if not tenant:
|
||||||
|
logger.error(f"未找到租户: {tenant_name}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 为租户添加模型
|
||||||
|
total_added = 0
|
||||||
|
for model_config in models:
|
||||||
|
if ModelManager.add_volc_model_for_tenant(tenant['id'], tenant['encrypt_public_key'], model_config):
|
||||||
|
total_added += 1
|
||||||
|
|
||||||
|
logger.info(f"为租户 {tenant_name} 添加火山模型完成,共添加 {total_added} 条记录。")
|
||||||
|
return total_added
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载配置文件失败: {e}")
|
logger.error(f"为租户 {tenant_name} 添加火山模型失败: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_model_exists(tenant_id, model_name):
|
def get_tenant_models(tenant_id: str):
|
||||||
"""检查指定租户是否已存在指定的模型"""
|
"""获取租户的模型列表"""
|
||||||
try:
|
try:
|
||||||
query = """
|
query = """
|
||||||
SELECT COUNT(*) FROM provider_models
|
SELECT
|
||||||
WHERE tenant_id = %s AND model_name = %s;
|
id,
|
||||||
|
provider_name,
|
||||||
|
model_name,
|
||||||
|
model_type,
|
||||||
|
encrypted_config,
|
||||||
|
is_valid,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM provider_models
|
||||||
|
WHERE tenant_id = %s
|
||||||
|
ORDER BY created_at DESC;
|
||||||
"""
|
"""
|
||||||
count = execute_query(query, (tenant_id, model_name), fetch_one=True)[0]
|
with get_db_cursor() as cursor:
|
||||||
return count > 0
|
cursor.execute(query, (tenant_id,))
|
||||||
|
models = cursor.fetchall()
|
||||||
|
|
||||||
|
return [{
|
||||||
|
"id": str(model[0]),
|
||||||
|
"provider_name": model[1],
|
||||||
|
"model_name": model[2],
|
||||||
|
"model_type": model[3],
|
||||||
|
"encrypted_config": json.loads(model[4]),
|
||||||
|
"is_valid": model[5],
|
||||||
|
"created_at": model[6],
|
||||||
|
"updated_at": model[7]
|
||||||
|
} for model in models]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"检查模型是否存在时发生错误: {e}")
|
logger.error(f"获取租户模型失败: {e}")
|
||||||
return False
|
raise
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_model_for_tenant(tenant_id, public_key_pem, model_config):
|
def add_model_for_tenant(tenant_id, public_key_pem, model_config):
|
||||||
|
|||||||
@ -1,7 +1,32 @@
|
|||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
class ModelConfig(BaseModel):
|
||||||
|
"""模型配置上传模型"""
|
||||||
|
model_name: str
|
||||||
|
provider_name: str
|
||||||
|
model_type: str
|
||||||
|
api_key: str
|
||||||
|
endpoint_url: Optional[str] = None
|
||||||
|
display_name: Optional[str] = None
|
||||||
|
context_size: Optional[int] = None
|
||||||
|
max_tokens_to_sample: Optional[int] = None
|
||||||
|
|
||||||
|
class ModelResponse(BaseModel):
|
||||||
|
"""模型响应模型"""
|
||||||
|
id: str
|
||||||
|
model_name: str
|
||||||
|
provider_name: str
|
||||||
|
model_type: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class TenantModelResponse(BaseModel):
|
||||||
|
"""租户模型响应模型"""
|
||||||
|
tenant_id: str
|
||||||
|
tenant_name: str
|
||||||
|
models: List[ModelResponse]
|
||||||
|
|
||||||
class AccountCreate(BaseModel):
|
class AccountCreate(BaseModel):
|
||||||
"""创建账户请求模型"""
|
"""创建账户请求模型"""
|
||||||
username: str
|
username: str
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { request } from '../../axios/service'
|
import { request } from '../../axios/service'
|
||||||
import type { LoginParams, RegisterParams, LoginForm } from '@/api/auth/types'
|
import type { LoginParams, RegisterParams, LoginForm } from '@/api/auth/types'
|
||||||
|
|
||||||
export const login = (formData: FormData | LoginForm) =>
|
export const login = (data: { username: string; password: string }) =>
|
||||||
request<{ access_token: string }>({
|
request<{ access_token: string }>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/auth/login',
|
url: '/api/auth/login',
|
||||||
data: formData,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
50
web/src/api/model/index.ts
Normal file
50
web/src/api/model/index.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { request } from '@/axios/service'
|
||||||
|
import type { ApiResponse, ModelConfig, ModelResponse, TenantModelResponse } from './types.ts'
|
||||||
|
|
||||||
|
export function uploadModelConfig(file: File): Promise<ApiResponse<{config: any}>> {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
return request({
|
||||||
|
url: '/api/models/upload',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assignModelToTenant(
|
||||||
|
modelConfig: ModelConfig,
|
||||||
|
tenantId?: string
|
||||||
|
): Promise<ApiResponse<{message: string}>> {
|
||||||
|
return request({
|
||||||
|
url: '/api/models/assign',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
model_config: modelConfig,
|
||||||
|
tenant_id: tenantId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTenantModels(tenantId: string): Promise<ApiResponse<TenantModelResponse>> {
|
||||||
|
return request({
|
||||||
|
url: `/api/models/${tenantId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteModel(modelId: string): Promise<ApiResponse<{message: string}>> {
|
||||||
|
return request({
|
||||||
|
url: `/api/models/${modelId}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteModelForAllTenants(modelName: string): Promise<ApiResponse<{message: string}>> {
|
||||||
|
return request({
|
||||||
|
url: `/api/models/all/${modelName}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
30
web/src/api/model/types.ts
Normal file
30
web/src/api/model/types.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelConfig {
|
||||||
|
model_name: string
|
||||||
|
provider_name: string
|
||||||
|
model_type: string
|
||||||
|
api_key: string
|
||||||
|
endpoint_url?: string
|
||||||
|
display_name?: string
|
||||||
|
context_size?: number
|
||||||
|
max_tokens_to_sample?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelResponse {
|
||||||
|
id: string
|
||||||
|
model_name: string
|
||||||
|
provider_name: string
|
||||||
|
model_type: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantModelResponse {
|
||||||
|
tenant_id: string
|
||||||
|
tenant_name: string
|
||||||
|
models: ModelResponse[]
|
||||||
|
}
|
||||||
@ -10,8 +10,15 @@ export interface TenantForm {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TenantListParams {
|
||||||
|
search?: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface TenantListResponse {
|
export interface TenantListResponse {
|
||||||
tenants: TenantItem[]
|
tenants: TenantItem[]
|
||||||
|
total?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantDetailResponse {
|
export interface TenantDetailResponse {
|
||||||
|
|||||||
@ -7,10 +7,12 @@ const service = createAxios()
|
|||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const userStore = useUserStore()
|
const token = localStorage.getItem('access_token')
|
||||||
if (userStore.token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
config.headers['Content-Type'] = 'application/json'
|
||||||
|
config.withCredentials = true
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@ -32,7 +32,8 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'model',
|
path: 'model',
|
||||||
component: () => import('../views/Model/index.vue')
|
component: () => import('../views/Model/index.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,20 +72,20 @@ const loginRules = {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loginFormRef = ref()
|
const loginFormRef = ref()
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
await loginFormRef.value.validate()
|
await loginFormRef.value.validate()
|
||||||
const encryptedPassword = await encryptPassword(loginForm.value.password)
|
const encryptedPassword = await encryptPassword(loginForm.value.password)
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('username', loginForm.value.username)
|
|
||||||
formData.append('password', encryptedPassword)
|
|
||||||
|
|
||||||
const res = await login(formData as LoginFormData)
|
const res = await login({
|
||||||
|
username: loginForm.value.username,
|
||||||
|
password: encryptedPassword
|
||||||
|
})
|
||||||
console.log('登录成功:', res)
|
console.log('登录成功:', res)
|
||||||
|
|
||||||
// 确保token存储完成后再跳转
|
// 确保token存储完成后再跳转
|
||||||
const token = res.access_token || res.token
|
const token = res.access_token
|
||||||
setToken(token)
|
setToken(token)
|
||||||
localStorage.setItem('token', token)
|
localStorage.setItem('token', token)
|
||||||
setupTokenRefresh()
|
setupTokenRefresh()
|
||||||
|
|||||||
@ -1,16 +1,249 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="model-container">
|
<div class="model-container">
|
||||||
<h1>模型管理</h1>
|
<h1>模型管理</h1>
|
||||||
<!-- 模型列表和配置表单将在这里实现 -->
|
|
||||||
|
<el-card class="upload-card">
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
accept=".json"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
拖拽模型配置文件到此处或<em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请上传JSON格式的模型配置文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="uploadModel"
|
||||||
|
:disabled="!selectedFile"
|
||||||
|
>
|
||||||
|
上传模型
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="tenant-search-card">
|
||||||
|
<el-form :inline="true">
|
||||||
|
<el-form-item label="租户查询">
|
||||||
|
<el-input
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="输入租户名称或ID"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="searchTenants">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="tenant-list-card" v-if="searchedTenants.length > 0">
|
||||||
|
<el-table :data="searchedTenants" style="width: 100%">
|
||||||
|
<el-table-column prop="name" label="租户名称" />
|
||||||
|
<el-table-column prop="id" label="租户ID" />
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="showTenantModels(scope.row.id)"
|
||||||
|
>
|
||||||
|
查询模型
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="modelDialogVisible" title="租户模型信息">
|
||||||
|
<el-table :data="models" style="width: 100%">
|
||||||
|
<el-table-column prop="model_name" label="模型名称" />
|
||||||
|
<el-table-column prop="provider_name" label="提供商" />
|
||||||
|
<el-table-column prop="model_type" label="模型类型" />
|
||||||
|
<el-table-column prop="created_at" label="创建时间" />
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="assignDialogVisible" title="分配模型">
|
||||||
|
<el-form :model="assignForm">
|
||||||
|
<el-form-item label="选择租户">
|
||||||
|
<el-select v-model="assignForm.tenantId" placeholder="请选择租户">
|
||||||
|
<el-option
|
||||||
|
v-for="tenant in tenants"
|
||||||
|
:key="tenant.id"
|
||||||
|
:label="tenant.name"
|
||||||
|
:value="tenant.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="assignDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmAssign">
|
||||||
|
确认分配
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// 模型管理逻辑将在这里实现
|
import { ref } from 'vue'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
uploadModelConfig,
|
||||||
|
assignModelToTenant,
|
||||||
|
getTenantModels,
|
||||||
|
deleteModel as deleteModelApi
|
||||||
|
} from '@/api/model'
|
||||||
|
import { fetchTenants } from '@/api/tenant'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { ModelResponse } from '@/api/model/types'
|
||||||
|
|
||||||
|
interface Tenant {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const models = ref<ModelResponse[]>([])
|
||||||
|
const tenants = ref<Tenant[]>([])
|
||||||
|
const searchedTenants = ref<Tenant[]>([])
|
||||||
|
const searchQuery = ref('')
|
||||||
|
const modelDialogVisible = ref(false)
|
||||||
|
const assignDialogVisible = ref(false)
|
||||||
|
const currentModel = ref<ModelResponse | null>(null)
|
||||||
|
const assignForm = ref({
|
||||||
|
tenantId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchTenants = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetchTenants({ search: searchQuery.value })
|
||||||
|
searchedTenants.value = res.tenants
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('查询租户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTenantModels = async (tenantId: string) => {
|
||||||
|
try {
|
||||||
|
const res = await getTenantModels(tenantId)
|
||||||
|
models.value = res.data.models
|
||||||
|
modelDialogVisible.value = true
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载模型列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTenantChange = async (tenantId: string) => {
|
||||||
|
try {
|
||||||
|
const res = await getTenantModels(tenantId)
|
||||||
|
models.value = res.data.models
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载模型列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (file: File) => {
|
||||||
|
selectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadModel = async () => {
|
||||||
|
if (!selectedFile.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadModelConfig(selectedFile.value)
|
||||||
|
ElMessage.success(res.message)
|
||||||
|
loadModels()
|
||||||
|
selectedFile.value = null
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadModels = async () => {
|
||||||
|
try {
|
||||||
|
// 从store获取当前租户ID
|
||||||
|
const currentTenantId = localStorage.getItem('currentTenantId')
|
||||||
|
if (!currentTenantId) {
|
||||||
|
ElMessage.warning('请先选择租户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await getTenantModels(currentTenantId)
|
||||||
|
models.value = res.data.models
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载模型列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTenants = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetchTenants({})
|
||||||
|
tenants.value = res.tenants
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('加载租户列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignModel = (model: ModelResponse) => {
|
||||||
|
currentModel.value = model
|
||||||
|
assignDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmAssign = async () => {
|
||||||
|
if (!currentModel.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assignModelToTenant({
|
||||||
|
model_name: currentModel.value.model_name,
|
||||||
|
provider_name: currentModel.value.provider_name,
|
||||||
|
model_type: currentModel.value.model_type,
|
||||||
|
api_key: '' // 实际应用中应从安全存储获取
|
||||||
|
}, assignForm.value.tenantId)
|
||||||
|
ElMessage.success('分配成功')
|
||||||
|
assignDialogVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('分配失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteModel = async (modelId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteModelApi(modelId)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadModels()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
loadModels()
|
||||||
|
loadTenants()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.model-container {
|
.model-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user