Files
memind/docs/schedule-reminder-design.md
john 229805a070 Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 23:06:43 +08:00

684 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 待办 / 日程 / 提醒能力设计文档
> **状态:** 设计稿
> **目标版本:** v0.2.x
> **适用范围:** H5 门户、微信服务号 Agent、MindSpace 行程展示页
> **生产提醒:** 当前仓库目录可能承载生产服务。开发和验证按 `docs/service-isolation-runbook.md` 先在测试目录完成,不在生产目录直接跑迁移或重启服务。
## 背景
用户希望用自然语言完成两类高频动作:
1. 记录事项:例如“我明天要去开会,帮我记录增加一个提醒”。
2. 查看计划:例如“看看我的行程计划”。
现状里已有几个可利用基础:
- `capabilities.mjs` 已预留 `todo` 能力,但默认关闭,且当前项目没有本地一等的待办/日程表。
- `wechat-mp.mjs` 已能接收微信服务号文本、转给用户专属 Agent、再通过客服消息回复。
- MindSpace 已有生成公开/私有 HTML 页面的能力,适合把一周行程做成简洁精美页面。
缺口在于:日程和提醒还没有结构化存储、没有到点派发 worker、没有 Agent 可调用的本地日程工具,也没有固定的澄清规则。
## 产品目标
### 必须支持
- 用户用自然语言创建待办、日程、提醒。
- 缺少必要时间信息时,助手必须追问,不猜测具体钟点。
- 用户明确“不提醒”时,只记录到待办或日程列表,不创建提醒推送。
- 对会议类表达,如果已有明确开始时间但没有指定提前多久提醒,默认提前 1 小时提醒。
- 用户查看行程时,按指定时间范围展示;未指定范围时展示最近 7 天。
- 行程查询结果优先以简洁消息回复;当内容较多或用户要求“页面/好看一点”时,生成 MindSpace HTML 页面。
### 暂不纳入 MVP
- 复杂重复规则,如“每月第二个周三”。
- 多人共享日程、会议邀请、外部日历同步。
- 地理围栏提醒。
- 跨端原生 push。MVP 先走已绑定微信服务号通知通道,后续再扩展短信、邮件或 App push。
## 概念模型
| 概念 | 说明 | 例子 |
|------|------|------|
| 待办 task | 有或没有截止时间的任务,不一定占用时间段 | “买票”、“周五前交材料” |
| 日程 event | 有开始时间,通常可以有结束时间,占用时间段 | “今天下午三点开会” |
| 提醒 reminder | 某个时间点触发的一次通知,可挂在 task/event 上 | “会议前 1 小时提醒” |
设计原则:
- 待办和日程是“记录”;提醒是“通知计划”。
- 一条待办/日程可以没有提醒。
- 一条待办/日程可以有多条提醒,但 MVP 默认最多一条。
- 所有时间入库使用 epoch 毫秒,额外保存用户时区,展示时按用户时区格式化。
## 自然语言交互规则
### 创建提醒或日程
#### 规则 1:缺少事件发生时间时必须追问
用户说:
```text
我明天要去开会,帮我记录增加一个提醒
```
如果系统当前日期是 2026-06-18,助手能解析“明天”为 2026-06-19,但缺少具体钟点,不能创建到点提醒。回复:
```text
可以。我先记下“明天开会”。你想几点提醒?会议大概几点开始,提前多久提醒你?
```
待用户补充后再创建。
#### 规则 2:用户明确不提醒时只记录
用户说:
```text
不用提醒,先记一下
```
系统创建一条待办或全天日程,不创建 `h5_schedule_reminders` 记录。回复:
```text
已记录到待办列表:明天开会,未设置提醒。
```
#### 规则 3:会议类有明确开始时间时默认提前 1 小时
用户说:
```text
今天下午三点有个会
```
如果系统当前日期是 2026-06-18,解析为:
- 日程:2026-06-18 15:00,标题“开会”或“会议”
- 提醒:2026-06-18 14:00
- 提醒偏移:60 分钟
回复:
```text
已记录:今天 15:00 会议。我会提前 1 小时,也就是 14:00 提醒你。
```
#### 规则 4:非会议类默认不擅自加提前提醒
用户说:
```text
明天上午十点去取护照
```
创建日程,但如果用户没有说“提醒我”,不自动创建提醒。回复可提示:
```text
已记录:明天 10:00 取护照。需要我提前提醒的话,可以告诉我提前多久。
```
#### 规则 5:用户直接指定提醒时间时按提醒时间创建
用户说:
```text
明天上午九点提醒我带材料
```
创建待办“带材料”,并创建 `remind_at = 明天 09:00` 的提醒。此时不需要追问“提前多久”。
#### 规则 6:每天固定时间推送当天待办记录
用户说:
```text
每天早上 7 点给我发一天的待办记录
```
系统必须创建一条每日待办摘要订阅:
- 类型:`todo_day`
- 时间:每天 07:00
- 通道:微信服务号
- 行为:每天到点查询用户当天待办记录,通过服务号主动发送给用户
回复:
```text
已设置:我会每天早上 7点 通过服务号把当天待办记录发给你。
```
如果用户只说“每天给我发待办记录”,缺少时间,必须追问具体时间。
### 查看行程
#### 规则 7:没有时间范围时默认最近 7 天
用户说:
```text
看看我的行程计划
```
查询范围:
- 起点:用户时区当天 00:00
- 终点:起点 + 7 天
回复格式优先按日期分组:
```text
未来 7 天你有 3 个安排:
6 月 18 日 周四
14:00 会议提醒
15:00 会议
6 月 19 日 周五
全天 开会
```
#### 规则 8:指定时间范围时按范围查询
用户说“看下明天的安排”、“下周有什么会”、“6 月 20 到 25 日的计划”,按指定范围查询。
#### 规则 9:需要精美展示时生成页面
触发条件:
- 用户明确说“用页面展示”、“好看一点”、“生成一个行程页”。
- 查询结果超过 8 条,普通文本不易读。
- 用户来自 H5 页面上下文,适合打开 MindSpace 页面。
页面要求:
- 第一屏直接是行程表,不做营销式 landing page。
- 按日期分组,突出今天、明天、逾期、即将到来。
- 对日程、待办、提醒用不同视觉标识。
- 移动端优先,宽屏下使用双栏或周视图。
## 系统架构
```mermaid
flowchart TD
U["用户文本: 微信/H5"] --> I["意图解析层"]
I -->|缺必要信息| Q["追问用户"]
I -->|可执行| T["Schedule Service"]
T --> DB["MySQL: schedule tables"]
T --> R["Reminder Worker"]
R --> W["微信通知通道"]
T --> V["行程查询 API"]
V --> A["Agent 文本回复"]
V --> P["MindSpace 行程页面"]
```
### 组件职责
| 组件 | 职责 |
|------|------|
| 意图解析层 | 从自然语言提取 action、title、date/time、reminder offset、query range;判断是否需要追问 |
| Schedule Service | 统一创建、查询、修改、取消待办/日程/提醒 |
| Reminder Worker | 周期扫描到期提醒,锁定、投递、记录成功/失败 |
| 微信通知通道 | 发送提醒消息。MVP 优先复用已绑定服务号 openid,生产上线前确认模板/订阅通知资质 |
| MindSpace 展示 | 将查询结果渲染为 HTML 页面,保存到用户 `public/` 或私有页面记录 |
## 数据库设计
### `h5_schedule_items`
记录待办和日程主体。
```sql
CREATE TABLE IF NOT EXISTS h5_schedule_items (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
kind ENUM('task', 'event') NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NULL,
status ENUM('active', 'completed', 'cancelled', 'deleted') NOT NULL DEFAULT 'active',
start_at BIGINT NULL,
end_at BIGINT NULL,
due_at BIGINT NULL,
all_day TINYINT(1) NOT NULL DEFAULT 0,
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
location VARCHAR(255) NULL,
source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent',
source_session_id VARCHAR(128) NULL,
source_message_id VARCHAR(128) NULL,
source_text TEXT NULL,
metadata_json JSON NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
deleted_at BIGINT NULL,
KEY idx_schedule_user_time (user_id, status, start_at, due_at),
KEY idx_schedule_user_updated (user_id, updated_at),
CONSTRAINT fk_schedule_item_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
字段说明:
- `kind = task`:待办,通常使用 `due_at`,也可没有时间。
- `kind = event`:日程,通常使用 `start_at/end_at`
- `all_day = 1`:只有日期没有具体钟点,如“明天开会”。
- `source_text`:保留用户原文,便于回溯和修正。
### `h5_schedule_reminders`
记录提醒计划和投递状态。
```sql
CREATE TABLE IF NOT EXISTS h5_schedule_reminders (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
item_id CHAR(36) NOT NULL,
remind_at BIGINT NOT NULL,
offset_minutes INT NULL,
channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat',
status ENUM('pending', 'locked', 'sent', 'failed', 'cancelled') NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
last_error VARCHAR(500) NULL,
locked_until BIGINT NULL,
sent_at BIGINT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
UNIQUE KEY uq_schedule_item_remind_at (item_id, remind_at, channel),
KEY idx_reminder_due (status, remind_at),
KEY idx_reminder_user (user_id, status, remind_at),
CONSTRAINT fk_schedule_reminder_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE,
CONSTRAINT fk_schedule_reminder_item FOREIGN KEY (item_id) REFERENCES h5_schedule_items(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### `h5_schedule_delivery_logs`
记录每次投递尝试,用于排错和审计。
```sql
CREATE TABLE IF NOT EXISTS h5_schedule_delivery_logs (
id CHAR(36) PRIMARY KEY,
reminder_id CHAR(36) NOT NULL,
user_id CHAR(36) NOT NULL,
channel ENUM('wechat', 'in_app') NOT NULL,
status ENUM('success', 'failed') NOT NULL,
provider_message_id VARCHAR(128) NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(500) NULL,
created_at BIGINT NOT NULL,
KEY idx_delivery_reminder (reminder_id, created_at),
KEY idx_delivery_user (user_id, created_at),
CONSTRAINT fk_schedule_delivery_reminder FOREIGN KEY (reminder_id) REFERENCES h5_schedule_reminders(id) ON DELETE CASCADE,
CONSTRAINT fk_schedule_delivery_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
## 后端模块设计
### 新增文件建议
| 文件 | 说明 |
|------|------|
| `schedule-service.mjs` | 领域服务:创建、查询、更新、取消、完成 |
| `schedule-intent.mjs` | 轻量规则解析和澄清决策,先覆盖中文常用表达 |
| `schedule-reminder-worker.mjs` | 到点提醒扫描、锁定、投递、重试 |
| `schedule-render.mjs` | 文本摘要和页面数据结构渲染 |
| `schedule-service.test.mjs` | 服务层单测 |
| `schedule-intent.test.mjs` | 意图解析和追问规则单测 |
| `schedule-reminder-worker.test.mjs` | worker 锁和重试单测 |
### `schedule-service.mjs`
建议导出:
```js
export function createScheduleService(pool, deps = {}) {
return {
createItem,
updateItem,
completeItem,
cancelItem,
listItems,
createReminder,
cancelReminder,
listDueReminders,
lockReminder,
markReminderSent,
markReminderFailed,
};
}
```
关键约束:
- 所有写入校验 `user_id` 所属。
- 删除使用软删除,避免误删历史提醒。
- 创建提醒时必须确认 `remind_at` 是具体时间点,不能是全天日期。
- `remind_at <= now` 的提醒允许创建,但 worker 应尽快投递,并在回复里提示“时间已到,会立即提醒”。
### `schedule-intent.mjs`
MVP 不必追求全量 NLP。先做规则 + LLM 辅助的混合策略:
1. 规则层识别高置信意图:提醒、开会、行程查询、不提醒、完成/取消。
2. 规则层解析常用中文时间:今天、明天、后天、上午/下午/晚上、几点、半点、下周。
3. 不确定时让 Agent 追问,而不是静默创建。
4. 后续可引入 LLM JSON 解析,但输出必须过 schema 校验。
解析结果结构:
```ts
type ScheduleIntent =
| {
action: 'create';
kind: 'task' | 'event';
title: string;
startAt?: number;
endAt?: number;
dueAt?: number;
allDay?: boolean;
reminder?: { remindAt?: number; offsetMinutes?: number; explicitNoReminder?: boolean };
needsClarification?: Array<'event_time' | 'reminder_time' | 'reminder_offset'>;
}
| {
action: 'query';
rangeStart?: number;
rangeEnd?: number;
preferPage?: boolean;
}
| {
action: 'update' | 'cancel' | 'complete';
targetText: string;
};
```
### Agent 接入策略
推荐分两阶段做。
#### 阶段 A:微信服务号入口先做确定性拦截
`wechat-mp.mjs` 转发给 Agent 前增加可选的 schedule preflight
1. 对用户文本调用 `schedule-intent.mjs`
2. 如果能确定执行,直接调用 `schedule-service.mjs` 并通过微信回复结果。
3. 如果缺必要信息,直接追问。
4. 如果不是日程意图,继续走现有 Agent 回复链路。
优点:
- 不依赖 Agent 是否会正确调用工具。
- 对“提醒”这种强业务流程更稳定。
- 不需要先改 goosed extension。
#### 阶段 B:给 H5 Agent 增加本地 schedule 工具
后续把日程能力暴露为平台工具,而不是让 Agent 调 H5 内部 API。建议工具:
- `schedule_create_item`
- `schedule_list_items`
- `schedule_update_item`
- `schedule_cancel_item`
- `schedule_create_reminder`
- `schedule_render_plan_page`
注意:当前 `api_lockdown` 会阻止 Agent 经代理访问 H5 本地接口,所以不要设计成“Agent 直接请求 `/mindspace/...``/api/schedule/...`”。更稳的是在服务端扩展平台工具,或在代理层专门白名单化受控的 schedule 工具调用。
## API 设计
H5 前端和管理调试可用 REST API;Agent 工具不直接走这些 API。
### 创建事项
`POST /api/schedule/items`
```json
{
"kind": "event",
"title": "会议",
"start_at": 1781766000000,
"end_at": null,
"all_day": false,
"timezone": "Asia/Shanghai",
"source_text": "今天下午三点有个会",
"reminder": {
"remind_at": 1781762400000,
"offset_minutes": 60,
"channel": "wechat"
}
}
```
### 查询事项
`GET /api/schedule/items?from=1781712000000&to=1782316800000&include_reminders=1`
返回按时间升序的 items,每条带提醒摘要。
### 更新、完成、取消
- `PATCH /api/schedule/items/:id`
- `POST /api/schedule/items/:id/complete`
- `POST /api/schedule/items/:id/cancel`
- `POST /api/schedule/reminders/:id/cancel`
所有接口需要登录态和 user ownership 校验。
## 提醒 worker 设计
### 扫描策略
每 30 秒扫描一次:
```sql
SELECT *
FROM h5_schedule_reminders
WHERE status = 'pending'
AND remind_at <= ?
ORDER BY remind_at ASC
LIMIT 50
```
锁定时使用事务和状态更新:
```sql
UPDATE h5_schedule_reminders
SET status = 'locked',
locked_until = ?,
attempts = attempts + 1,
updated_at = ?
WHERE id = ?
AND status = 'pending';
```
投递成功后标记 `sent`;失败则:
- 可重试错误:状态改回 `pending``remind_at = now + backoff`
- 不可重试错误:状态改为 `failed`
- 超过最大次数:状态改为 `failed`
### 幂等和并发
- `locked_until` 防止多进程重复投递。
- `delivery_logs` 记录每次尝试。
- `uq_schedule_item_remind_at` 防止同一事项重复创建同一时间提醒。
### 通知通道
MVP
- 已绑定微信服务号用户:通过微信通知。
- 未绑定或通知失败:保留站内提醒状态,用户下次打开 H5 时展示。
生产上线前必须确认:
- 服务号客服消息是否适用于该次主动提醒。
- 是否需要模板消息或订阅通知模板。
- 对失败码做明确分流,例如未关注、超出发送窗口、模板不可用。
## 行程页面设计
### 页面数据结构
```json
{
"title": "未来 7 天行程",
"range_label": "6 月 18 日 - 6 月 24 日",
"days": [
{
"date": "2026-06-18",
"weekday": "周四",
"items": [
{
"time": "15:00",
"title": "会议",
"kind": "event",
"reminder": "14:00 提醒",
"status": "active"
}
]
}
]
}
```
### 视觉要求
- 页面第一屏就是行程,不做大段介绍。
- 移动端使用纵向时间线;桌面端可使用 7 日网格。
- 颜色不使用单一紫蓝渐变;建议用清爽白底、墨色文本、低饱和蓝/绿/橙作为状态色。
- 所有文字在 375px 宽度下不溢出。
- 对“已过期、今天、明天、已提醒、无提醒”有明确状态。
## 权限和隐私
- 行程属于用户私密数据,默认不发布到 Plaza。
- 生成 MindSpace 页面时默认保存为私有草稿或用户私有空间;只有用户明确要求分享时才创建公开页。
- API 只返回当前登录用户的数据。
- 日程原文 `source_text` 可能含隐私,后台日志不要直接打印全文。
## 配置项
建议新增环境变量:
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `H5_SCHEDULE_ENABLED` | `0` | 是否启用日程 API 和微信 preflight |
| `H5_REMINDER_WORKER_ENABLED` | `0` | 是否启动提醒 worker |
| `H5_REMINDER_SCAN_INTERVAL_MS` | `30000` | worker 扫描间隔 |
| `H5_REMINDER_DEFAULT_MEETING_OFFSET_MINUTES` | `60` | 会议默认提前提醒分钟数 |
| `H5_REMINDER_MAX_ATTEMPTS` | `5` | 最大投递次数 |
| `H5_DEFAULT_TIMEZONE` | `Asia/Shanghai` | 默认用户时区 |
部署“每天早上 7 点服务号推送待办记录”时,至少需要:
```bash
H5_WECHAT_MP_ENABLED=1
H5_SCHEDULE_ENABLED=1
H5_REMINDER_WORKER_ENABLED=1
H5_DEFAULT_TIMEZONE=Asia/Shanghai
```
## 开发步骤
### P0:设计和测试骨架
- 新增本文档。
- 新增 `schedule-intent.test.mjs`,把关键中文场景先写成测试。
- 新增空服务骨架,确保不影响现有启动。
### P1:本地存储和 API
-`schema.sql` 加三张表。
-`db.mjs``migrateSchema``CREATE TABLE IF NOT EXISTS`
- 实现 `schedule-service.mjs`
- 实现 REST API,并补 `src/api/client.ts` 类型。
### P2:微信入口 preflight
-`wechat-mp.mjs` 中,当 `H5_SCHEDULE_ENABLED=1` 时启用日程意图解析。
- 对可执行请求直接创建并回复。
- 对缺信息请求直接追问。
- 非日程消息保持现有 Agent 路径。
### P3:提醒 worker
- 实现 `schedule-reminder-worker.mjs`
-`server.mjs` 启动时按 env 开关启动。
- 先接入微信发送方法,失败后写 delivery log。
### P4:行程展示
- 文本摘要:直接在微信/H5 聊天里展示。
- 页面摘要:生成 MindSpace HTML 草稿或公开页,用户确认后再公开。
## 测试计划
### 单元测试
必须覆盖:
- “我明天要去开会,帮我记录增加一个提醒”解析为缺 `event_time``reminder_offset`
- “不用提醒,先记一下”不会创建 reminder。
- “今天下午三点有个会”创建 event,并默认 offset 60。
- “明天上午九点提醒我带材料”创建 task reminderremind_at 为明天 09:00。
- “看看我的行程计划”默认查询最近 7 天。
- “看下明天的安排”查询明天 00:00 到后天 00:00。
### 服务层测试
- 创建 event + reminder 成功。
- 同一 item 同一 remind_at 重复创建被幂等处理。
- cancel item 后 reminder 自动不可投递。
- listItems 不返回其他用户数据。
### Worker 测试
- 到期 reminder 被锁定并发送。
- 并发 worker 不重复发送。
- 发送失败按 backoff 重试。
- 超过最大次数标记 failed。
### 回归测试
```bash
pnpm test
pnpm run build
```
涉及微信入口时补 `wechat-mp.test.mjs`
- 日程意图命中时不进入 Agent。
- 非日程文本仍走 Agent。
- 重复微信 `msgId` 仍只处理一次。
## 验收用例
| 输入 | 期望 |
|------|------|
| 我明天要去开会,帮我记录增加一个提醒 | 追问具体会议时间和提前多久提醒 |
| 不提醒,先记一下 | 创建待办/全天日程,无 reminder |
| 今天下午三点有个会 | 创建 15:00 会议,14:00 提醒 |
| 明天上午九点提醒我带材料 | 创建 09:00 提醒 |
| 每天早上 7 点给我发一天的待办记录 | 创建每日 07:00 服务号待办摘要订阅 |
| 看看我的行程计划 | 展示最近 7 天 |
| 看看明天行程 | 只展示明天 |
| 生成一个好看的本周行程页面 | 生成 MindSpace 行程页面 |
## 风险和决策
| 风险 | 处理 |
|------|------|
| 微信主动通知规则限制 | 上线前确认模板/订阅通知资质;客服消息仅作可用时通道 |
| 自然语言时间歧义 | 缺关键时间必须追问 |
| Agent 幻觉创建 | MVP 在微信入口做确定性 preflight,后续再开放工具 |
| 生产库迁移风险 | 测试库先迁移,生产按维护窗口执行 |
| 用户隐私 | 默认私有,不自动公开行程页面 |
## 推荐结论
这项能力可做,建议按 P1 到 P3 先交付一个可靠 MVP:能记录、能追问、能查询、能到点提醒。MindSpace 精美行程页作为 P4 增强,不阻塞核心提醒能力上线。