diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py index 2767f0a1..eece4367 100644 --- a/backend/apps/system/api/aimodel.py +++ b/backend/apps/system/api/aimodel.py @@ -6,14 +6,14 @@ from fastapi import APIRouter, Query from sqlmodel import func, select, update from apps.system.models.system_model import AiModelDetail -from common.core.deps import SessionDep +from common.core.deps import SessionDep, Trans from common.utils.time import get_timestamp from common.utils.utils import SQLBotLogUtil router = APIRouter(tags=["system/aimodel"], prefix="/system/aimodel") @router.post("/status") -async def check_llm(info: AiModelCreator): +async def check_llm(info: AiModelCreator, trans: Trans): try: additional_params = {item.key: item.val for item in info.config_list} config = LLMConfig( @@ -28,7 +28,7 @@ async def check_llm(info: AiModelCreator): SQLBotLogUtil.info(f"check_llm result: {result}") except Exception as e: SQLBotLogUtil.error(f"Error checking LLM: {e}") - raise e + raise Exception(trans('i18n_llm.validate_error', msg = str(e))) @router.get("", response_model=list[AiModelGridItem]) async def query( @@ -102,11 +102,12 @@ async def update_model( @router.delete("/{id}") async def delete_model( session: SessionDep, + trans: Trans, id: int ): item = session.get(AiModelDetail, id) if item.default_model: - raise RuntimeError(f"Can not delete [${item.name}], because it is default model!") + raise Exception(trans('i18n_llm.delete_default_error', key = item.name)) session.delete(item) session.commit() diff --git a/backend/apps/system/api/login.py b/backend/apps/system/api/login.py index e2fd0bb2..8e360e08 100644 --- a/backend/apps/system/api/login.py +++ b/backend/apps/system/api/login.py @@ -2,7 +2,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from fastapi.security import OAuth2PasswordRequestForm from apps.system.schemas.system_schema import BaseUserDTO -from common.core.deps import SessionDep +from common.core.deps import SessionDep, Trans from ..crud.user import authenticate from common.core.security import create_access_token from datetime import timedelta @@ -13,16 +13,16 @@ router = APIRouter(tags=["login"], prefix="/login") @router.post("/access-token") def local_login( session: SessionDep, + trans: Trans, form_data: Annotated[OAuth2PasswordRequestForm, Depends()] ) -> Token: user: BaseUserDTO = authenticate(session=session, account=form_data.username, password=form_data.password) if not user: - raise HTTPException(status_code=400, detail="Incorrect account or password") - + raise HTTPException(status_code=400, detail=trans('i18n_login.account_pwd_error')) if not user.oid or user.oid == 0: - raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator") + raise HTTPException(status_code=400, detail=trans('i18n_login.no_associated_ws', msg = trans('i18n_concat_admin'))) if user.status != 1: - raise HTTPException(status_code=400, detail="User is disabled, Please contact the administrator") + raise HTTPException(status_code=400, detail=trans('i18n_login.user_disable', msg = trans('i18n_concat_admin'))) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) user_dict = user.to_dict() return Token(access_token=create_access_token( diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index d048ef6f..04c64432 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -99,7 +99,7 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int): ws_list: list[UserWs] = await user_ws_options(session, current_user.id) if not any(x.id == oid for x in ws_list): - raise HTTPException(f"oid [{oid}] is invalid!") + raise Exception(f"oid [{oid}] is invalid!") user_model: UserModel = get_db_user(session = session, user_id = current_user.id) user_model.oid = oid session.add(user_model) @@ -115,13 +115,13 @@ async def query(session: SessionDep, trans: Trans, id: int) -> UserEditor: return result @router.post("") -async def create(session: SessionDep, creator: UserCreator): +async def create(session: SessionDep, creator: UserCreator, trans: Trans): if check_account_exists(session=session, account=creator.account): - raise Exception(f"Account [{creator.account}] already exists!") + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.account') [{creator.account}]}")) if check_email_exists(session=session, email=creator.email): - raise Exception(f"Email [{creator.email}] already exists!") + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email') [{creator.email}]}")) if not check_email_format(creator.email): - raise Exception(f"Email [{creator.email}] format is invalid!") + raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email') [{creator.email}]}")) data = creator.model_dump(exclude_unset=True) user_model = UserModel.model_validate(data) #user_model.create_time = get_timestamp() @@ -144,16 +144,16 @@ async def create(session: SessionDep, creator: UserCreator): @router.put("") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id") -async def update(session: SessionDep, editor: UserEditor): +async def update(session: SessionDep, editor: UserEditor, trans: Trans): user_model: UserModel = get_db_user(session = session, user_id = editor.id) if not user_model: raise Exception(f"User with id [{editor.id}] not found!") if editor.account != user_model.account: raise Exception(f"account cannot be changed!") if editor.email != user_model.email and check_email_exists(session=session, account=editor.email): - raise Exception(f"Email [{editor.email}] already exists!") + raise Exception(trans('i18n_exist', msg = f"{trans('i18n_user.email') [{editor.email}]}")) if not check_email_format(editor.email): - raise Exception(f"Email [{editor.email}] format is invalid!") + raise Exception(trans('i18n_format_invalid', key = f"{trans('i18n_user.email') [{editor.email}]}")) origin_oid: int = user_model.oid del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == editor.id) session.exec(del_stmt) @@ -188,10 +188,10 @@ async def batch_del(session: SessionDep, id_list: list[int]): @router.put("/language") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") -async def langChange(session: SessionDep, current_user: CurrentUser, language: UserLanguage): +async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, language: UserLanguage): lang = language.language if lang not in ["zh-CN", "en"]: - return {"message": "Language not supported"} + raise Exception(trans('i18n_user.language_not_support', key = lang)) db_user: UserModel = get_db_user(session=session, user_id=current_user.id) db_user.language = lang session.add(db_user) @@ -199,9 +199,9 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U @router.patch("/pwd/{id}") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id") -async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int): +async def pwdReset(session: SessionDep, current_user: CurrentUser, trans: Trans, id: int): if not current_user.isAdmin: - raise HTTPException('only for admin') + raise Exception(trans('i18n_permission.no_permission', url = " patch[/user/pwd/id],", msg = trans('i18n_permission.only_admin'))) db_user: UserModel = get_db_user(session=session, user_id=id) db_user.password = default_md5_pwd() session.add(db_user) @@ -209,22 +209,22 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int): @router.put("/pwd") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id") -async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor): +async def pwdUpdate(session: SessionDep, current_user: CurrentUser, trans: Trans, editor: PwdEditor): new_pwd = editor.new_pwd if not check_pwd_format(new_pwd): - raise Exception("Password format is invalid!") + raise Exception(trans('i18n_format_invalid', key = trans('i18n_user.password'))) db_user: UserModel = get_db_user(session=session, user_id=current_user.id) if not verify_md5pwd(editor.pwd, db_user.password): - raise Exception(f"pwd [{editor.pwd}] error") + raise Exception(trans('i18n_error', key = trans('i18n_user.password'))) db_user.password = md5pwd(new_pwd) session.add(db_user) session.commit() @router.patch("/status") @clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="statusDto.id") -async def langChange(session: SessionDep, current_user: CurrentUser, statusDto: UserStatus): +async def langChange(session: SessionDep, current_user: CurrentUser, trans: Trans, statusDto: UserStatus): if not current_user.isAdmin: - raise Exception("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin'))) status = statusDto.status if status not in [0, 1]: return {"message": "status not supported"} diff --git a/backend/apps/system/api/workspace.py b/backend/apps/system/api/workspace.py index 5201200d..893333ca 100644 --- a/backend/apps/system/api/workspace.py +++ b/backend/apps/system/api/workspace.py @@ -17,15 +17,16 @@ router = APIRouter(tags=["system/workspace"], prefix="/system/workspace") async def option_pager( session: SessionDep, current_user: CurrentUser, + trans: Trans, pageNum: int, pageSize: int, oid: int = Query(description="空间ID"), keyword: Optional[str] = Query(None, description="搜索关键字(可选)"), ): if not current_user.isAdmin: - raise RuntimeError('only for admin') + raise Exception(trans('i18n_permission.no_permission', url = ", ", msg = trans('i18n_permission.only_admin'))) if not oid: - raise RuntimeError('oid miss error') + raise Exception(trans('i18n_miss_args', key = '[oid]')) pagination = PaginationParams(page=pageNum, size=pageSize) paginator = Paginator(session) stmt = select(UserModel.id, UserModel.account, UserModel.name).where( @@ -50,12 +51,13 @@ async def option_pager( async def option_user( session: SessionDep, current_user: CurrentUser, + trans: Trans, keyword: str = Query(description="搜索关键字") ): if not keyword: - raise HTTPException("keyword is required") + raise Exception(trans('i18n_miss_args', key = '[keyword]')) if (not current_user.isAdmin) and current_user.weight == 0: - raise RuntimeError("no permission to execute this api") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid = current_user.oid stmt = select(UserModel.id, UserModel.account, UserModel.name).where( @@ -76,13 +78,14 @@ async def option_user( async def pager( session: SessionDep, current_user: CurrentUser, + trans: Trans, pageNum: int, pageSize: int, keyword: Optional[str] = Query(None, description="搜索关键字(可选)"), oid: Optional[int] = Query(None, description="空间ID(仅admin用户生效)"), ): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) if current_user.isAdmin: workspace_id = oid if oid else current_user.oid else: @@ -112,9 +115,9 @@ async def pager( @router.post("/uws") -async def create(session: SessionDep, current_user: CurrentUser, creator: UserWsDTO): +async def create(session: SessionDep, current_user: CurrentUser, trans: Trans, creator: UserWsDTO): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid: int = creator.oid if (current_user.isAdmin and creator.oid) else current_user.oid weight = creator.weight if (current_user.isAdmin and creator.weight) else 0 # 判断uid_list以及oid合法性 @@ -134,9 +137,9 @@ async def create(session: SessionDep, current_user: CurrentUser, creator: UserWs session.commit() @router.put("/uws") -async def edit(session: SessionDep, editor: UserWsEditor): +async def edit(session: SessionDep, trans: Trans, editor: UserWsEditor): if not editor.oid or not editor.uid: - raise HTTPException("param [oid, uid] miss") + raise Exception(trans('i18n_miss_args', key = '[oid, uid]')) db_model = session.exec(select(UserWsModel).where(UserWsModel.uid == editor.uid, UserWsModel.oid == editor.oid)).first() if not db_model: raise HTTPException("uws not exist") @@ -150,9 +153,9 @@ async def edit(session: SessionDep, editor: UserWsEditor): session.commit() @router.delete("/uws") -async def delete(session: SessionDep, current_user: CurrentUser, dto: UserWsBase): +async def delete(session: SessionDep, current_user: CurrentUser, trans: Trans, dto: UserWsBase): if not current_user.isAdmin and current_user.weight == 0: - raise HTTPException("no permission to execute") + raise Exception(trans('i18n_permission.no_permission', url = '', msg = '')) oid: int = dto.oid if (current_user.isAdmin and dto.oid) else current_user.oid db_model_list: list[UserWsModel] = session.exec(select(UserWsModel).where(UserWsModel.uid.in_(dto.uid_list), UserWsModel.oid == oid)).all() if not db_model_list: diff --git a/backend/common/utils/locale.py b/backend/common/utils/locale.py index 32d9009d..eb107548 100644 --- a/backend/common/utils/locale.py +++ b/backend/common/utils/locale.py @@ -1,16 +1,15 @@ from pathlib import Path import json -from typing import Dict, Optional +from typing import Dict, Optional, Any from fastapi import Request class I18n: def __init__(self, locale_dir: str = "locales"): self.locale_dir = Path(locale_dir) - self.translations: Dict[str, Dict[str, str]] = {} + self.translations: Dict[str, Dict[str, Any]] = {} self.load_translations() def load_translations(self): - if not self.locale_dir.exists(): self.locale_dir.mkdir() return @@ -34,9 +33,21 @@ class I18nHelper: self.request = request self.lang = i18n.get_language(request) + def _get_nested_translation(self, data: Dict[str, Any], key_path: str) -> str: + keys = key_path.split('.') + current = data + + for key in keys: + if isinstance(current, dict) and key in current: + current = current[key] + else: + return key_path # 如果找不到,返回原键 + + return current if isinstance(current, str) else key_path + def __call__(self, key: str, **kwargs) -> str: lang_data = self.i18n.translations.get(self.lang, {}) - text = lang_data.get(key, key) # 找不到则返回原key + text = self._get_nested_translation(lang_data, key) if kwargs: try: diff --git a/backend/locales/en.json b/backend/locales/en.json index 1bb123f7..15602cf0 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -1,4 +1,28 @@ { "i18n_default_workspace": "Default workspace", - "i18n_ds_name_exist": "Name exist" + "i18n_ds_name_exist": "Name already exists", + "i18n_concat_admin": "Please contact administrator!", + "i18n_exist": "{msg} already exists!", + "i18n_error": "{key} error!", + "i18n_miss_args": "Missing {key} parameter!", + "i18n_format_invalid": "{key} format is incorrect!", + "i18n_login": { + "account_pwd_error": "Account or password error!", + "no_associated_ws": "No associated workspace, {msg}", + "user_disable": "Account disabled, {msg}" + }, + "i18n_user": { + "account": "Account", + "email": "Email", + "password": "Password", + "language_not_support": "System does not support [{key}] language!" + }, + "i18n_permission": { + "only_admin": "Only administrators can call this!", + "no_permission": "No permission to access {url}{msg}" + }, + "i18n_llm": { + "validate_error": "Validation failed [{msg}]", + "delete_default_error": "Cannot delete default model [{key}]!" + } } \ No newline at end of file diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index 8a9f8c2a..8bd2d5f4 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -1,4 +1,29 @@ { "i18n_default_workspace": "默认工作空间", - "i18n_ds_name_exist": "名称已存在" + "i18n_ds_name_exist": "名称已存在", + "i18n_concat_admin": "请联系管理员!", + "i18n_exist": "{msg}已存在!", + "i18n_error": "{key}错误!", + "i18n_miss_args": "缺失{key}参数!", + "i18n_format_invalid": "{key}格式不正确!", + "i18n_login": { + "account_pwd_error": "账号或密码错误!", + "no_associated_ws": "没有关联的工作空间,{msg}", + "user_disable": "账号已禁用,{msg}" + }, + "i18n_user": { + "account": "账号", + "email": "邮箱", + "password": "密码", + "language_not_support": "系统不支持[{key}]语言!" + }, + "i18n_permission": { + "only_admin": "仅支持管理员调用!", + "no_permission": "无权调用{url}{msg}" + + }, + "i18n_llm": { + "validate_error": "校验失败[{msg}]", + "delete_default_error": "无法删除默认模型[{key}]!" + } } \ No newline at end of file