feat: 添加视觉设计语言系统并重构认证页面UI

- 新增 visual_design_language.md 设计规范文档
- 新增 auth 设计 tokens (authBackground, authCard, authInput, feedback 系列等)
- 重构登录/注册/验证码/重置密码页面为新设计系统
- 新增 AuthHeroHeader, AuthSurfaceCard, AuthSection, AuthField, PasswordField 组件
- 重构 AppBanner 和 Toast 支持多类型配置 (info/success/warning/error)
- 后端 AgentScope: 重整 schemas/prompts/tools 作用域, 新增协议文档
- 更新 AGENTS.md 集成视觉设计语言约束
This commit is contained in:
qzl
2026-03-13 14:10:13 +08:00
parent fb3c649db7
commit a10a2db27a
100 changed files with 6333 additions and 4800 deletions
+6 -14
View File
@@ -64,22 +64,14 @@ class AgentAttachmentStorage:
if storage is None:
raise RuntimeError("Supabase storage client unavailable")
get_bucket = getattr(storage, "get_bucket", None)
if callable(get_bucket):
try:
get_bucket(bucket)
return
except Exception: # noqa: BLE001
pass
create_bucket = getattr(storage, "create_bucket", None)
if not callable(create_bucket):
raise RuntimeError("Supabase storage create_bucket is unavailable")
if not callable(get_bucket):
raise RuntimeError("Supabase storage get_bucket is unavailable")
try:
create_bucket(bucket, options={"public": False})
get_bucket(bucket)
except Exception as exc: # noqa: BLE001
message = str(exc).lower()
if "already exists" in message or "duplicate" in message:
return
msg = str(exc).lower()
if "bucket" in msg and "not found" in msg:
raise RuntimeError(f"Storage bucket '{bucket}' does not exist")
raise
await asyncio.to_thread(_ensure)
+10 -11
View File
@@ -1,39 +1,38 @@
from __future__ import annotations
from collections.abc import AsyncIterator
import asyncio
from datetime import date
import os
import re
import tempfile
import time
from collections.abc import AsyncIterator
from datetime import date
from typing import Annotated, Union
from ag_ui.core import RunAgentInput
from core.agentscope.events import to_sse_event
from core.auth.jwt_verifier import JwtVerifier, TokenValidationError
from core.auth.models import CurrentUser
from core.config.settings import config
from core.logging import get_logger
from fastapi import (
APIRouter,
Depends,
File,
Form,
Header,
HTTPException,
Query,
Request,
status,
UploadFile,
status,
)
from fastapi import HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from core.agentscope.events import to_sse_event
from core.agentscope.schemas.agui_input import (
from schemas.agent.agui_input import (
extract_latest_tool_result,
parse_run_input,
validate_run_request_messages_contract,
)
from core.auth.jwt_verifier import JwtVerifier, TokenValidationError
from core.auth.models import CurrentUser
from core.config.settings import config
from core.logging import get_logger
from services.base.redis import get_or_init_redis_client
from v1.agent.dependencies import get_agent_service
from v1.agent.schemas import (
+7 -4
View File
@@ -11,6 +11,7 @@ from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.friendships import Friendship, FriendshipStatus
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
from schemas.inbox.messages import FriendshipContent
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
@@ -80,7 +81,7 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
super().__init__(session, Friendship)
async def create_request(
self, initiator_id: UUID, recipient_id: UUID, content: str | None = None
self, initiator_id: UUID, recipient_id: UUID, message: str | None = None
) -> tuple[Friendship, InboxMessage]:
try:
user_low_id = min(initiator_id, recipient_id)
@@ -99,12 +100,13 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
self._session.add(friendship)
await self._session.flush()
inbox_content = FriendshipContent(type="request", message=message)
inbox = InboxMessage(
recipient_id=recipient_id,
sender_id=initiator_id,
message_type=InboxMessageType.FRIEND_REQUEST,
friendship_id=friendship.id,
content=content,
content=inbox_content.model_dump(),
status=InboxMessageStatus.PENDING,
created_by=initiator_id,
)
@@ -124,7 +126,7 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
self,
friendship: Friendship,
initiator_id: UUID,
content: str | None = None,
message: str | None = None,
) -> tuple[Friendship, InboxMessage]:
try:
now = datetime.now(timezone.utc)
@@ -133,6 +135,7 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
friendship.initiator_id = initiator_id
friendship.updated_by = initiator_id
inbox_content = FriendshipContent(type="request", message=message)
inbox = InboxMessage(
recipient_id=(
friendship.user_low_id
@@ -142,7 +145,7 @@ class SQLAlchemyFriendshipRepository(BaseRepository[Friendship]):
sender_id=initiator_id,
message_type=InboxMessageType.FRIEND_REQUEST,
friendship_id=friendship.id,
content=content,
content=inbox_content.model_dump(),
status=InboxMessageStatus.PENDING,
created_by=initiator_id,
)
+1 -1
View File
@@ -20,7 +20,7 @@ class FriendRequestResponse(BaseModel):
id: UUID
sender: UserContext
recipient: UserContext
content: str | None
content: dict | None
status: Literal["pending", "accepted", "rejected", "canceled"]
created_at: datetime
+5 -3
View File
@@ -6,17 +6,19 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from schemas.schedule.items import (
AttachmentType,
from schemas.inbox.messages import (
CalendarContent,
CalendarDeleteContent,
CalendarInviteContent,
CalendarUpdateContent,
parse_calendar_content,
)
from schemas.schedule.items import (
AttachmentType,
ScheduleItemMetadata,
ScheduleItemMetadataAttachment,
ScheduleItemSourceType,
ScheduleItemStatus,
parse_calendar_content,
)
__all__ = [
+16 -23
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Protocol, Literal
from uuid import UUID
@@ -345,26 +344,22 @@ class ScheduleItemService(BaseService):
)
elif existing_msg.status == InboxMessageStatus.REJECTED:
existing_msg.status = InboxMessageStatus.PENDING
existing_msg.content = json.dumps(
{
"type": "invite",
"permission": request_permission,
"action": "pending",
}
)
existing_msg.content = {
"type": "invite",
"permission": request_permission,
"action": "pending",
}
else:
message = InboxMessage(
recipient_id=recipient_id,
sender_id=user_id,
message_type=InboxMessageType.CALENDAR,
schedule_item_id=item.id,
content=json.dumps(
{
"type": "invite",
"permission": request_permission,
"action": "pending",
}
),
content={
"type": "invite",
"permission": request_permission,
"action": "pending",
},
created_by=user_id,
)
self._session.add(message)
@@ -432,7 +427,7 @@ class ScheduleItemService(BaseService):
status_code=404, detail="No pending invitation found"
)
content = json.loads(inbox.content or "{}")
content = inbox.content or {}
permission = content.get("permission", 1)
existing = await self._repository.get_subscription(item_id, user_id)
@@ -505,13 +500,11 @@ class ScheduleItemService(BaseService):
if sub.subscriber_id == user_id:
continue
content = json.dumps(
{
"type": action_type,
"title": title,
"action": action_type,
}
)
content = {
"type": action_type,
"title": title,
"action": action_type,
}
message = InboxMessage(
recipient_id=sub.subscriber_id,
+6 -13
View File
@@ -11,26 +11,19 @@ from pydantic import (
model_validator,
)
from schemas.user.context import UserContext
class UserResponse(BaseModel):
id: str
username: str
email: str | None = None
avatar_url: str | None = None
bio: str | None = None
class UserResponse(UserContext):
"""当前用户,含 email,无 settings"""
settings: None = Field(default=None, exclude=True) # type: ignore[assignment]
class UserSearchRequest(BaseModel):
query: str = Field(min_length=1, max_length=100)
class UserSearchResult(BaseModel):
id: str
username: str
avatar_url: str | None = None
bio: str | None = None
class UserUpdateRequest(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")