refactor: 移除前端 Mock API,新增共享组件,优化认证流程

- 删除 mock_api_client、mock_calendar_service、mock_history_service
- 新增 fixed_length_code_input、link_button、message_composer 共享组件
- 优化登录/注册/密码重置页面使用新组件
- 简化 injection.dart 移除 mock 分支
- 更新 env.dart 配置(BACKEND_URL 替换 API_URL)
- 后端 agentscope 工具和测试更新
- 重构 AGENTS.md 文档结构
- 新增 deploy/ 目录和 protocol 文档
This commit is contained in:
qzl
2026-03-12 16:41:45 +08:00
parent d7fbb74bf8
commit 01c36eb32e
70 changed files with 5138 additions and 5829 deletions
@@ -1,7 +1,7 @@
from core.agentscope.tools.custom.calendar import (
calendar_share,
calendar_read,
calendar_write,
user_resolve,
)
__all__ = ["calendar_read", "calendar_write", "user_resolve"]
__all__ = ["calendar_read", "calendar_write", "calendar_share"]
@@ -1,42 +1,21 @@
from typing import Annotated, Any, Literal, cast
from uuid import UUID
from fastapi import HTTPException
from pydantic import Field
from core.auth.jwt_verifier import JwtVerifier, TokenValidationError
from core.agentscope.tools.custom.calendar_backend_ops import (
_execute_list_calendar_events,
_execute_mutate_calendar_event,
_execute_resolve_user_identity,
_execute_share_calendar_event,
)
from core.agentscope.tools.tool_response_builder import build_tool_response
from core.agentscope.schemas.ui_schema import (
build_calendar_list,
build_calendar_operation,
)
from core.config.settings import config
from core.agentscope.tools.response import build_tool_response
def _unauthorized_response() -> dict[str, object]:
return {
"type": "calendar_operation.v1",
"version": "v1",
"data": {
"ok": False,
"code": "UNAUTHORIZED",
"message": "calendar.write requires validated user token",
},
"actions": [],
}
def _invalid_argument_response(*, message: str) -> dict[str, object]:
return {
"type": "calendar_operation.v1",
"version": "v1",
"data": {
"ok": False,
"code": "INVALID_ARGUMENT",
"message": message,
},
"actions": [],
}
def _verify_user_token(*, user_token: str, owner_id: UUID) -> bool:
@@ -56,6 +35,67 @@ def _verify_user_token(*, user_token: str, owner_id: UUID) -> bool:
return isinstance(subject, str) and subject == str(owner_id)
def _failure_response(
*,
card_type: Literal["calendar_event_list.v1", "calendar_operation.v1"],
operation: str | None,
code: str,
message: str,
) -> dict[str, object]:
if card_type == "calendar_event_list.v1":
return build_calendar_list(
items=[],
page=1,
page_size=20,
total=0,
) | {"data": {"ok": False, "code": code, "message": message}}
return build_calendar_operation(
operation=operation or "operation",
ok=False,
message=message,
code=code,
)
def _authorized_or_response(
*,
session: Any,
owner_id: Any,
user_token: str | None,
card_type: Literal["calendar_event_list.v1", "calendar_operation.v1"],
operation: str | None,
) -> tuple[Any, UUID] | dict[str, object]:
if session is None or owner_id is None:
raise ValueError("calendar tool missing runtime preset arguments")
if not isinstance(user_token, str) or not user_token.strip():
return _failure_response(
card_type=card_type,
operation=operation,
code="UNAUTHORIZED",
message="calendar tool requires validated user token",
)
if not _verify_user_token(user_token=user_token, owner_id=cast(UUID, owner_id)):
return _failure_response(
card_type=card_type,
operation=operation,
code="UNAUTHORIZED",
message="calendar tool requires validated user token",
)
return cast(Any, session), cast(UUID, owner_id)
def _map_exception(exc: Exception) -> tuple[str, str]:
if isinstance(exc, HTTPException):
detail = exc.detail
if isinstance(detail, str) and detail.strip():
return "OPERATION_FAILED", detail.strip()
return "OPERATION_FAILED", "calendar operation failed"
if isinstance(exc, ValueError):
return "INVALID_ARGUMENT", str(exc)
return "INTERNAL_ERROR", "calendar operation failed"
async def calendar_read(
query: Annotated[
str | None,
@@ -73,31 +113,34 @@ async def calendar_read(
owner_id: Any = None,
user_token: str | None = None,
) -> Any:
"""Read calendar events and return a structured paginated response.
Args:
query: Optional search keyword for event filtering.
page: Page index starting from 1.
page_size: Page size for pagination.
session: Runtime-injected database session.
owner_id: Runtime-injected user ID.
user_token: Runtime-injected user access token.
Returns:
A tool response payload containing a calendar event list.
"""
if session is None or owner_id is None:
raise ValueError("calendar.read missing runtime preset arguments")
if not isinstance(user_token, str) or not user_token.strip():
return build_tool_response(_unauthorized_response())
if not _verify_user_token(user_token=user_token, owner_id=cast(UUID, owner_id)):
return build_tool_response(_unauthorized_response())
result = await _execute_list_calendar_events(
session=cast(Any, session),
owner_id=cast(UUID, owner_id),
tool_args={"query": query, "page": page, "pageSize": page_size},
auth_result = _authorized_or_response(
session=session,
owner_id=owner_id,
user_token=user_token,
card_type="calendar_event_list.v1",
operation=None,
)
if isinstance(auth_result, dict):
return build_tool_response(auth_result)
runtime_session, runtime_owner_id = auth_result
try:
result = await _execute_list_calendar_events(
session=runtime_session,
owner_id=runtime_owner_id,
tool_args={"query": query, "page": page, "pageSize": page_size},
)
except Exception as exc:
code, message = _map_exception(exc)
return build_tool_response(
_failure_response(
card_type="calendar_event_list.v1",
operation=None,
code=code,
message=message,
)
)
return build_tool_response(result)
@@ -151,6 +194,68 @@ async def calendar_write(
bool,
Field(description="Whether to use the replace strategy for conflicts."),
] = False,
session: Any = None,
owner_id: Any = None,
user_token: str | None = None,
) -> Any:
auth_result = _authorized_or_response(
session=session,
owner_id=owner_id,
user_token=user_token,
card_type="calendar_operation.v1",
operation=operation,
)
if isinstance(auth_result, dict):
return build_tool_response(auth_result)
runtime_session, runtime_owner_id = auth_result
tool_args: dict[str, object] = {"operation": operation, "replace": replace}
if event_id is not None:
tool_args["eventId"] = event_id
if title is not None:
tool_args["title"] = title
if description is not None:
tool_args["description"] = description
if start_at is not None:
tool_args["startAt"] = start_at
if end_at is not None:
tool_args["endAt"] = end_at
if timezone is not None:
tool_args["timezone"] = timezone
if location is not None:
tool_args["location"] = location
if color is not None:
tool_args["color"] = color
if reminder_minutes is not None:
tool_args["reminderMinutes"] = reminder_minutes
if status is not None:
tool_args["status"] = status
try:
result = await _execute_mutate_calendar_event(
session=runtime_session,
owner_id=runtime_owner_id,
tool_args=tool_args,
)
except Exception as exc:
code, message = _map_exception(exc)
return build_tool_response(
_failure_response(
card_type="calendar_operation.v1",
operation=operation,
code=code,
message=message,
)
)
return build_tool_response(result)
async def calendar_share(
event_id: Annotated[
str,
Field(description="Target event ID (UUID string)."),
],
invite_user_emails: Annotated[
list[str] | None,
Field(description="Optional invite targets by email."),
@@ -179,136 +284,45 @@ async def calendar_write(
owner_id: Any = None,
user_token: str | None = None,
) -> Any:
"""Execute calendar write operations with runtime authorization checks.
Args:
operation: Write operation type.
event_id: Target event ID.
title: Event title.
description: Event description.
start_at: Event start time in ISO 8601 format.
end_at: Event end time in ISO 8601 format.
timezone: Event timezone.
location: Event location.
color: Event color.
reminder_minutes: Reminder minutes before event start.
status: Event lifecycle status.
replace: Replace-strategy flag for conflict handling.
session: Runtime-injected database session.
owner_id: Runtime-injected user ID.
user_token: Runtime-injected user access token.
Returns:
A tool response payload describing the mutation result.
"""
if operation in ("update", "delete") and (
not isinstance(event_id, str) or not event_id.strip()
):
return build_tool_response(
_invalid_argument_response(
message="event_id is required for update and delete operations"
)
)
if operation == "create" and isinstance(event_id, str) and event_id.strip():
return build_tool_response(
_invalid_argument_response(
message="event_id must not be provided for create operation"
)
)
if isinstance(title, str) and len(title.strip()) > 255:
return build_tool_response(
_invalid_argument_response(message="title length must be <= 255")
)
if isinstance(description, str) and len(description.strip()) > 2000:
return build_tool_response(
_invalid_argument_response(message="description length must be <= 2000")
)
if isinstance(timezone, str) and len(timezone.strip()) > 50:
return build_tool_response(
_invalid_argument_response(message="timezone length must be <= 50")
)
if reminder_minutes is not None and (
reminder_minutes < 0 or reminder_minutes > 10080
):
return build_tool_response(
_invalid_argument_response(message="reminder_minutes must be 0..10080")
)
if session is None or owner_id is None:
raise ValueError("calendar.write missing runtime preset arguments")
if not isinstance(user_token, str) or not user_token.strip():
return build_tool_response(_unauthorized_response())
if not _verify_user_token(user_token=user_token, owner_id=cast(UUID, owner_id)):
return build_tool_response(_unauthorized_response())
auth_result = _authorized_or_response(
session=session,
owner_id=owner_id,
user_token=user_token,
card_type="calendar_operation.v1",
operation="share",
)
if isinstance(auth_result, dict):
return build_tool_response(auth_result)
runtime_session, runtime_owner_id = auth_result
tool_args: dict[str, object] = {
"operation": operation,
"replace": replace,
"eventId": event_id,
"invitePermissionView": invite_permission_view,
"invitePermissionEdit": invite_permission_edit,
"invitePermissionInvite": invite_permission_invite,
}
if event_id is not None:
tool_args["eventId"] = event_id
if title is not None:
tool_args["title"] = title
if description is not None:
tool_args["description"] = description
if start_at is not None:
tool_args["startAt"] = start_at
if end_at is not None:
tool_args["endAt"] = end_at
if timezone is not None:
tool_args["timezone"] = timezone
if location is not None:
tool_args["location"] = location
if color is not None:
tool_args["color"] = color
if reminder_minutes is not None:
tool_args["reminderMinutes"] = reminder_minutes
if status is not None:
tool_args["status"] = status
if invite_user_emails is not None:
tool_args["inviteUserEmails"] = invite_user_emails
if invite_user_names is not None:
tool_args["inviteUserNames"] = invite_user_names
if invite_user_ids is not None:
tool_args["inviteUserIds"] = invite_user_ids
tool_args["invitePermissionView"] = invite_permission_view
tool_args["invitePermissionEdit"] = invite_permission_edit
tool_args["invitePermissionInvite"] = invite_permission_invite
result = await _execute_mutate_calendar_event(
session=cast(Any, session),
owner_id=cast(UUID, owner_id),
tool_args=tool_args,
)
return build_tool_response(result)
async def user_resolve(
user_email: Annotated[
str | None,
Field(description="User email to resolve user ID."),
] = None,
user_name: Annotated[
str | None,
Field(description="Username to resolve user ID."),
] = None,
session: Any = None,
owner_id: Any = None,
user_token: str | None = None,
) -> Any:
if session is None or owner_id is None:
raise ValueError("user.resolve missing runtime preset arguments")
if not isinstance(user_token, str) or not user_token.strip():
return build_tool_response(_unauthorized_response())
if not _verify_user_token(user_token=user_token, owner_id=cast(UUID, owner_id)):
return build_tool_response(_unauthorized_response())
result = await _execute_resolve_user_identity(
session=cast(Any, session),
owner_id=cast(UUID, owner_id),
tool_args={
"userEmail": user_email,
"userName": user_name,
},
)
try:
result = await _execute_share_calendar_event(
session=runtime_session,
owner_id=runtime_owner_id,
tool_args=tool_args,
)
except Exception as exc:
code, message = _map_exception(exc)
return build_tool_response(
_failure_response(
card_type="calendar_operation.v1",
operation="share",
code=code,
message=message,
)
)
return build_tool_response(result)
@@ -379,12 +379,6 @@ async def _execute_create(
)
event_data = _event_payload(created)
event_id = str(event_data["id"])
invite_result = await _share_event_with_invitees(
session=service._session,
owner_id=service.require_user_id(),
event_id=UUID(event_id),
tool_args=tool_args,
)
return {
"type": "calendar_card.v1",
"version": "v1",
@@ -393,7 +387,6 @@ async def _execute_create(
"sourceType": "agent_generated",
"ok": True,
"message": "日程已创建",
"inviteResult": invite_result,
},
"actions": [
{
@@ -475,12 +468,6 @@ async def _execute_update(
ScheduleItemUpdateRequest.model_validate(update_data),
)
event_data = _event_payload(updated)
invite_result = await _share_event_with_invitees(
session=service._session,
owner_id=service.require_user_id(),
event_id=UUID(str(event_data["id"])),
tool_args=tool_args,
)
return {
"type": "calendar_card.v1",
"version": "v1",
@@ -489,7 +476,6 @@ async def _execute_update(
"sourceType": "agent_generated",
"ok": True,
"message": "日程已更新",
"inviteResult": invite_result,
},
"actions": [
{
@@ -538,3 +524,34 @@ async def _execute_mutate_calendar_event(
if operation == "delete":
return await _execute_delete(service=service, tool_args=tool_args)
raise ValueError("operation must be one of: create, update, delete")
async def _execute_share_calendar_event(
*,
session: AsyncSession,
owner_id: UUID,
tool_args: dict[str, object],
) -> dict[str, object]:
event_id = _parse_event_id(tool_args.get("eventId"))
invite_result = await _share_event_with_invitees(
session=session,
owner_id=owner_id,
event_id=event_id,
tool_args=tool_args,
)
if invite_result is None:
raise ValueError(
"at least one invite target is required: inviteUserEmails, inviteUserNames, or inviteUserIds"
)
return {
"type": "calendar_operation.v1",
"version": "v1",
"data": {
"operation": "share",
"id": str(event_id),
"ok": True,
"message": "日程已分享",
"shareResult": invite_result,
},
"actions": [],
}
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any, AsyncGenerator, Callable
from core.agentscope.tools.response import build_tool_response
from core.agentscope.tools.tool_response_builder import build_tool_response
from core.agentscope.tools.tool_meta import ToolMeta
@@ -6,7 +6,7 @@ from dataclasses import dataclass
TOOL_APPROVAL_REQUIRED: dict[str, bool] = {
"calendar_read": False,
"calendar_write": False,
"user_resolve": False,
"calendar_share": False,
}
+5 -7
View File
@@ -7,9 +7,9 @@ from uuid import UUID
from sqlalchemy.ext.asyncio import AsyncSession
from core.agentscope.tools.custom.calendar import (
calendar_share,
calendar_read,
calendar_write,
user_resolve,
)
from core.agentscope.tools.hitl_middleware import register_tool_middlewares
from core.agentscope.tools.tool_meta import TOOL_META
@@ -29,12 +29,10 @@ class ToolGroup:
TOOL_GROUPS: dict[str, ToolGroup] = {
"intent": ToolGroup(
stage="intent", tool_names=frozenset({"calendar_read", "user_resolve"})
),
"intent": ToolGroup(stage="intent", tool_names=frozenset({"calendar_read"})),
"execution": ToolGroup(
stage="execution",
tool_names=frozenset({"calendar_read", "calendar_write", "user_resolve"}),
tool_names=frozenset({"calendar_read", "calendar_write", "calendar_share"}),
),
"report": ToolGroup(stage="report", tool_names=frozenset()),
}
@@ -73,8 +71,8 @@ def _load_custom_tool_bindings(
},
),
CustomToolBinding(
name="user_resolve",
func=user_resolve,
name="calendar_share",
func=calendar_share,
preset_kwargs={
"session": session,
"owner_id": owner_id,