feat: 添加日历批量操作与客户端时区感知功能,优化前端 UI 交互体验
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated, Any, Literal, cast
|
||||
from uuid import UUID
|
||||
|
||||
@@ -22,7 +21,8 @@ from core.agentscope.tools.utils.calendar_ui import (
|
||||
calendar_write_hints,
|
||||
dump_tool_output,
|
||||
)
|
||||
from schemas.agent.runtime_models import ToolAgentOutput, ToolStatus
|
||||
from schemas.agent.runtime_models import ErrorInfo, ToolAgentOutput, ToolStatus
|
||||
from schemas.agent.ui_hints import UiHintListItem, UiHintStatus
|
||||
from v1.schedule_items.schemas import (
|
||||
ScheduleItemCreateRequest,
|
||||
ScheduleItemShareRequest,
|
||||
@@ -146,85 +146,126 @@ async def calendar_read(
|
||||
|
||||
|
||||
async def calendar_write(
|
||||
operation: Annotated[
|
||||
Literal["create", "update", "delete"],
|
||||
Field(description="Write operation: create, update, or delete."),
|
||||
],
|
||||
event_id: Annotated[
|
||||
str | None,
|
||||
Field(description="Required event ID for update/delete operations."),
|
||||
] = None,
|
||||
title: Annotated[
|
||||
str | None,
|
||||
Field(description="Event title.", max_length=255),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
str | None,
|
||||
Field(description="Event description.", max_length=2000),
|
||||
] = None,
|
||||
start_at: Annotated[
|
||||
str | None,
|
||||
Field(description="Event start time in ISO 8601 format."),
|
||||
] = None,
|
||||
end_at: Annotated[
|
||||
str | None,
|
||||
Field(description="Event end time in ISO 8601 format."),
|
||||
] = None,
|
||||
event_timezone: Annotated[
|
||||
str | None,
|
||||
Field(description="IANA timezone name for the event.", max_length=50),
|
||||
] = None,
|
||||
location: Annotated[str | None, Field(description="Event location.")] = None,
|
||||
color: Annotated[
|
||||
str | None,
|
||||
Field(description="Event color value, for example #4F46E5."),
|
||||
] = None,
|
||||
reminder_minutes: Annotated[
|
||||
int | None,
|
||||
operations: Annotated[
|
||||
list[Literal["create", "update", "delete"]],
|
||||
Field(
|
||||
description="Minutes before start time to trigger reminder (0-10080).",
|
||||
ge=0,
|
||||
le=10080,
|
||||
description=(
|
||||
"Batch operations list. Each item must be create, update, or delete."
|
||||
),
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
),
|
||||
],
|
||||
event_ids: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Optional event id list aligned with operations. "
|
||||
"Required for update/delete item."
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
status: Annotated[
|
||||
Literal["active", "completed", "canceled", "archived"] | None,
|
||||
Field(description="Event status: active, completed, canceled, or archived."),
|
||||
titles: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(description="Optional title list aligned with operations."),
|
||||
] = None,
|
||||
descriptions: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(description="Optional description list aligned with operations."),
|
||||
] = None,
|
||||
start_ats: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Optional start time list aligned with operations, ISO 8601 with timezone."
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
end_ats: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Optional end time list aligned with operations, ISO 8601 with timezone."
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
event_timezones: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Optional event timezone list aligned with operations, IANA timezone."
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
locations: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(description="Optional location list aligned with operations."),
|
||||
] = None,
|
||||
colors: Annotated[
|
||||
list[str | None] | None,
|
||||
Field(description="Optional color list aligned with operations."),
|
||||
] = None,
|
||||
reminder_minutes_list: Annotated[
|
||||
list[int | None] | None,
|
||||
Field(
|
||||
description=(
|
||||
"Optional reminder minutes list aligned with operations, value range 0-10080."
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
statuses: Annotated[
|
||||
list[Literal["active", "completed", "canceled", "archived"] | None] | None,
|
||||
Field(description="Optional status list aligned with operations."),
|
||||
] = None,
|
||||
session: Any = None,
|
||||
owner_id: Any = None,
|
||||
) -> ToolResponse:
|
||||
"""Create, update, or delete a calendar event.
|
||||
"""Batch create/update/delete calendar events using aligned list parameters.
|
||||
|
||||
Args:
|
||||
operation: Write operation type, one of create, update, delete.
|
||||
event_id: Target event id for update and delete operations.
|
||||
title: Event title.
|
||||
description: Event description.
|
||||
start_at: Event start time in ISO 8601 format.
|
||||
end_at: Event end time in ISO 8601 format.
|
||||
event_timezone: IANA timezone string.
|
||||
location: Event location.
|
||||
color: Event color in hex format, for example #4F46E5.
|
||||
reminder_minutes: Reminder lead time in minutes.
|
||||
status: Event status value.
|
||||
operations: Operation list. Length defines batch size.
|
||||
event_ids: Optional event id list aligned with operations.
|
||||
titles: Optional title list aligned with operations.
|
||||
descriptions: Optional description list aligned with operations.
|
||||
start_ats: Optional start time list aligned with operations.
|
||||
end_ats: Optional end time list aligned with operations.
|
||||
event_timezones: Optional event timezone list aligned with operations.
|
||||
locations: Optional location list aligned with operations.
|
||||
colors: Optional color list aligned with operations.
|
||||
reminder_minutes_list: Optional reminder minute list aligned with operations.
|
||||
statuses: Optional status list aligned with operations.
|
||||
|
||||
Constraints:
|
||||
- All provided list parameters must have the same length as operations.
|
||||
- create item requires start_ats[i] and event_timezones[i].
|
||||
- update/delete item requires event_ids[i].
|
||||
- start/end datetime must include timezone offset.
|
||||
|
||||
Returns:
|
||||
ToolResponse with serialized ToolAgentOutput payload.
|
||||
"""
|
||||
tool_name = "calendar_write"
|
||||
|
||||
def _align_list(name: str, values: list[Any] | None, size: int) -> list[Any | None]:
|
||||
if values is None:
|
||||
return [None] * size
|
||||
if len(values) != size:
|
||||
raise ValueError(f"{name} 长度必须与 operations 一致")
|
||||
return list(values)
|
||||
|
||||
batch_size = len(operations)
|
||||
tool_call_args = {
|
||||
"operation": operation,
|
||||
"event_id": event_id,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"start_at": start_at,
|
||||
"end_at": end_at,
|
||||
"event_timezone": event_timezone,
|
||||
"location": location,
|
||||
"color": color,
|
||||
"reminder_minutes": reminder_minutes,
|
||||
"status": status,
|
||||
"operations": operations,
|
||||
"event_ids": event_ids,
|
||||
"titles": titles,
|
||||
"descriptions": descriptions,
|
||||
"start_ats": start_ats,
|
||||
"end_ats": end_ats,
|
||||
"event_timezones": event_timezones,
|
||||
"locations": locations,
|
||||
"colors": colors,
|
||||
"reminder_minutes_list": reminder_minutes_list,
|
||||
"statuses": statuses,
|
||||
}
|
||||
runtime_error = _validate_runtime_context(
|
||||
tool_name=tool_name,
|
||||
@@ -239,143 +280,235 @@ async def calendar_write(
|
||||
service = create_schedule_service(
|
||||
cast(AsyncSession, session), cast(UUID, owner_id)
|
||||
)
|
||||
aligned_event_ids = _align_list("event_ids", event_ids, batch_size)
|
||||
aligned_titles = _align_list("titles", titles, batch_size)
|
||||
aligned_descriptions = _align_list("descriptions", descriptions, batch_size)
|
||||
aligned_start_ats = _align_list("start_ats", start_ats, batch_size)
|
||||
aligned_end_ats = _align_list("end_ats", end_ats, batch_size)
|
||||
aligned_event_timezones = _align_list(
|
||||
"event_timezones", event_timezones, batch_size
|
||||
)
|
||||
aligned_locations = _align_list("locations", locations, batch_size)
|
||||
aligned_colors = _align_list("colors", colors, batch_size)
|
||||
aligned_reminders = _align_list(
|
||||
"reminder_minutes_list", reminder_minutes_list, batch_size
|
||||
)
|
||||
aligned_statuses = _align_list("statuses", statuses, batch_size)
|
||||
|
||||
if operation == "create":
|
||||
parsed_start = parse_iso_datetime(start_at) if start_at else None
|
||||
if parsed_start is None:
|
||||
parsed_start = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
parsed_end = parse_iso_datetime(end_at) if end_at else None
|
||||
tz = (
|
||||
event_timezone.strip()
|
||||
if event_timezone and event_timezone.strip()
|
||||
else "Asia/Shanghai"
|
||||
)
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
result_items: list[dict[str, Any]] = []
|
||||
|
||||
created = await service.create_agent_generated(
|
||||
ScheduleItemCreateRequest(
|
||||
title=title.strip() if title and title.strip() else "新的日程",
|
||||
description=description.strip()
|
||||
if description and description.strip()
|
||||
else None,
|
||||
start_at=parsed_start,
|
||||
end_at=parsed_end,
|
||||
timezone=tz,
|
||||
metadata=build_schedule_metadata(location, color, reminder_minutes),
|
||||
)
|
||||
)
|
||||
event_dict = schedule_event_to_dict(created)
|
||||
summary = f"日程「{created.title}」已创建"
|
||||
return dump_tool_output(
|
||||
ToolAgentOutput(
|
||||
tool_name=tool_name,
|
||||
tool_call_id=f"{tool_name}-call",
|
||||
tool_call_args=tool_call_args,
|
||||
status=ToolStatus.SUCCESS,
|
||||
result_summary=summary,
|
||||
ui_hints=calendar_write_hints(
|
||||
operation="create",
|
||||
message=summary,
|
||||
event=event_dict,
|
||||
event_id=event_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
for idx, operation in enumerate(operations):
|
||||
event_id = aligned_event_ids[idx]
|
||||
title = aligned_titles[idx]
|
||||
description = aligned_descriptions[idx]
|
||||
start_at = aligned_start_ats[idx]
|
||||
end_at = aligned_end_ats[idx]
|
||||
event_timezone = aligned_event_timezones[idx]
|
||||
location = aligned_locations[idx]
|
||||
color = aligned_colors[idx]
|
||||
reminder_minutes = aligned_reminders[idx]
|
||||
status = aligned_statuses[idx]
|
||||
|
||||
if operation == "update":
|
||||
if not event_id:
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="更新日程需要提供 event_id",
|
||||
retryable=False,
|
||||
)
|
||||
parsed_event_id = UUID(event_id)
|
||||
update_data: dict[str, Any] = {}
|
||||
if title:
|
||||
update_data["title"] = title.strip()
|
||||
if description:
|
||||
update_data["description"] = description.strip()
|
||||
if start_at:
|
||||
update_data["start_at"] = parse_iso_datetime(start_at)
|
||||
if end_at:
|
||||
update_data["end_at"] = parse_iso_datetime(end_at)
|
||||
if event_timezone:
|
||||
update_data["timezone"] = event_timezone.strip()
|
||||
if status:
|
||||
try:
|
||||
update_data["status"] = ScheduleItemStatus(status)
|
||||
except ValueError:
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="status 必须是 active, completed, canceled, archived 之一",
|
||||
retryable=False,
|
||||
try:
|
||||
if operation == "create":
|
||||
if start_at is None or not start_at.strip():
|
||||
raise ValueError(
|
||||
"创建日程需要提供 start_at,且必须包含时区偏移"
|
||||
)
|
||||
if event_timezone is None or not event_timezone.strip():
|
||||
raise ValueError("创建日程需要提供 event_timezone")
|
||||
parsed_start = parse_iso_datetime(start_at)
|
||||
if parsed_start is None:
|
||||
raise ValueError(
|
||||
"创建日程需要提供 start_at,且必须包含时区偏移"
|
||||
)
|
||||
parsed_end = parse_iso_datetime(end_at) if end_at else None
|
||||
created = await service.create_agent_generated(
|
||||
ScheduleItemCreateRequest(
|
||||
title=title.strip()
|
||||
if title and title.strip()
|
||||
else "新的日程",
|
||||
description=description.strip()
|
||||
if description and description.strip()
|
||||
else None,
|
||||
start_at=parsed_start,
|
||||
end_at=parsed_end,
|
||||
timezone=event_timezone.strip(),
|
||||
metadata=build_schedule_metadata(
|
||||
location,
|
||||
color,
|
||||
cast(int | None, reminder_minutes),
|
||||
),
|
||||
)
|
||||
)
|
||||
if location or color or reminder_minutes is not None:
|
||||
existing = await service.get_by_id(parsed_event_id)
|
||||
update_data["metadata"] = merge_schedule_metadata_for_update(
|
||||
existing_metadata=existing.metadata,
|
||||
location=location,
|
||||
color=color,
|
||||
reminder_minutes=reminder_minutes,
|
||||
success_count += 1
|
||||
result_items.append(
|
||||
{
|
||||
"index": idx,
|
||||
"operation": operation,
|
||||
"status": "success",
|
||||
"eventId": str(created.id),
|
||||
"message": f"日程「{created.title}」已创建",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if operation == "update":
|
||||
if event_id is None or not event_id.strip():
|
||||
raise ValueError("更新日程需要提供 event_id")
|
||||
parsed_event_id = UUID(event_id)
|
||||
update_data: dict[str, Any] = {}
|
||||
if title is not None:
|
||||
update_data["title"] = title.strip()
|
||||
if description is not None:
|
||||
update_data["description"] = description.strip()
|
||||
if start_at:
|
||||
update_data["start_at"] = parse_iso_datetime(start_at)
|
||||
if end_at:
|
||||
update_data["end_at"] = parse_iso_datetime(end_at)
|
||||
if event_timezone is not None:
|
||||
timezone_value = event_timezone.strip()
|
||||
if not timezone_value:
|
||||
raise ValueError("event_timezone 不能为空")
|
||||
update_data["timezone"] = timezone_value
|
||||
if status:
|
||||
update_data["status"] = ScheduleItemStatus(status)
|
||||
if location or color or reminder_minutes is not None:
|
||||
existing = await service.get_by_id(parsed_event_id)
|
||||
update_data["metadata"] = merge_schedule_metadata_for_update(
|
||||
existing_metadata=existing.metadata,
|
||||
location=cast(str | None, location),
|
||||
color=cast(str | None, color),
|
||||
reminder_minutes=cast(int | None, reminder_minutes),
|
||||
)
|
||||
updated = await service.update(
|
||||
parsed_event_id,
|
||||
ScheduleItemUpdateRequest.model_validate(update_data),
|
||||
)
|
||||
success_count += 1
|
||||
result_items.append(
|
||||
{
|
||||
"index": idx,
|
||||
"operation": operation,
|
||||
"status": "success",
|
||||
"eventId": str(updated.id),
|
||||
"message": f"日程「{updated.title}」已更新",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if operation == "delete":
|
||||
if event_id is None or not event_id.strip():
|
||||
raise ValueError("删除日程需要提供 event_id")
|
||||
await service.delete(UUID(event_id))
|
||||
success_count += 1
|
||||
result_items.append(
|
||||
{
|
||||
"index": idx,
|
||||
"operation": operation,
|
||||
"status": "success",
|
||||
"eventId": event_id,
|
||||
"message": f"日程 {event_id} 已删除",
|
||||
}
|
||||
)
|
||||
continue
|
||||
except Exception as exc:
|
||||
code, message, _ = map_calendar_exception(exc)
|
||||
failed_count += 1
|
||||
result_items.append(
|
||||
{
|
||||
"index": idx,
|
||||
"operation": operation,
|
||||
"status": "failure",
|
||||
"eventId": event_id,
|
||||
"code": code,
|
||||
"message": message,
|
||||
}
|
||||
)
|
||||
|
||||
updated = await service.update(
|
||||
parsed_event_id, ScheduleItemUpdateRequest.model_validate(update_data)
|
||||
if failed_count == 0:
|
||||
final_status = ToolStatus.SUCCESS
|
||||
ui_status = UiHintStatus.SUCCESS
|
||||
summary = f"日程批量操作完成,共 {batch_size} 条,成功 {success_count} 条"
|
||||
elif success_count == 0:
|
||||
final_status = ToolStatus.FAILURE
|
||||
ui_status = UiHintStatus.ERROR
|
||||
summary = f"日程批量操作失败,共 {batch_size} 条,失败 {failed_count} 条"
|
||||
else:
|
||||
final_status = ToolStatus.PARTIAL
|
||||
ui_status = UiHintStatus.WARNING
|
||||
summary = f"日程批量操作部分成功,共 {batch_size} 条,成功 {success_count} 条,失败 {failed_count} 条"
|
||||
|
||||
error_info: ErrorInfo | None = None
|
||||
if final_status == ToolStatus.FAILURE:
|
||||
first_failure = next(
|
||||
(
|
||||
item
|
||||
for item in result_items
|
||||
if isinstance(item, dict) and item.get("status") == "failure"
|
||||
),
|
||||
None,
|
||||
)
|
||||
event_dict = schedule_event_to_dict(updated)
|
||||
summary = f"日程「{updated.title}」已更新"
|
||||
return dump_tool_output(
|
||||
ToolAgentOutput(
|
||||
tool_name=tool_name,
|
||||
tool_call_id=f"{tool_name}-call",
|
||||
tool_call_args=tool_call_args,
|
||||
status=ToolStatus.SUCCESS,
|
||||
result_summary=summary,
|
||||
ui_hints=calendar_write_hints(
|
||||
operation="update",
|
||||
message=summary,
|
||||
event=event_dict,
|
||||
event_id=event_id,
|
||||
),
|
||||
)
|
||||
error_info = ErrorInfo(
|
||||
code=str(
|
||||
first_failure.get("code") if first_failure else "BATCH_FAILED"
|
||||
),
|
||||
message=str(first_failure.get("message") if first_failure else summary),
|
||||
retryable=False,
|
||||
details={"results": result_items},
|
||||
)
|
||||
|
||||
if operation == "delete":
|
||||
if not event_id:
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="删除日程需要提供 event_id",
|
||||
retryable=False,
|
||||
)
|
||||
await service.delete(UUID(event_id))
|
||||
summary = f"日程 {event_id} 已删除"
|
||||
return dump_tool_output(
|
||||
ToolAgentOutput(
|
||||
tool_name=tool_name,
|
||||
tool_call_id=f"{tool_name}-call",
|
||||
tool_call_args=tool_call_args,
|
||||
status=ToolStatus.SUCCESS,
|
||||
result_summary=summary,
|
||||
ui_hints=calendar_write_hints(
|
||||
operation="delete",
|
||||
message=summary,
|
||||
event=None,
|
||||
event_id=event_id,
|
||||
),
|
||||
)
|
||||
result_list_items = [
|
||||
UiHintListItem(
|
||||
id=(
|
||||
str(item.get("eventId"))
|
||||
if isinstance(item, dict) and item.get("eventId") is not None
|
||||
else None
|
||||
),
|
||||
title=(
|
||||
f"#{int(item.get('index', 0)) + 1} {str(item.get('operation', 'unknown'))}"
|
||||
if isinstance(item, dict)
|
||||
else "unknown"
|
||||
),
|
||||
subtitle=(
|
||||
"成功"
|
||||
if isinstance(item, dict) and item.get("status") == "success"
|
||||
else "失败"
|
||||
),
|
||||
description=(
|
||||
str(item.get("message") or "") if isinstance(item, dict) else ""
|
||||
),
|
||||
)
|
||||
for item in result_items
|
||||
]
|
||||
|
||||
return calendar_error_output(
|
||||
tool_name=tool_name,
|
||||
tool_call_args=tool_call_args,
|
||||
code="INVALID_ARGUMENT",
|
||||
message="无效的操作类型",
|
||||
retryable=False,
|
||||
return dump_tool_output(
|
||||
ToolAgentOutput(
|
||||
tool_name=tool_name,
|
||||
tool_call_id=f"{tool_name}-call",
|
||||
tool_call_args=tool_call_args,
|
||||
status=final_status,
|
||||
result_summary=summary,
|
||||
error=error_info,
|
||||
ui_hints=calendar_write_hints(
|
||||
operation="batch",
|
||||
message=summary,
|
||||
event=None,
|
||||
event_id=None,
|
||||
status=ui_status,
|
||||
).model_copy(
|
||||
update={
|
||||
"list_items": result_list_items,
|
||||
"meta": {
|
||||
"total": batch_size,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
|
||||
@@ -118,11 +118,11 @@ def parse_iso_datetime(value: str | None) -> datetime | None:
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
except ValueError:
|
||||
return None
|
||||
except ValueError as exc:
|
||||
raise ValueError("时间格式必须是 ISO8601 且包含时区偏移") from exc
|
||||
if parsed.tzinfo is None:
|
||||
raise ValueError("时间必须包含时区信息")
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def resolve_share_target_email_map(invitee_user_ids: list[str]) -> dict[str, str]:
|
||||
|
||||
@@ -79,7 +79,7 @@ def calendar_read_hints(
|
||||
UiHintKvItem(key="page_size", label="每页", value=page_size),
|
||||
UiHintKvItem(key="total_pages", label="总页数", value=total_pages),
|
||||
],
|
||||
list_items=event_items,
|
||||
listItems=event_items,
|
||||
actions=[
|
||||
UiHintAction(
|
||||
label="打开日历",
|
||||
@@ -97,6 +97,7 @@ def calendar_write_hints(
|
||||
message: str,
|
||||
event: dict[str, Any] | None,
|
||||
event_id: str | None,
|
||||
status: UiHintStatus = UiHintStatus.SUCCESS,
|
||||
) -> UiHintsPayload:
|
||||
kv_items: list[UiHintKvItem] = []
|
||||
|
||||
@@ -126,10 +127,10 @@ def calendar_write_hints(
|
||||
|
||||
return UiHintsPayload(
|
||||
intent=UiHintIntent.STATUS,
|
||||
status=UiHintStatus.SUCCESS,
|
||||
status=status,
|
||||
title="日历操作完成",
|
||||
body=message,
|
||||
items=kv_items if kv_items else None,
|
||||
items=kv_items,
|
||||
actions=[
|
||||
UiHintAction(
|
||||
label="查看日历",
|
||||
@@ -159,7 +160,5 @@ def calendar_share_hints(
|
||||
UiHintKvItem(key="event_id", label="日程ID", value=event_id, copyable=True),
|
||||
UiHintKvItem(key="permission", label="权限", value=permission_text),
|
||||
],
|
||||
list_items=[UiHintListItem(title=email) for email in invited]
|
||||
if invited
|
||||
else [],
|
||||
listItems=[UiHintListItem(title=email) for email in invited] if invited else [],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user