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