feat: 新增工具插件、系统日志、workflow配置及前端优化
1. 新增工具插件(edit_file, python_executor, search_file, shell_executor, write_file) 2. 新增系统事件日志模块和API 3. 新增workflow配置文件和详情API 4. 前端增加SSE、错误边界、设置引导等组件 5. 优化认证加密、速率限制、配置加载等工具模块 6. 删除废弃的cluster和health API 7. 补充单元测试和集成测试 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,8 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 2
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = 7
|
||||
_INSECURE_SECRETS = {"secret", "114514", "changethiskey12345"}
|
||||
|
||||
|
||||
@@ -84,9 +85,51 @@ class Accessor:
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode.update({"exp": int(expire.timestamp())})
|
||||
to_encode.update({"exp": int(expire.timestamp()), "type": "access"})
|
||||
return jwt.encode(to_encode, _get_secret_key(), algorithm=ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def _create_refresh_token(data: dict) -> str:
|
||||
"""生成长效 refresh token(默认 7 天有效期)。"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
days=REFRESH_TOKEN_EXPIRE_DAYS
|
||||
)
|
||||
to_encode.update({"exp": int(expire.timestamp()), "type": "refresh"})
|
||||
return jwt.encode(to_encode, _get_secret_key(), algorithm=ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def verify_refresh_token(token: str) -> TokenData:
|
||||
"""校验 refresh token 有效性并返回用户身份;过期或类型不对抛 401。"""
|
||||
try:
|
||||
payload = jwt.decode(token, _get_secret_key(), algorithms=[ALGORITHM])
|
||||
if payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 refresh token",
|
||||
)
|
||||
return TokenData(**{k: v for k, v in payload.items() if k != "type"})
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Refresh token 已过期,请重新登录",
|
||||
)
|
||||
except (jwt.InvalidTokenError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的 refresh token",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def refresh_access_token(refresh_token: str) -> dict:
|
||||
"""用 refresh token 换取新的 access token + refresh token 对。"""
|
||||
token_data = Accessor.verify_refresh_token(refresh_token)
|
||||
payload = {"user_id": token_data.user_id, "username": token_data.username}
|
||||
return {
|
||||
"access_token": Accessor._create_access_token(payload),
|
||||
"refresh_token": Accessor._create_refresh_token(payload),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""校验明文口令是否匹配数据库中存储的哈希。"""
|
||||
@@ -105,8 +148,8 @@ class Accessor:
|
||||
return Accessor._decode_token(token)
|
||||
|
||||
@staticmethod
|
||||
def login_hashed_password(user: "User", password: str) -> str:
|
||||
"""完成登录核验:找不到用户或密码错误抛 401,否则签发新令牌。"""
|
||||
def login_hashed_password(user: "User", password: str) -> dict:
|
||||
"""完成登录核验:找不到用户或密码错误抛 401,否则签发 access + refresh 令牌对。"""
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
@@ -118,13 +161,21 @@ class Accessor:
|
||||
detail="用户名或密码错误",
|
||||
)
|
||||
token_payload = {"user_id": str(user.user_id), "username": user.user_name}
|
||||
return Accessor._create_access_token(data=token_payload)
|
||||
return {
|
||||
"access_token": Accessor._create_access_token(data=token_payload),
|
||||
"refresh_token": Accessor._create_refresh_token(data=token_payload),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""对明文口令做强哈希;空值或长度不足 6 位会抛 ValueError。"""
|
||||
"""对明文口令做强哈希;空值或不满足复杂度要求会抛 ValueError。"""
|
||||
if not password:
|
||||
raise ValueError("密码不能为空")
|
||||
if len(password) < 6:
|
||||
raise ValueError("密码长度不能小于 6 位")
|
||||
if len(password) < 8:
|
||||
raise ValueError("密码长度不能小于 8 位")
|
||||
has_upper = any(c.isupper() for c in password)
|
||||
has_lower = any(c.islower() for c in password)
|
||||
has_digit = any(c.isdigit() for c in password)
|
||||
if not (has_upper and has_lower and has_digit):
|
||||
raise ValueError("密码必须包含大写字母、小写字母和数字")
|
||||
return password_hasher.hash(password)
|
||||
|
||||
Reference in New Issue
Block a user