feat: 添加日历批量操作与客户端时区感知功能,优化前端 UI 交互体验

This commit is contained in:
zl-q
2026-03-17 00:13:41 +08:00
parent d3783522e6
commit c26cdbbc27
27 changed files with 1532 additions and 412 deletions
@@ -0,0 +1,285 @@
# Calendar Timezone Unification Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Eliminate calendar time mismatches by enforcing one end-to-end timezone policy across App input, Agent runtime context, tool execution, and UTC database storage.
**Architecture:** Keep database schema unchanged (`start_at/end_at TIMESTAMPTZ + timezone`) and enforce strict runtime normalization. Device timezone is injected from `RunAgentInput.forwardedProps`, resolved into a single `effective_timezone`, then written explicitly into tool arguments and persisted as event timezone while timestamps are stored in UTC. Calendar read responses include deterministic event-timezone-rendered values so frontend rendering is stable and no implicit `toLocal()` conversion remains.
**Tech Stack:** FastAPI, Pydantic v2, AgentScope runtime/tooling, Flutter (Dart), PostgreSQL TIMESTAMPTZ, pytest, Flutter test.
---
## Chunk 1: Protocol and Backend Runtime Normalization
### Task 1: Freeze protocol and timezone precedence contract
**Files:**
- Modify: `docs/protocols/agent/run-agent-input.md`
- Create: `docs/protocols/calendar/timezone-policy.md`
- [ ] **Step 1: Write protocol delta checklist in docs first**
Document the exact policy:
- `event_timezone > device_timezone > profile.timezone > UTC`
- `event_timezone` must be present in final tool call
- `start_at/end_at` must be timezone-aware
- DB stores UTC timestamps and IANA timezone string
- [ ] **Step 2: Update RunAgentInput protocol with forwardedProps contract**
Add canonical payload example:
```json
{
"forwardedProps": {
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000
}
}
}
```
- [ ] **Step 3: Add calendar timezone policy protocol doc**
Include:
- accepted datetime formats
- explicit error codes
- write/read response semantics
- DST handling rule
- [ ] **Step 4: Verify docs consistency**
Run: `cd backend && uv run python -m pytest tests/unit/core/agentscope/test_system_prompt.py -q`
Expected: PASS (no protocol-breaking prompt assumptions)
### Task 2: Parse forwarded device time and compute effective timezone
**Files:**
- Modify: `backend/src/core/agentscope/schemas/agui_input.py`
- Modify: `backend/src/core/agentscope/runtime/runner.py`
- Modify: `backend/src/core/agentscope/prompts/system_prompt.py`
- Test: `backend/tests/unit/core/agentscope/test_system_prompt.py`
- [ ] **Step 1: Write failing tests for effective timezone resolution**
Add tests covering:
- forwarded `device_timezone` present -> selected
- missing forwarded timezone -> fallback profile timezone
- invalid forwarded timezone -> fallback profile timezone
- [ ] **Step 2: Run tests to confirm RED**
Run: `cd backend && uv run pytest tests/unit/core/agentscope/test_system_prompt.py -k timezone -v`
Expected: FAIL on new assertions
- [ ] **Step 3: Implement minimal runtime context extraction**
Implement a typed helper in runner path to read:
- `run_input.forwarded_props.client_time.device_timezone`
- `client_now_iso`
- `client_epoch_ms`
Compute `effective_timezone` using fixed precedence and pass it into `build_system_prompt(...)`.
- [ ] **Step 4: Inject effective_timezone into ENV section**
Update `build_system_prompt` env payload to include:
- `timezone_profile`
- `timezone_device`
- `timezone_effective`
Update guidance sentence to resolve ambiguous time with `timezone_effective`.
- [ ] **Step 5: Run tests to confirm GREEN**
Run: `cd backend && uv run pytest tests/unit/core/agentscope/test_system_prompt.py -v`
Expected: PASS
### Task 3: Remove timezone ambiguity and hidden fallbacks from calendar write
**Files:**
- Modify: `backend/src/core/agentscope/tools/utils/calendar_domain.py`
- Modify: `backend/src/core/agentscope/tools/custom/calendar.py`
- Modify: `backend/src/v1/schedule_items/schemas.py`
- Modify: `backend/src/v1/schedule_items/service.py`
- Test: `backend/tests/unit/core/agentscope/test_calendar_tools.py`
- Test: `backend/tests/unit/v1/schedule_items/test_schemas.py`
- Test: `backend/tests/unit/v1/schedule_items/test_service.py`
- [ ] **Step 1: Write failing tests for forbidden naive datetime and required timezone**
Add tests for:
- naive `start_at` rejected
- missing `event_timezone` rejected in tool path
- parse failure does not fallback to `now + 1h`
- [ ] **Step 2: Run tests to confirm RED**
Run: `cd backend && uv run pytest tests/unit/core/agentscope/test_calendar_tools.py tests/unit/v1/schedule_items/test_schemas.py -v`
Expected: FAIL on new constraints
- [ ] **Step 3: Implement strict parsing and normalization**
Implementation requirements:
- `parse_iso_datetime` rejects naive input
- remove default `Asia/Shanghai` in tool
- remove fallback auto-generated start time
- validate IANA timezone and normalize `start_at/end_at` to UTC before persistence
- [ ] **Step 4: Enforce service-level invariants**
Service invariant set:
- timezone non-empty and valid IANA
- `end_at is None or end_at >= start_at`
- [ ] **Step 5: Run backend tests**
Run: `cd backend && uv run pytest tests/unit/core/agentscope/test_calendar_tools.py tests/unit/v1/schedule_items/test_schemas.py tests/unit/v1/schedule_items/test_service.py tests/integration/test_schedule_items_routes.py -v`
Expected: PASS
### Task 4: Keep DB schema, add non-breaking constraint migration only
**Files:**
- Create: `backend/alembic/versions/20260316_000x_schedule_items_time_constraints.py`
- Test: `backend/tests/integration/test_schedule_items_routes.py`
- [ ] **Step 1: Write migration test expectation first**
Add/extend integration assertion for invalid `end_at < start_at` returning 422.
- [ ] **Step 2: Run integration test to confirm RED**
Run: `cd backend && uv run pytest tests/integration/test_schedule_items_routes.py -k end_at -v`
Expected: FAIL
- [ ] **Step 3: Implement migration with CHECK only (no new columns)**
Migration includes:
- `CHECK (end_at IS NULL OR end_at >= start_at)`
- [ ] **Step 4: Run migration + integration test**
Run: `cd backend && uv run alembic upgrade head && uv run pytest tests/integration/test_schedule_items_routes.py -v`
Expected: PASS
---
## Chunk 2: Frontend Deterministic Display and Agent Input Wiring
### Task 5: Wire device timezone into RunAgentInput forwardedProps
**Files:**
- Modify: `apps/lib/features/chat/data/services/ag_ui_service.dart`
- Modify: `apps/lib/features/chat/data/models/ag_ui_event.dart` (only if serialization helper is needed)
- Test: `apps/test/features/chat/ag_ui_event_test.dart`
- [ ] **Step 1: Write failing test for forwarded client_time payload**
Assert outgoing run request contains:
- `forwardedProps.client_time.device_timezone`
- `client_now_iso`
- `client_epoch_ms`
- [ ] **Step 2: Run test to confirm RED**
Run: `cd apps && flutter test test/features/chat/ag_ui_event_test.dart`
Expected: FAIL
- [ ] **Step 3: Implement payload injection in one place**
Add a single helper to build client time context and attach it to run input requests.
- [ ] **Step 4: Run test to confirm GREEN**
Run: `cd apps && flutter test test/features/chat/ag_ui_event_test.dart`
Expected: PASS
### Task 6: Remove implicit local-time rendering and render by event timezone
**Files:**
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_month_screen.dart`
- Modify: `apps/lib/features/messages/ui/widgets/calendar_message_card.dart`
- Modify: `apps/lib/features/calendar/ui/widgets/create_event_sheet.dart`
- Test: `apps/test/features/calendar/ui/calendar_time_utils_test.dart`
- Test: `apps/test/features/calendar/ui/create_event_sheet_time_align_test.dart`
- [ ] **Step 1: Write failing tests for timezone-specific rendering**
Cover cases:
- same UTC event shows different local clock time under different `event.timezone`
- list/day/week/month are consistent for one event
- create sheet sends explicit timezone in payload
- [ ] **Step 2: Run tests to confirm RED**
Run: `cd apps && flutter test test/features/calendar/ui/calendar_time_utils_test.dart test/features/calendar/ui/create_event_sheet_time_align_test.dart`
Expected: FAIL
- [ ] **Step 3: Implement deterministic time conversion utility**
Implement one utility used by all calendar UI surfaces:
- input: UTC datetime + IANA timezone
- output: event-local datetime
Replace direct `.toLocal()` usage in calendar model/view with this utility.
- [ ] **Step 4: Enforce explicit timezone on create/update payload**
Create/update must always include `timezone` field from selected event timezone.
- [ ] **Step 5: Run Flutter tests**
Run: `cd apps && flutter test test/features/calendar/ui/calendar_time_utils_test.dart test/features/calendar/ui/create_event_sheet_time_align_test.dart`
Expected: PASS
### Task 7: End-to-end verification matrix and release checklist
**Files:**
- Modify: `docs/plans/timezone-e2e-checklist.md`
- Test: `backend/tests/integration/test_schedule_items_routes.py`
- Test: `apps/test/features/calendar/ui/create_event_sheet_time_align_test.dart`
- [ ] **Step 1: Add reproducible matrix**
Matrix axes:
- device timezone: `America/Los_Angeles`, `Asia/Shanghai`
- profile timezone: `Asia/Shanghai`, `Europe/Paris`
- explicit event timezone: `Asia/Tokyo`
- [ ] **Step 2: Run backend + frontend verification commands**
Run:
- `cd backend && uv run pytest tests/unit/core/agentscope/test_system_prompt.py tests/unit/core/agentscope/test_calendar_tools.py tests/integration/test_schedule_items_routes.py -v`
- `cd apps && flutter test test/features/chat/ag_ui_event_test.dart test/features/calendar/ui/calendar_time_utils_test.dart test/features/calendar/ui/create_event_sheet_time_align_test.dart`
Expected: all PASS
- [ ] **Step 3: Manual scenario check**
Manual script:
1. device timezone set to Los Angeles
2. profile timezone set to Shanghai
3. ask agent create "明天上午9点开会"
4. verify assistant text, tool card, DB UTC value, and calendar detail all align to chosen event timezone semantics
- [ ] **Step 4: Capture release notes**
Record:
- removed hidden timezone defaults
- deterministic precedence
- no schema expansion
---
Plan complete and saved to `docs/superpowers/plans/2026-03-16-calendar-timezone-unification.md`. Ready to execute?
+75
View File
@@ -185,6 +185,44 @@ interface Context {
---
## forwardedProps.client_time Schema
`RunAgentInput.forwardedProps` 支持透传客户端时间上下文。日历相关能力必须使用以下结构:
```typescript
interface ForwardedProps {
client_time?: {
device_timezone: string; // IANA 时区,例如 "America/Los_Angeles"
client_now_iso: string; // RFC3339 带偏移时间,例如 "2026-03-16T09:12:33-07:00"
client_epoch_ms: number; // Unix epoch 毫秒
};
}
```
### 时间来源优先级(固定)
后端在运行时按以下顺序解析事件时区:
1. `event_timezone`(工具调用显式传参)
2. `forwardedProps.client_time.device_timezone`
3. `users.profile.settings.timezone`
4. `UTC`
### 约束
- `device_timezone` 必须是有效 IANA 时区。
- `client_now_iso` 必须是 RFC3339 且包含时区偏移。
- `client_epoch_ms` 必须是整数毫秒时间戳。
- 业务代码不得使用服务器本地时区作为事件语义时区。
### 说明
- `forwardedProps` 是透传字段,不改变 AG-UI 主体协议结构。
-`forwardedProps.client_time` 缺失或非法时,运行时回退到 `users.profile.settings.timezone`
- 日历写入必须在最终工具调用中带上 `event_timezone`,不得依赖工具默认值。
---
## Validation Rules
Backend 实现了以下验证规则:
@@ -203,6 +241,16 @@ Backend 实现了以下验证规则:
| binary 不允许使用 data | `binary content data is not allowed` |
| 单条消息最多 3 张附件 | `Too many attachments` |
### forwardedProps.client_time Validation
建议在后端校验层返回以下错误(按业务实现映射到 4xx):
| Rule | Error Message |
|------|---------------|
| `device_timezone` 非 IANA 时区 | `invalid client_time.device_timezone` |
| `client_now_iso` 无法解析或缺少时区 | `invalid client_time.client_now_iso` |
| `client_epoch_ms` 非整数毫秒值 | `invalid client_time.client_epoch_ms` |
---
## Request Example
@@ -292,6 +340,32 @@ Backend 实现了以下验证规则:
}
```
### 带 forwardedProps.client_time 的请求
```json
{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-004",
"state": {},
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "帮我明天早上9点创建一个日历"
}
],
"tools": [],
"context": [],
"forwardedProps": {
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000
}
}
}
```
---
## Response
@@ -454,3 +528,4 @@ interface UiSchemaRenderer {
- backend 验证通过后,会将 binary url 转换为内部存储路径
- `tools` 是前端工具通道字段;当前后端运行时不基于该字段构造后端工具 prompt
- `RunAgentInput` 同时接受 camelCase 与 snake_case 别名输入(推荐统一使用 camelCase)
- 日历能力依赖 `forwardedProps.client_time` 透传设备时间上下文;缺失时回退用户 profile 时区
@@ -0,0 +1,87 @@
# Calendar Timezone Policy Protocol
## Version
- Current: `1.0`
- Status: Active
---
## Goal
统一日历事件在 App、Agent、工具、数据库之间的时间语义,消除时区不一致导致的显示和落库偏差。
---
## Canonical Rules
1. 数据库存储基准为 UTC。
2. 事件语义时区使用 IANA 时区字符串(`event_timezone` / `timezone`)。
3. 禁止无时区时间(naive datetime)进入日历写入链路。
4. 日历写入必须显式确定事件时区,不允许工具层硬编码默认时区。
---
## Timezone Resolution Priority
运行时事件时区解析顺序固定如下:
1. `event_timezone`(工具调用显式传参)
2. `forwardedProps.client_time.device_timezone`
3. `users.profile.settings.timezone`
4. `UTC`
---
## Write Contract
### Required fields
- `start_at`: RFC3339 且必须包含时区偏移
- `timezone`: IANA 时区
### Optional fields
- `end_at`: RFC3339 且必须包含时区偏移(若提供)
### Validation
- `timezone` 非法 -> 拒绝请求
- `start_at`/`end_at` 无时区 -> 拒绝请求
- `end_at < start_at` -> 拒绝请求
---
## Read Contract
读接口最小语义要求:
- 返回 UTC 时间字段(`start_at`, `end_at`
- 返回事件时区字段(`timezone`
- 前端展示必须以 `timezone` 作为事件本地时间转换基准,不允许直接按设备本地时区隐式渲染
---
## Error Codes
推荐错误码(由后端映射为 4xx):
- `INVALID_DATETIME_FORMAT`
- `NAIVE_DATETIME_FORBIDDEN`
- `INVALID_TIMEZONE`
- `TIMEZONE_REQUIRED`
- `INVALID_TIME_RANGE`
---
## DST Rule
- 夏令时切换期间,时间解释以 IANA 时区数据库为准。
- 对于歧义本地时间(例如回拨重复小时),由后端统一按标准库解析策略处理并返回确定 UTC 结果。
---
## Non-Goals
- 本协议不引入新的数据库时间列。
- 本协议不改变 `schedule_items` 现有 `TIMESTAMPTZ + timezone` 存储结构。