feat: 实现日历提醒完整功能(操作执行、通知服务重构、归档)
- 新增 ReminderActionExecutor 处理取消/稍后提醒操作 - 新增 ReminderOutboxStore 本地存储待处理操作 - 重构 LocalNotificationService 支持聚合提醒和交互操作 - 新增 event_color_resolver 工具类统一颜色解析 - 新增 CalendarService.archiveEvent 归档方法 - 增强 ModelTracking 支持缓存命中、推理token和成本追踪 - 添加 qwen3.5-35b-a3b 模型配置 - 更新 AndroidManifest 全屏intent权限 - 补充相关单元测试和文档
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
# Reminder Alert Archival Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Deliver alarm-style reminder popups with cancel/snooze actions, 30s timeout auto-snooze, overlap handling, and archived/gray lifecycle consistency across Android and iOS.
|
||||
|
||||
**Architecture:** Keep scheduling local on device (Flutter local notifications), persist user reminder actions with an app-side outbox for eventual backend sync, and use backend PATCH update for archive status as the source of truth. Add a backend safety net job to auto-archive expired active events so app-terminated scenarios still converge. Implement shared reminder payload and action handler with platform-specific notification configuration (Android full-screen intent, iOS category actions).
|
||||
|
||||
**Tech Stack:** Flutter (`flutter_local_notifications`, existing calendar service), FastAPI schedule-items API, SQLAlchemy service layer, uv/pytest, Dart tests.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update protocol and state semantics first
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/protocols/` (create a new reminder interaction protocol doc or extend existing schedule protocol doc)
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
|
||||
**Step 1: Write failing doc checks (manual checklist as fail-first gate)**
|
||||
|
||||
```text
|
||||
Checklist fails until all are documented:
|
||||
1) cancel action semantics
|
||||
2) snooze +10 minutes semantics
|
||||
3) timeout(30s) = ignore -> snooze
|
||||
4) overlap aggregation semantics
|
||||
5) archive + gray render semantics
|
||||
6) iOS degraded behavior note
|
||||
```
|
||||
|
||||
**Step 2: Run verification of checklist**
|
||||
|
||||
Run: manual review (expect FAIL before edits)
|
||||
|
||||
**Step 3: Write minimal protocol spec**
|
||||
|
||||
Include exact payload keys and action enum:
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"title": "string",
|
||||
"startAt": "iso8601",
|
||||
"endAt": "iso8601|null",
|
||||
"timezone": "IANA",
|
||||
"location": "string|null",
|
||||
"notes": "string|null",
|
||||
"color": "#RRGGBB|null",
|
||||
"mode": "single|aggregate",
|
||||
"aggregateIds": ["uuid"]
|
||||
}
|
||||
```
|
||||
|
||||
Actions:
|
||||
- `cancel`: archive target events and stop reminders
|
||||
- `snooze_10m`: reschedule +10m, stop when `now >= endAt`
|
||||
- `timeout_30s`: same as `snooze_10m`
|
||||
|
||||
**Step 4: Verify checklist passes**
|
||||
|
||||
Run: manual review (expect PASS)
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/protocols docs/runtime/runtime-route.md
|
||||
git commit -m "docs: define reminder interaction protocol and lifecycle semantics"
|
||||
```
|
||||
|
||||
### Task 2: Add frontend reminder action models and payload codec
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/models/reminder_payload.dart`
|
||||
- Create: `apps/lib/features/calendar/reminders/models/reminder_action.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('round-trips payload with single and aggregate modes', () {
|
||||
final payload = ReminderPayload(...);
|
||||
expect(ReminderPayload.fromJson(payload.toJson()), payload);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
Expected: FAIL (type/file missing)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement immutable model + json codec + enum parser.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/models apps/test/features/calendar/reminders/models/reminder_payload_test.dart
|
||||
git commit -m "feat: add reminder payload and action models"
|
||||
```
|
||||
|
||||
### Task 3: Refactor local notification service for action-capable reminders (Android + iOS)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
- Modify: `apps/lib/main.dart`
|
||||
- Modify: `apps/ios/Runner/AppDelegate.swift`
|
||||
- Test: `apps/test/core/notifications/local_notification_service_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('uses alarm-style Android details with actions and timeout', () async {});
|
||||
test('uses Darwin category actions for cancel/snooze', () async {});
|
||||
test('encodes payload in notification details', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests to verify failure**
|
||||
|
||||
Run: `flutter test test/core/notifications/local_notification_service_test.dart`
|
||||
Expected: FAIL
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement:
|
||||
- Android notification actions: `cancel`, `snooze_10m`
|
||||
- `timeoutAfter: 30000`
|
||||
- payload serialization
|
||||
- iOS `DarwinNotificationCategory` + action identifiers
|
||||
- initialize callback registration for action responses
|
||||
|
||||
Also keep existing full-screen alarm setup and exact alarm fallback behavior.
|
||||
|
||||
**Step 4: Run tests to verify pass**
|
||||
|
||||
Run: `flutter test test/core/notifications/local_notification_service_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/notifications/local_notification_service.dart apps/lib/main.dart apps/ios/Runner/AppDelegate.swift apps/test/core/notifications/local_notification_service_test.dart
|
||||
git commit -m "feat: support actionable reminder notifications on android and ios"
|
||||
```
|
||||
|
||||
### Task 4: Implement reminder action executor + local outbox for eventual consistency
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_outbox_store.dart`
|
||||
- Modify: `apps/lib/features/calendar/data/services/calendar_service.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('cancel archives remotely and cancels local reminders', () async {});
|
||||
test('network failure writes outbox item and keeps local state updated', () async {});
|
||||
test('snooze reschedules +10m and stops after endAt', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Rules:
|
||||
- Cancel: local cancel now, enqueue archive API job, best-effort immediate PATCH
|
||||
- Snooze: schedule at `now + 10m`, if `next >= endAt` then archive path
|
||||
- Timeout action uses same path as snooze
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders apps/lib/features/calendar/data/services/calendar_service.dart apps/test/features/calendar/reminders/reminder_action_executor_test.dart
|
||||
git commit -m "feat: add reminder action executor with offline outbox"
|
||||
```
|
||||
|
||||
### Task 5: Add startup reconciliation (replay outbox + rebuild reminders)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/startup/auth_session_bootstrapper.dart`
|
||||
- Modify: `apps/lib/main.dart`
|
||||
- Test: `apps/test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('replays pending reminder actions after login', () async {});
|
||||
test('rebuilds reminders after outbox replay', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
In authenticated startup flow:
|
||||
1) replay outbox
|
||||
2) fetch events with overlap semantics (active and not ended)
|
||||
3) rebuild active reminders with compensation scheduling:
|
||||
- `now < remindAt`: schedule at remindAt
|
||||
- `remindAt <= now < endAt`: schedule immediate compensation reminder (e.g. +5s)
|
||||
- `now >= endAt`: archive path
|
||||
4) enforce reminder dedupe key to avoid duplicate reminders after reinstall/restart
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/startup/auth_session_bootstrapper.dart apps/lib/main.dart apps/test/core/startup/auth_session_bootstrapper_test.dart
|
||||
git commit -m "feat: replay reminder outbox on startup"
|
||||
```
|
||||
|
||||
### Task 6: Implement overlap strategy (aggregate popup)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_overlap_policy.dart`
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('groups reminders whose fire time falls into same minute bucket', () {});
|
||||
test('creates aggregate payload with top-3 preview and ids', () {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Policy:
|
||||
- same minute bucket => one aggregate popup
|
||||
- actions apply to all members by default
|
||||
- payload includes aggregateIds
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/reminder_overlap_policy.dart apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart
|
||||
git commit -m "feat: add overlap aggregation policy for reminders"
|
||||
```
|
||||
|
||||
### Task 7: Render archived events as gray in calendar UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/ui/**` (event color resolution points)
|
||||
- Test: `apps/test/features/calendar/ui/*archived*test.dart` (new if missing)
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
testWidgets('archived events use gray token color', (tester) async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/ui`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Rule:
|
||||
- if `status == archived`, force token-based gray (do not mutate persisted `metadata.color`)
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/ui`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/ui apps/test/features/calendar/ui
|
||||
git commit -m "feat: render archived calendar events in gray"
|
||||
```
|
||||
|
||||
### Task 8: Backend reuse route + add expired-event auto-archive safety job
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/service.py` (if needed for stricter status transition)
|
||||
- Modify: `backend/src/v1/schedule_items/repository.py` (range query to overlap query)
|
||||
- Create: `backend/src/jobs/schedule_item_archive_job.py` (or existing worker module path)
|
||||
- Modify: worker scheduler registration file under `backend/src/core/celery/` (actual existing path)
|
||||
- Test: `backend/tests/unit/v1/schedule_items/test_service.py`
|
||||
- Test: `backend/tests/unit/v1/schedule_items/test_repository.py`
|
||||
- Test: `backend/tests/unit/jobs/test_schedule_item_archive_job.py`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
def test_patch_status_archived_allowed_for_owner() -> None: ...
|
||||
def test_list_by_overlap_includes_started_but_not_ended_items() -> None: ...
|
||||
def test_archive_job_marks_expired_active_items_archived() -> None: ...
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement/verify:
|
||||
- route reuse: PATCH status archived works as-is for authorized user
|
||||
- overlap query for bootstrap: `start_at <= window_end AND (end_at IS NULL OR end_at >= window_start)` and `status=active`
|
||||
- periodic archive job: `end_at < now and status=active -> archived`
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src backend/tests
|
||||
git commit -m "feat: add expired schedule auto-archive safety job"
|
||||
```
|
||||
|
||||
### Task 9: End-to-end verification and release notes
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-runbook.md`
|
||||
- Modify: `docs/protocols/` reminder doc from Task 1
|
||||
|
||||
**Step 1: Run frontend verification**
|
||||
|
||||
Run:
|
||||
- `flutter analyze`
|
||||
- `flutter test`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run backend verification**
|
||||
|
||||
Run:
|
||||
- `uv run pytest backend/tests/unit/v1/schedule_items -q`
|
||||
- `uv run pytest backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Manual device matrix**
|
||||
|
||||
Android:
|
||||
- app foreground/background/terminated for cancel/snooze/timeout
|
||||
- overlap popup behavior
|
||||
- endAt stop reminder + archive
|
||||
|
||||
iOS:
|
||||
- action button behavior in foreground/background/terminated
|
||||
- timeout -> snooze behavior after relaunch sync
|
||||
- archive sync after offline period
|
||||
|
||||
**Step 4: Document operational caveats**
|
||||
|
||||
Include:
|
||||
- Android full-screen may degrade to heads-up by OEM policy
|
||||
- iOS does not guarantee Android-style full-screen alarm behavior
|
||||
- eventual consistency via outbox + startup replay + backend safety job
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-runbook.md docs/protocols
|
||||
git commit -m "docs: add reminder action runbook and platform caveats"
|
||||
```
|
||||
|
||||
## Notes on iOS parity
|
||||
|
||||
- iOS supports actionable local notifications via Darwin categories; implement `cancel` and `snooze_10m` action identifiers with same payload model.
|
||||
- iOS cannot guarantee Android-like forced full-screen alarm takeover; use lock-screen alert + sound + action buttons as equivalent UX.
|
||||
- App-terminated network callback reliability is lower on iOS; therefore outbox + startup replay is mandatory for parity.
|
||||
|
||||
## Data contracts and constraints (added)
|
||||
|
||||
### Reminder payload contract
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"title": "string",
|
||||
"startAt": "iso8601-with-offset",
|
||||
"endAt": "iso8601-with-offset|null",
|
||||
"timezone": "IANA",
|
||||
"location": "string|null",
|
||||
"notes": "string|null",
|
||||
"color": "#RRGGBB|null",
|
||||
"mode": "single|aggregate",
|
||||
"aggregateIds": ["uuid"]
|
||||
}
|
||||
```
|
||||
|
||||
Constraints:
|
||||
- `eventId` required and valid UUID.
|
||||
- `startAt` must be timezone-aware datetime.
|
||||
- `mode=aggregate` requires `aggregateIds.length >= 2`.
|
||||
- Payload versioning should be explicit if schema evolves.
|
||||
|
||||
### Reminder outbox contract
|
||||
|
||||
```json
|
||||
{
|
||||
"opId": "uuid",
|
||||
"eventId": "uuid",
|
||||
"action": "cancel|snooze_10m|timeout_30s|auto_archive",
|
||||
"targetStatus": "archived|null",
|
||||
"occurredAt": "iso8601-with-offset",
|
||||
"retryCount": 0,
|
||||
"nextRetryAt": "iso8601-with-offset|null",
|
||||
"state": "pending|done|dead",
|
||||
"lastError": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
Constraints:
|
||||
- Idempotency key: `(eventId, action, occurredAtBucket)`.
|
||||
- Exponential backoff retries with capped max attempts.
|
||||
- `cancel` and `auto_archive` both map to backend `status=archived` PATCH.
|
||||
|
||||
### Uniqueness and dedupe rules
|
||||
|
||||
- Notification identity uses deterministic key per event+cycle: `hash(eventId + cycleStartEpochMinutes + mode)`.
|
||||
- Before scheduling any reminder, cancel existing pending reminders for same dedupe key.
|
||||
- On bootstrap/reinstall, dedupe against local pending requests and outbox state before creating new schedules.
|
||||
- Compensation reminder (`remindAt <= now < endAt`) must generate exactly one immediate reminder per cycle window.
|
||||
|
||||
## Rollback plan
|
||||
|
||||
1) Disable action handling by feature flag while keeping plain reminders.
|
||||
2) Keep backend PATCH status route unchanged (safe rollback path).
|
||||
3) Pause auto-archive job if unexpected archival spikes occur.
|
||||
@@ -76,6 +76,7 @@ Base URL: `/api/v1/agent`
|
||||
- `200 OK`
|
||||
- `Content-Type: text/event-stream`
|
||||
- 事件类型与字段见 `docs/protocols/agent/sse-events.md`
|
||||
- usage 审计与成本回退策略见 `docs/protocols/agent/sse-events.md`(5) Usage 审计协议)
|
||||
- 空闲时会发送 keep-alive 注释行 `: keep-alive`
|
||||
|
||||
### 错误码
|
||||
|
||||
@@ -197,7 +197,107 @@ data: <json>
|
||||
|
||||
`inputTokens`、`outputTokens`、`cost`、`latencyMs`、`model` 属于后端内部统计字段,不在 SSE 对外协议中暴露。
|
||||
|
||||
### 3.5 快照事件
|
||||
---
|
||||
|
||||
## 5) Usage 审计协议(后端内部)
|
||||
|
||||
本节描述后端对 LLM usage 的内部审计与计费策略。该协议用于数据库持久化、成本统计与运行观测,不对 SSE 外部协议直接暴露。
|
||||
|
||||
### 5.1 当前厂商范围
|
||||
|
||||
- DashScope(Qwen)
|
||||
- DeepSeek
|
||||
|
||||
当前实现仅针对上述两家做深度适配。
|
||||
|
||||
### 5.2 原始字段采集(Provider -> Runtime)
|
||||
|
||||
`TrackingChatModel` 会优先读取 provider 直接字段,读取不到时再从 metadata 补齐。
|
||||
|
||||
优先级如下:
|
||||
|
||||
1. 直接字段(优先)
|
||||
- `usage.input_tokens`
|
||||
- `usage.output_tokens`
|
||||
- `usage.total_tokens`
|
||||
- `usage.time`(秒)
|
||||
- `usage.cost`(若存在)
|
||||
2. metadata 字段(补齐)
|
||||
- `metadata.prompt_tokens`
|
||||
- `metadata.completion_tokens`
|
||||
- `metadata.total_tokens`
|
||||
- `metadata.prompt_tokens_details.cached_tokens`
|
||||
- `metadata.prompt_cache_hit_tokens`
|
||||
- `metadata.prompt_cache_miss_tokens`
|
||||
- `metadata.completion_tokens_details.reasoning_tokens`
|
||||
- `metadata.cost` / `metadata.total_cost`(若存在)
|
||||
|
||||
### 5.3 归一化后的内部 usage_summary 字段
|
||||
|
||||
`TrackingChatModel.usage_summary()` 当前输出:
|
||||
|
||||
- `input_tokens`
|
||||
- `output_tokens`
|
||||
- `total_tokens`
|
||||
- `latency_ms`(由 `usage.time * 1000` 转换)
|
||||
- `cached_prompt_tokens`
|
||||
- `prompt_cache_hit_tokens`
|
||||
- `prompt_cache_miss_tokens`
|
||||
- `reasoning_tokens`
|
||||
- `direct_cost`
|
||||
- `direct_cost_observed`(0/1)
|
||||
- `direct_cost_complete`(0/1)
|
||||
- `model_call_records`
|
||||
- `usage_records`
|
||||
- `direct_cost_records`
|
||||
- `cost_source`(`provider` | `catalog_fallback`)
|
||||
|
||||
### 5.4 成本计算策略(严谨优先)
|
||||
|
||||
核心原则:**能直接用 provider 返回就直接用;缺失才 fallback。**
|
||||
|
||||
`LiteLLMService.build_usage_metadata()` 执行规则:
|
||||
|
||||
1. 仅当以下条件同时满足时使用 provider 直出成本:
|
||||
- `usageComplete == true`(`model_call_records == usage_records`)
|
||||
- `direct_cost_observed == 1`
|
||||
- `direct_cost_complete == 1`
|
||||
- `direct_cost` 为有效非负数
|
||||
2. 否则使用 catalog 价格回退计算(`calculate_cost`)
|
||||
|
||||
### 5.5 Fallback 计费细节
|
||||
|
||||
- 档位选择:按 `prompt_tokens` 命中 `pricing_tiers.max_prompt_tokens`
|
||||
- 公式:
|
||||
|
||||
```text
|
||||
cost = uncached_prompt_tokens * input_cost_per_token
|
||||
+ cached_prompt_tokens * cached_token_rate
|
||||
+ completion_tokens * output_cost_per_token
|
||||
```
|
||||
|
||||
- `cached_token_rate` 规则:
|
||||
- 若 tier 配置了 `cache_hit_cost_per_token` 且 > 0,使用该值
|
||||
- 否则回退为 `input_cost_per_token`
|
||||
|
||||
### 5.6 内部 costSource 语义
|
||||
|
||||
- `provider`: 使用 provider 直接成本
|
||||
- `catalog_fallback`: 正常使用价格表回退
|
||||
- `catalog_fallback_incomplete_provider_cost`: provider 返回了部分 direct cost,但不完整,回退价格表
|
||||
- `incomplete_usage_fallback`: usage 本身不完整,回退价格表
|
||||
|
||||
### 5.7 DeepSeek / DashScope 当前观测到的返回特征
|
||||
|
||||
根据当前线上探针与运行结果:
|
||||
|
||||
- 两家都稳定返回:`input_tokens`、`output_tokens`、`time`
|
||||
- `usage.total_tokens` 顶层可能为空,但 `metadata.total_tokens` 可用
|
||||
- DeepSeek 常见 `prompt_tokens_details.cached_tokens`、`prompt_cache_hit_tokens`、`prompt_cache_miss_tokens`
|
||||
- DashScope 常见 `completion_tokens_details.reasoning_tokens`(可能为 `null`)
|
||||
- 两家当前都未稳定提供直接 `cost` 字段,因此多数场景为 catalog fallback
|
||||
|
||||
## 6) 快照事件
|
||||
|
||||
编码器支持以下 AG-UI 类型映射:
|
||||
|
||||
@@ -208,7 +308,7 @@ data: <json>
|
||||
|
||||
---
|
||||
|
||||
## 4) 字段命名约定
|
||||
## 7) 字段命名约定
|
||||
|
||||
- 事件顶层通用字段使用 AG-UI 风格:`type`、`threadId`、`runId`
|
||||
- 部分业务字段沿运行时模型历史命名保留下划线:
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Calendar Reminder Alert Lifecycle Protocol
|
||||
|
||||
## Version
|
||||
|
||||
- Current: `1.0`
|
||||
- Status: Draft
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
定义日程提醒弹窗在 Android/iOS 的统一行为语义,覆盖提醒触发、用户操作、超时、离线补偿、归档与重装恢复,确保多端一致性和可恢复性。
|
||||
|
||||
---
|
||||
|
||||
## Canonical Rules
|
||||
|
||||
1. 提醒弹窗动作语义跨平台一致:`cancel`、`snooze_10m`、`timeout_30s`。
|
||||
2. `timeout_30s` 语义固定为忽略当前弹窗并按 10 分钟后再次提醒,不直接归档。
|
||||
3. `cancel` 必须归档对应日程(`status=archived`)。
|
||||
4. 到达事件结束时间后(`now >= end_at`)必须停止提醒并归档。
|
||||
5. 归档后的 UI 渲染必须灰色显示;不强制改写原始 metadata 颜色。
|
||||
6. 前端动作上报后端采用最终一致性:本地 outbox + 重放机制。
|
||||
|
||||
---
|
||||
|
||||
## Reminder Payload Contract
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"title": "string",
|
||||
"startAt": "iso8601-with-offset",
|
||||
"endAt": "iso8601-with-offset|null",
|
||||
"timezone": "IANA",
|
||||
"location": "string|null",
|
||||
"notes": "string|null",
|
||||
"color": "#RRGGBB|null",
|
||||
"mode": "single|aggregate",
|
||||
"aggregateIds": ["uuid"],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Constraints
|
||||
|
||||
- `eventId` 必填且为 UUID。
|
||||
- `startAt` 必须带时区偏移。
|
||||
- `mode=aggregate` 时,`aggregateIds` 至少包含 2 个 id。
|
||||
- `version` 必填,用于后续协议升级兼容。
|
||||
|
||||
---
|
||||
|
||||
## Action Contract
|
||||
|
||||
动作枚举:
|
||||
|
||||
- `cancel`: 用户取消提醒并归档事件
|
||||
- `snooze_10m`: 用户点击稍后提醒,重排到 `now + 10m`
|
||||
- `timeout_30s`: 用户 30s 未处理,按 `snooze_10m` 处理
|
||||
- `auto_archive`: 系统判定事件已过期,自动归档
|
||||
|
||||
---
|
||||
|
||||
## Outbox Contract (Frontend)
|
||||
|
||||
```json
|
||||
{
|
||||
"opId": "uuid",
|
||||
"eventId": "uuid",
|
||||
"action": "cancel|snooze_10m|timeout_30s|auto_archive",
|
||||
"targetStatus": "archived|null",
|
||||
"occurredAt": "iso8601-with-offset",
|
||||
"retryCount": 0,
|
||||
"nextRetryAt": "iso8601-with-offset|null",
|
||||
"state": "pending|done|dead",
|
||||
"lastError": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
### Constraints
|
||||
|
||||
- 幂等键:`(eventId, action, occurredAtBucket)`。
|
||||
- 重试策略:指数退避,最大重试次数可配置。
|
||||
- `cancel` 和 `auto_archive` 都映射到后端 `PATCH status=archived`。
|
||||
- Outbox 记录必须本地持久化,App 重启后可恢复。
|
||||
|
||||
---
|
||||
|
||||
## Scheduling and Compensation Rules
|
||||
|
||||
### Normal schedule
|
||||
|
||||
- `remindAt = startAt - reminderMinutes`
|
||||
|
||||
### Bootstrap/reinstall compensation
|
||||
|
||||
启动重建时对每个 active 事件执行:
|
||||
|
||||
1. `now < remindAt`:按 `remindAt` 正常调度。
|
||||
2. `remindAt <= now < endAt`:立刻补偿提醒(建议 `+5s`),然后进入 10 分钟节奏。
|
||||
3. `now >= endAt`:不再提醒,走归档流程。
|
||||
|
||||
---
|
||||
|
||||
## Uniqueness and Dedupe Rules
|
||||
|
||||
- 通知唯一键:`hash(eventId + cycleStartEpochMinutes + mode)`。
|
||||
- 每次创建提醒前必须取消同 dedupe key 的旧提醒(upsert 语义)。
|
||||
- 补偿提醒在同一 cycle 窗口内最多触发一次。
|
||||
- 启动恢复时要同时参考 pending notification 和 outbox 状态,避免重复调度。
|
||||
|
||||
---
|
||||
|
||||
## Overlap Rule
|
||||
|
||||
- 同一分钟内多个提醒合并为一个 aggregate 弹窗。
|
||||
- aggregate 弹窗默认操作作用于全部成员事件。
|
||||
- aggregate 负载中必须包含 `aggregateIds` 以支持后续批处理。
|
||||
|
||||
---
|
||||
|
||||
## Backend Contract Reuse
|
||||
|
||||
- 复用现有接口:`PATCH /schedule-items/{item_id}`,请求体传 `{"status":"archived"}`。
|
||||
- 建议提供/改造 overlap 查询语义用于启动补偿:
|
||||
- `start_at <= window_end`
|
||||
- `end_at IS NULL OR end_at >= window_start`
|
||||
- `status=active`
|
||||
|
||||
---
|
||||
|
||||
## Platform Notes
|
||||
|
||||
### Android
|
||||
|
||||
- 优先 full-screen intent;系统可能因策略降级为 heads-up/横幅。
|
||||
- 声音和振动受通知通道及系统设置影响。
|
||||
|
||||
### iOS
|
||||
|
||||
- 支持动作按钮与本地提醒语义。
|
||||
- 不保证 Android 式强制全屏闹钟弹窗;以锁屏/横幅提醒为主。
|
||||
@@ -1 +1,5 @@
|
||||
当前项目有语音识别功能,但是语音识别的cost成本计算没有实现。目前我们用的模型是fun-asr-realtime-2026-02-28,价格是0.00033元/每秒。我希望把它做到backend/src/core/config/static/database/llm_catalog.yaml,加一个asr字段,引入model_code代替原agent router里的硬编码,通过加载配置获取模型信息和报价,然后根据后端路由接收到的音频长度然后来估算价格,或者看看dashscope的sdk是否会返回消耗token金额,将这个token金额看看如何审计
|
||||
- 当前项目有语音识别功能,但是语音识别的cost成本计算没有实现。目前我们用的模型是fun-asr-realtime-2026-02-28,价格是0.00033元/每秒。我希望把它做到backend/src/core/config/static/database/llm_catalog.yaml,加一个asr字段,引入model_code代替原agent router里的硬编码,通过加载配置获取模型信息和报价,然后根据后端路由接收到的音频长度然后来估算价格,或者看看dashscope的sdk是否会返回消耗token金额,将这个token金额看看如何审计
|
||||
- 路由细分,方便agent url的导航跳转
|
||||
- agent模式重构
|
||||
- 日历导出和导入
|
||||
- 手机号注册、登录
|
||||
|
||||
Reference in New Issue
Block a user