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
@@ -47,11 +47,6 @@ def build_tool_content_summary(
if target:
return _truncate(f"已分享日程给 {target}")
if tool_name == "user_resolve":
target = _pick_first_str(normalized_result, ("name", "userName", "userId"))
if target:
return _truncate(f"已匹配用户:{target}")
result_content = _pick_first_str(normalized_result, ("content", "message"))
if result_content:
return _truncate(result_content)
@@ -0,0 +1,770 @@
"""
UI Schema Protocol Implementation.
This module is the single source of truth for UI Schema.
All implementations must follow docs/protocols/ui-schema.md.
Version: 1.0
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Literal, NotRequired, TypedDict, Union
# ========== Enums ==========
class SchemaType(str, Enum):
"""Schema type identifier."""
TOOL_RESULT = "tool_result"
AGENT_RESPONSE = "agent_response"
NOTIFICATION = "notification"
class UiStatus(str, Enum):
"""Unified status for all nodes."""
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
PENDING = "pending"
class IconSource(str, Enum):
"""Icon source type."""
ICON = "icon"
EMOJI = "emoji"
URL = "url"
class ActionType(str, Enum):
"""Action type identifier."""
NAVIGATION = "navigation"
URL = "url"
EVENT = "event"
TOOL = "tool"
COPY = "copy"
PAYLOAD = "payload"
class OperationType(str, Enum):
"""Operation node operation type."""
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
EXECUTE = "execute"
class OperationResult(str, Enum):
"""Operation node result type."""
SUCCESS = "success"
FAILURE = "failure"
PARTIAL = "partial"
class ContainerDirection(str, Enum):
"""Container node direction."""
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
class TextFormat(str, Enum):
"""Text node format."""
PLAIN = "plain"
MARKDOWN = "markdown"
class KvLayout(str, Enum):
"""Key-value node layout."""
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
GRID = "grid"
class BadgeVariant(str, Enum):
"""Badge variant."""
DEFAULT = "default"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
INFO = "info"
class ActionStyle(str, Enum):
"""Action button style."""
PRIMARY = "primary"
SECONDARY = "secondary"
GHOST = "ghost"
DANGER = "danger"
class RendererTheme(str, Enum):
"""Renderer theme."""
DEFAULT = "default"
DARK = "dark"
LIGHT = "light"
# ========== Common Types ==========
class UiIcon(TypedDict, total=False):
"""Icon structure."""
source: IconSource
value: str
color: str
size: int
class UiBadge(TypedDict, total=False):
"""Badge structure."""
label: str
variant: BadgeVariant
class Pagination(TypedDict):
"""Pagination info."""
page: int
pageSize: int
total: int
hasMore: bool
class ActionConfirm(TypedDict, total=False):
"""Action confirmation config."""
title: str
message: str
confirmLabel: str
cancelLabel: str
class KeyValuePair(TypedDict, total=False):
"""Key-value pair."""
key: str
label: str
value: Union[str, int, bool]
copyable: bool
class TableColumn(TypedDict, total=False):
"""Table column definition."""
key: str
label: str
width: str
align: Literal["left", "center", "right"]
class TableRow(TypedDict, total=False):
"""Table row."""
id: str
cells: dict[str, Any]
metadata: dict[str, Any]
actions: list[dict[str, Any]]
class ListItem(TypedDict, total=False):
"""List item."""
id: str
title: str
subtitle: str
description: str
icon: UiIcon
badge: UiBadge
metadata: dict[str, Any]
actions: list[dict[str, Any]]
# ========== Action Types ==========
class NavigateAction(TypedDict):
"""Navigate to internal path."""
type: Literal["navigation"]
path: str
params: NotRequired[dict[str, Any]]
class LinkAction(TypedDict):
"""Open external URL."""
type: Literal["url"]
url: str
target: NotRequired[Literal["_self", "_blank"]]
class EventAction(TypedDict):
"""Trigger frontend event."""
type: Literal["event"]
event: str
payload: NotRequired[dict[str, Any]]
class ToolAction(TypedDict):
"""Re-execute a tool."""
type: Literal["tool"]
toolId: str
params: NotRequired[dict[str, Any]]
class CopyAction(TypedDict):
"""Copy content to clipboard."""
type: Literal["copy"]
content: str
successMessage: NotRequired[str]
class PayloadAction(TypedDict):
"""Submit payload to endpoint."""
type: Literal["payload"]
payload: dict[str, Any]
submitTo: NotRequired[str]
# ========== Action ==========
class UiAction(TypedDict, total=False):
"""Action structure."""
id: str
label: str
icon: UiIcon
style: ActionStyle
disabled: bool
action: (
NavigateAction
| LinkAction
| EventAction
| ToolAction
| CopyAction
| PayloadAction
)
confirm: ActionConfirm
# ========== Node Types (using dict for simplicity) ==========
# Type alias for any node
UiNode = dict[str, Any]
# ========== Builder Functions ==========
def build_document(
status: UiStatus,
nodes: list[UiNode],
*,
version: str = "1.0",
schema_type: SchemaType = SchemaType.TOOL_RESULT,
doc_id: str | None = None,
timestamp: str | None = None,
locale: str = "zh-CN",
renderer: dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build a UI schema document."""
doc: dict[str, Any] = {
"version": version,
"schemaType": schema_type.value,
"status": status.value,
"nodes": nodes,
}
if doc_id:
doc["docId"] = doc_id
if timestamp:
doc["timestamp"] = timestamp
if locale:
doc["locale"] = locale
if renderer:
doc["renderer"] = renderer
if meta:
doc["meta"] = meta
return doc
def build_success_document(
nodes: list[UiNode],
**kwargs: Any,
) -> dict[str, Any]:
"""Build a success document."""
return build_document(status=UiStatus.SUCCESS, nodes=nodes, **kwargs)
def build_error_document(
nodes: list[UiNode],
**kwargs: Any,
) -> dict[str, Any]:
"""Build an error document."""
return build_document(status=UiStatus.ERROR, nodes=nodes, **kwargs)
def build_text_node(
content: str,
*,
node_id: str | None = None,
format: TextFormat = TextFormat.PLAIN,
icon: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a text node."""
node: dict[str, Any] = {
"type": "text",
"content": content,
"format": format.value,
}
if node_id:
node["id"] = node_id
if icon:
node["icon"] = icon
if actions:
node["actions"] = actions
return node
def build_card_node(
*,
node_id: str | None = None,
title: str | None = None,
description: str | None = None,
icon: dict[str, Any] | None = None,
status: UiStatus | None = None,
timestamp: str | None = None,
children: list[UiNode] | None = None,
footer: dict[str, Any] | None = None,
extensions: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a card node."""
node: dict[str, Any] = {"type": "card"}
if node_id:
node["id"] = node_id
if title:
node["title"] = title
if description:
node["description"] = description
if icon:
node["icon"] = icon
if status:
node["status"] = status.value
if timestamp:
node["timestamp"] = timestamp
if children:
node["children"] = children
if footer:
node["footer"] = footer
if extensions:
node["extensions"] = extensions
if actions:
node["actions"] = actions
return node
def build_kv_node(
pairs: list[dict[str, Any]],
*,
node_id: str | None = None,
title: str | None = None,
description: str | None = None,
icon: dict[str, Any] | None = None,
status: UiStatus | None = None,
layout: KvLayout = KvLayout.VERTICAL,
extensions: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a key-value node."""
node: dict[str, Any] = {
"type": "kv",
"pairs": pairs,
"layout": layout.value,
}
if node_id:
node["id"] = node_id
if title:
node["title"] = title
if description:
node["description"] = description
if icon:
node["icon"] = icon
if status:
node["status"] = status.value
if extensions:
node["extensions"] = extensions
if actions:
node["actions"] = actions
return node
def build_list_node(
items: list[dict[str, Any]],
*,
node_id: str | None = None,
title: str | None = None,
description: str | None = None,
icon: dict[str, Any] | None = None,
status: UiStatus | None = None,
pagination: dict[str, Any] | None = None,
empty_text: str | None = None,
extensions: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a list node."""
node: dict[str, Any] = {
"type": "list",
"items": items,
}
if node_id:
node["id"] = node_id
if title:
node["title"] = title
if description:
node["description"] = description
if icon:
node["icon"] = icon
if status:
node["status"] = status.value
if pagination:
node["pagination"] = pagination
if empty_text:
node["emptyText"] = empty_text
if extensions:
node["extensions"] = extensions
if actions:
node["actions"] = actions
return node
def build_error_node(
error_code: str,
message: str,
*,
node_id: str | None = None,
title: str | None = None,
icon: dict[str, Any] | None = None,
details: str | None = None,
stack: str | None = None,
retryable: bool = False,
suggestions: list[str] | None = None,
retry: dict[str, Any] | None = None,
support: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build an error node."""
node: dict[str, Any] = {
"type": "error",
"errorCode": error_code,
"message": message,
"retryable": retryable,
}
if node_id:
node["id"] = node_id
if title:
node["title"] = title
if icon:
node["icon"] = icon
if details:
node["details"] = details
if stack:
node["stack"] = stack
if suggestions:
node["suggestions"] = suggestions
if retry:
node["retry"] = retry
if support:
node["support"] = support
if actions:
node["actions"] = actions
return node
def build_operation_node(
operation: OperationType,
result: OperationResult,
*,
node_id: str | None = None,
title: str | None = None,
description: str | None = None,
icon: dict[str, Any] | None = None,
status: UiStatus | None = None,
message: str | None = None,
affected_count: int | None = None,
details: dict[str, Any] | None = None,
rollback: dict[str, Any] | None = None,
extensions: dict[str, Any] | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build an operation node."""
node: dict[str, Any] = {
"type": "operation",
"operation": operation.value,
"result": result.value,
}
if node_id:
node["id"] = node_id
if title:
node["title"] = title
if description:
node["description"] = description
if icon:
node["icon"] = icon
if status:
node["status"] = status.value
if message:
node["message"] = message
if affected_count is not None:
node["affectedCount"] = affected_count
if details:
node["details"] = details
if rollback:
node["rollback"] = rollback
if extensions:
node["extensions"] = extensions
if actions:
node["actions"] = actions
return node
def build_container_node(
children: list[UiNode],
direction: ContainerDirection = ContainerDirection.VERTICAL,
*,
node_id: str | None = None,
gap: int | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Build a container node."""
node: dict[str, Any] = {
"type": "container",
"direction": direction.value,
"children": children,
}
if node_id:
node["id"] = node_id
if gap is not None:
node["gap"] = gap
if actions:
node["actions"] = actions
return node
def build_action(
action_id: str,
label: str,
action: (
NavigateAction
| LinkAction
| EventAction
| ToolAction
| CopyAction
| PayloadAction
),
*,
icon: dict[str, Any] | None = None,
style: ActionStyle | None = None,
disabled: bool = False,
confirm: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build an action."""
act: dict[str, Any] = {
"id": action_id,
"label": label,
"action": action,
}
if icon:
act["icon"] = icon
if style:
act["style"] = style.value
if disabled:
act["disabled"] = disabled
if confirm:
act["confirm"] = confirm
return act
def build_icon(
source: IconSource,
value: str,
*,
color: str | None = None,
size: int | None = None,
) -> dict[str, Any]:
"""Build an icon."""
icon: dict[str, Any] = {"source": source.value, "value": value}
if color:
icon["color"] = color
if size is not None:
icon["size"] = size
return icon
# ========== Legacy Compatibility Wrappers ==========
# These wrappers maintain API compatibility with existing code
def build_card(
card_type: str,
data: dict[str, Any],
*,
version: str = "1.0",
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Legacy wrapper - builds a card node."""
return {
"type": card_type,
"version": version,
"data": data,
"actions": actions or [],
}
def build_calendar_card(
title: str,
start_at: str,
*,
id: str | None = None,
end_at: str | None = None,
description: str | None = None,
timezone: str | None = None,
location: str | None = None,
color: str | None = None,
source_type: str | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Legacy wrapper for calendar card."""
data: dict[str, Any] = {
"title": title,
"startAt": start_at,
}
if id is not None:
data["id"] = id
if end_at is not None:
data["endAt"] = end_at
if description is not None:
data["description"] = description
if timezone is not None:
data["timezone"] = timezone
if location is not None:
data["location"] = location
if color is not None:
data["color"] = color
if source_type is not None:
data["sourceType"] = source_type
return build_card(
card_type="calendar_card.v1",
data=data,
actions=actions,
)
def build_calendar_list(
items: list[dict[str, Any]],
*,
page: int = 1,
page_size: int = 20,
total: int | None = None,
total_pages: int | None = None,
) -> dict[str, Any]:
"""Legacy wrapper for calendar list."""
pagination: dict[str, Any] = {
"page": page,
"pageSize": page_size,
}
if total is not None:
pagination["total"] = total
if total_pages is not None:
pagination["totalPages"] = total_pages
return build_card(
card_type="calendar_event_list.v1",
data={
"items": items,
"pagination": pagination,
},
)
def build_calendar_operation(
operation: str,
ok: bool,
message: str,
*,
code: str | None = None,
actions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Legacy wrapper for calendar operation."""
data: dict[str, Any] = {
"operation": operation,
"ok": ok,
"message": message,
}
if code is not None:
data["code"] = code
return build_card(
card_type="calendar_operation.v1",
data=data,
actions=actions,
)
def build_error_card(
message: str,
*,
code: str | None = None,
) -> dict[str, Any]:
"""Legacy wrapper for error card."""
data: dict[str, Any] = {"message": message}
if code is not None:
data["code"] = code
return build_card(
card_type="error_card.v1",
data=data,
)
def build_action_legacy(
label: str,
action_type: str = "primary",
*,
target: str | None = None,
action: str | None = None,
) -> dict[str, Any]:
"""Legacy wrapper for action."""
result: dict[str, Any] = {
"type": action_type,
"label": label,
}
if target is not None:
result["target"] = target
if action is not None:
result["action"] = action
return result
@@ -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,
+3 -3
View File
@@ -68,12 +68,12 @@ def _verified_access_token_for_user(
*,
authorization: str | None,
current_user: CurrentUser,
) -> str | None:
) -> str:
if not isinstance(authorization, str):
return None
raise HTTPException(status_code=401, detail="Unauthorized")
normalized = authorization.strip()
if not normalized:
return None
raise HTTPException(status_code=401, detail="Unauthorized")
if not normalized.lower().startswith("bearer "):
raise HTTPException(status_code=401, detail="Unauthorized")
token = normalized[7:].strip()
+1 -6
View File
@@ -15,12 +15,7 @@ class VerificationCreateRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=SUPABASE_PASSWORD_MIN_LENGTH)
redirect_to: str | None = None
invite_code: str | None = Field(
default=None,
min_length=8,
max_length=8,
pattern=r"^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{8}$",
)
invite_code: str | None = None
class VerificationResendRequest(BaseModel):
+20 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from typing import Protocol
from v1.auth.schemas import (
@@ -56,7 +57,11 @@ class AuthService:
async def create_verification(
self, request: VerificationCreateRequest
) -> VerificationCreateResponse:
return await self._gateway.create_verification(request)
normalized_invite_code = _normalize_invite_code(request.invite_code)
normalized_request = request.model_copy(
update={"invite_code": normalized_invite_code}
)
return await self._gateway.create_verification(normalized_request)
async def verify_verification(
self, request: VerificationVerifyRequest
@@ -82,3 +87,17 @@ class AuthService:
self, request: PasswordResetConfirmRequest
) -> None:
await self._gateway.confirm_password_reset(request)
_INVITE_CODE_PATTERN = re.compile(r"^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$")
def _normalize_invite_code(value: str | None) -> str | None:
if value is None:
return None
normalized = value.strip().upper()
if not normalized:
return None
return normalized if _INVITE_CODE_PATTERN.fullmatch(normalized) else None