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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user