# 第二阶段主动调度 MVP 功能预期

## 0. 当前状态摘要（2026-05-02）

本文档仍作为主动调度 MVP 的产品边界说明；具体实现拆分、表结构和阶段进度以《第二阶段主动调度 MVP 实现方案.md》和《主动调度缺口分阶段实施计划.md》为准。

截至当前仓库状态，MVP 主闭环已经从“功能预期”推进到可演示、可排障的实现状态：

1. `important_urgent_task` 已能从任务池生成主动调度 trigger，写入 `active_schedule_previews`，并通过飞书 webhook 触达用户。
2. 飞书链接已统一进入 `/assistant/{conversation_id}`，不再使用独立 `/schedule-adjust/{preview_id}` 入口。
3. 用户进入助手页后，可以看到主动调度卡片，并打开日程预览与精排弹窗；确认前只展示 preview，不写正式 `schedule_events`。
4. 用户确认后才进入同步 apply 链路；apply 失败必须回写可排障状态，不能半写正式日程。
5. `unfinished_feedback` 的“定位 -> ask_user -> 重跑 graph -> 刷新 preview”闭环已经落地，阶段 5 不再重复全套主链路，只保留 1 条真实 chat 烟测确认入口未回归。

演示和验收时需要特别注意：主动调度窗口是“从当前时刻起滚动 24 小时”，不是自然日或整周。如果窗口落在周六且该用户没有课程，预览里只出现待安排任务是符合语义的；需要展示“已有课程 + 待确认任务”同屏时，应把 `mock_now` 固定到有课程的周四窗口。

## 1. 文档目的

本文档先讨论第二阶段最终想做成什么功能，不进入具体代码拆分和表结构实现。

第二阶段的核心目标不是让 Agent 获得直接改正式日程的能力，而是让系统具备一条可控的主动发现链路：

```text
课程 / 任务事实变化
  -> 后台观测滚动 24 小时内的任务与日程风险
  -> 生成结构化诊断和候选方案
  -> 让 LLM 做解释、追问和有限选择
  -> 写入预览 / 触达用户
  -> 用户回系统确认后再进入正式应用
```

这里的关键是：**系统主动观测，后端给候选并粗排，LLM 做解释 / 追问 / 有限选择，用户掌握最终确认权**。

---

## 2. 产品边界

### 2.1 只处理滚动 24 小时内的局部问题

第一版主动调度只关注从当前时刻起滚动 24 小时内的问题。

不处理：

1. 未来 72 小时的全局计划。
2. 本周整体重排。
3. 多天任务均衡。
4. 为了优化指标而跨天反复搬动任务。

这样做的原因：

1. 滚动 24 小时内的问题最接近用户当下行动，主动触达更有必要。
2. 范围越小，误改日程的风险越低。
3. 第二阶段重点是验证“任务池与日程视图联通”，不是做完整排程引擎。

### 2.2 不给 Agent 直接写正式日程的权限

日程是高风险数据。第一版不计划让 Agent 绕过用户确认直接写 `schedule`。

允许：

1. 读取任务与日程事实。
2. 生成风险诊断。
3. 生成候选方案。
4. 写入待确认预览。
5. 发布飞书触达事件。

不允许：

1. 后台 worker 直接修改正式日程。
2. 飞书通知直接应用日程变更。
3. LLM 根据自然语言自由构造正式日程写入。
4. 因为后台诊断认为存在风险，就绕过用户确认改动正式日程。

### 2.3 主动调度不是新增一批通用日程工具

现有日程工具已经有大量可复用能力，包括读取、分析、局部移动、交换、队列处理、结构化结果展示等。

第二阶段新增能力的重点不应是重做 `schedule` 工具，而是补上“任务池与日程视图联通”所缺的观测能力：

1. 哪些任务已经进入四象限“重要且紧急”池，应该被拉入日程视图。
2. 哪些任务尚未进入 schedule 视图但应该被提醒安排。
3. 用户明确反馈未完成后，哪些后继任务会被挤压。
4. 当前是否存在值得打扰用户的候选方案。

---

## 3. 设计理念

### 3.1 参考 `analyze_health`，做观测与候选生成

当前 `analyze_health` 的有效经验是：

1. 后端先做结构化观测。
2. 后端输出问题、指标、裁决和候选操作。
3. LLM 不做开放式全窗搜索，也不承担主排序；它只在后端候选里做有限选择与表达。
4. 候选必须是后端验证过合法性和收益的。
5. 如果没有值得继续处理的问题，后端明确返回 close / ask_user，而不是继续诱导 LLM 硬调。

第二阶段应延续这个模式。

也就是说，新能力更像一个主动调度观测能力，而不是一个自由排程工具。具体工程工具名后续再确认，本阶段只固定职责边界。

### 3.2 让 LLM 做解释、追问和有限选择

LLM 的职责：

1. 把结构化诊断转换成用户能理解的解释。
2. 在后端候选非常接近时，做有限选择。
3. 在信息不足时，把后端 `missing_info` 转成自然追问。
4. 根据主动注入的上下文理解用户偏好，但不调用单独的 `user_preference.get` 工具。

后端的职责：

1. 注入用户上下文、偏好和必要任务事实。
2. 消费四象限“重要且紧急”任务池，不在主动调度内重复管理 DDL 阈值。
3. 枚举候选方案。
4. 校验候选方案是否可行。
5. 明确给出候选的风险、收益和确认要求。

---

## 4. 上下文注入方式

第二阶段不新增 `user_preference.get` 工具。

偏好、近期反馈、必要上下文应由主动调度入口在运行前注入，例如：

```text
ActiveScheduleContext
  user_id
  now
  time_window = now ~ now+24h（滚动 24 小时）
  course_context
  task_context
  schedule_context
  quadrant_context
  injected_preferences
  recent_user_feedback
  trigger_source
```

这样可以保证：

1. LLM 不需要额外调用偏好读取工具。
2. worker 和 API 测试触发走同一套上下文构造逻辑。
3. 偏好采集仍归 memory / 上下文系统负责，主动调度只消费注入后的上下文。
4. 后续飞书入口、Web 入口、后台定时入口可以共享上下文协议。

---

## 5. MVP 触发来源

第二阶段触发基本由课程和任务本身驱动，暂不做泛化生活习惯型触发。

### 5.1 重要且紧急任务进入日程视图

触发条件：

1. 任务来自四象限“重要且紧急”池。
2. 任务未完成。
3. 当前日程视图中没有足够明确的完成安排，或任务尚未进入 schedule 视图。
4. 主动调度不单独维护 DDL 临近阈值，阈值判断交给四象限自动轮换机制。

系统目标：

1. 判断该任务是否应该被加入 schedule 预览。
2. 判断滚动 24 小时内是否存在合适的可用窗口。
3. 给出一个或多个“加入日程视图”的候选方案。
4. 若没有可用窗口，则给出 ask_user 或风险提醒。
5. 注意当前四象限轮换机制是懒加载：只有用户访问时才会轮换。第二阶段需要考虑在后台触发前刷新轮换结果，或补一个 worker 侧轮换入口，避免主动调度读到过期任务池。

### 5.2 用户反馈已排任务未完成

触发条件：

1. 任务已经出现在 schedule 视图中。
2. 用户明确反馈该任务没有完成，例如“刚才那个没做完”“这项要延后”。
3. 该任务仍然影响滚动 24 小时内的 DDL、后继任务或今日计划。

系统目标：

1. 默认情况下，动态任务计划时间过去后按已完成推进，不主动追问。
2. 用户明确反馈未完成时，再把该任务拉回主动调度链路。
3. 分析滚动 24 小时内后继任务是否被挤压。
4. 生成局部补救候选，例如今天补做、延后低优先级后继任务，或者暂不调整。

### 5.3 课程表变化触发后置

课程变化触发暂不进入第一版。

原因：

1. 当前课程基本不可移动，主动调度很难在课程层直接补救。
2. 调停课、补课、课程调整等机制还需要先完善。
3. 等课程调整机制稳定后，再把课程变化纳入主动调度触发源。

后续触发条件可以是：

1. 滚动 24 小时内课程时间发生变化。
2. 变化挤占了原本计划给任务的时间。
3. 被挤占任务尚未开始，或用户明确反馈未完成，且仍有 DDL 或后继影响。

系统目标：

1. 判断受影响任务是否需要重新进入候选。
2. 仅围绕受影响任务和直接后继任务生成局部方案。
3. 不做全局重排。

### 5.4 用户反馈疲劳

触发条件：

1. 用户在对话或反馈中明确表达“很累”“今天撑不住”“不想继续”等状态。
2. 滚动 24 小时内仍存在待执行任务、DDL 高压任务，或用户明确反馈未完成的任务。

系统目标：

1. 判断是否有低优先级任务可以移出今日预览。
2. 判断是否应只提醒用户确认而不是主动给移动候选。
3. 不因疲劳反馈自动取消 DDL 高风险任务。

---

## 6. 核心场景的功能预期

### 6.1 重要且紧急任务还没进入日程视图

这是“从任务池进入日程视图”的问题。

用户视角：

```text
系统发现你有一个重要且紧急任务还没有安排到接下来 24 小时的时间表里。
系统给出 1~3 个可选安排建议。
你确认后，它才进入正式日程。
```

系统预期：

1. 从四象限“重要且紧急”池识别应被拉入日程视图的任务。
2. 排除已完成、已取消、信息不足的任务。
3. 检查滚动 24 小时内可用时间。
4. 输出候选项，而不是直接调用正式 schedule 写入。

候选项示例：

```json
{
  "candidate_id": "place_task_123_slot_a",
  "action": "加入日程预览",
  "target": {
    "task_id": 123,
    "source": "task_pool"
  },
  "preview_change": {
    "type": "add_to_schedule_view",
    "day": 0,
    "slot_start": 7,
    "slot_end": 8
  },
  "why": "任务已进入重要且紧急池，当前尚未进入日程视图；该时段未被课程占用。",
  "risk": "需要用户确认预计耗时是否足够。",
  "requires_user_confirm": true
}
```

### 6.2 Schedule 内任务由用户反馈未完成

这是“用户明确说没做完之后的后继调整”问题。

用户视角：

```text
系统默认刚才安排的动态任务已经完成，不会频繁追问。
如果你告诉系统“没做完”，它会基于接下来 24 小时给出补救候选。
```

系统预期：

1. 动态任务计划时间过去后默认按 `assumed_completed` 处理。
2. 不因为缺少完成证据就主动进入风险确认。
3. 用户反馈未完成后，才分析滚动 24 小时内后继任务是否被影响。
4. 输出“今天补做 / 延后后继 / 暂不调整”类型候选。

候选项示例：

```json
{
  "candidate_id": "unfinished_task_456_repair_today",
  "action": "未完成补救预览",
  "target": {
    "task_id": 456,
    "source": "schedule_task"
  },
  "preview_change": {
    "type": "repair_unfinished_task",
    "day": 0,
    "slot_start": 9,
    "slot_end": 10
  },
  "why": "用户反馈该任务没有完成，且它仍影响 24 小时内的后续安排。",
  "risk": "需要用户确认是否接受挤占后续低优先级任务。",
  "requires_user_confirm": true
}
```

### 6.3 动态任务失败后的后继挤压补救

这是“当前动态任务炸了，但后面还有后继任务”的问题。

用户视角：

```text
如果你告诉系统当前任务没做完，而后面还有安排，系统会先尝试在滚动 24 小时内局部补救。
它会优先找不那么痛的方案；如果时间真的不够，会把选择交给你。
```

系统预期：

1. 先判断当前动态任务是否仍值得补救，而不是直接跳过。
2. 如果滚动 24 小时内空间足够，生成“把当前动态任务往后挤进去”的预览。
3. 这个预览允许在必要时打破部分用户偏好，例如原本偏好的学习时段、任务顺序或轻重搭配，但必须清楚说明代价。
4. 如果空间不够，优先询问用户是否能延后结束时间。
5. 如果用户不能延后，或延后仍然不够，再生成“强行融入下一个动态任务”的 rush 预览。

补救策略顺序：

```text
策略 A：局部重排补救
  直接复用并魔改粗排思路，但范围只限滚动 24 小时。
  输入只传受影响的部分 task item，而不是整批任务。
  用户偏好 weekday / slot 优先遵守；如果无法达成，再打破偏好。
  打破偏好的优先级低于“把任务压进时间表”。
  目标是把失败的动态任务重新塞回时间表，并尽量少影响后继任务。

策略 B：协商延后结束时间
  如果当前 24 小时容量不够，询问用户能否延后结束时间或放宽边界。

策略 C：压缩融合到下一个动态任务
  如果不能延后，就把失败任务强行融入下一个动态任务。
  第一版默认两个动态任务各 50% 压缩，让用户 rush 一下。
```

策略 C 的产品判断：

1. 它不是理想方案，但通常比直接跳过失败任务更合理。
2. 两节课各自减半的影响，往往小于完全放弃其中一项。
3. 第一版只把它作为兜底候选，不默认自动执行。
4. 第一版默认 50% / 50%，暂不按优先级、DDL 或预计耗时动态分配。
5. 后续如果出现更好的补救策略，可以替换或降低它的优先级。

候选项示例：

```json
{
  "candidate_id": "rush_merge_task_456_into_789",
  "action": "压缩融合预览",
  "target": {
    "unfinished_task_id": 456,
    "next_task_id": 789
  },
  "preview_change": {
    "type": "rush_merge",
    "unfinished_task_ratio": "50%",
    "next_task_ratio": "50%"
  },
  "why": "用户反馈当前动态任务未完成，且 24 小时内没有足够独立空档；直接跳过会损失更大。",
  "risk": "两个任务都会被压缩，需要用户接受 rush 模式。",
  "requires_user_confirm": true
}
```

---

## 7. 产品能力边界

本阶段可以讨论“系统需要哪些能力”，但不在这里冻结具体工具名、入参 schema、handler 注册方式或内部实现路径。

也就是说，本章只定义产品级能力边界；工程级工具设计在产品预期敲定后另开文档。

### 7.1 主动观测能力

第二阶段需要一个类似 `analyze_health` 的主动观测能力。

它负责：

1. 汇总滚动 24 小时内任务 / 日程联通风险。
2. 区分 task DDL 临近和用户反馈未完成后的补救两类问题。
3. 输出结构化指标、问题、裁决和候选。
4. 告诉 LLM 是否应该继续、提醒用户、生成预览或收口。

它不负责：

1. 直接写正式 schedule。
2. 替代现有 `place` / `move` / `swap` 等日程工具。
3. 自行读取偏好工具。
4. 做 24 小时外全局重排。
5. 在飞书内完成复杂确认。

### 7.2 候选生成能力

候选生成能力的产品职责是“出选择题”，不是让 LLM 自由编写正式日程参数。

第一版候选只允许覆盖这些产品动作：

```text
加入日程预览
  将任务池中的 DDL 临近任务加入预览，不直接写正式日程。

未完成补救预览
  对用户明确反馈未完成的日程任务生成补救预览。

后继挤压重排预览
  当前动态任务失败且影响后继时，在滚动 24 小时内做局部重排候选。

延后结束询问
  当前容量不足时，询问用户是否允许延后结束时间或放宽边界。

压缩融合预览
  无法延后时，把失败任务融入下一个动态任务，形成 rush 候选。

询问用户
  信息不足、风险过高、用户反馈不够明确时询问用户。

仅提醒
  只提醒用户查看，不建议调整。

收口
  当前无值得打扰用户的问题。
```

第一版明确不支持这些产品动作：

```text
直接写正式日程
全局重排
自动标记完成
飞书内直接应用日程
后台自动进入 rush 模式
```

### 7.3 预览写入能力

预览写入能力只负责保存待确认方案，让用户能看到“改前 / 改后 / 为什么”。

它不代表正式日程已经变化，也不承担复杂排程判断。

### 7.4 用户确认能力

用户确认能力负责把用户选择的候选项转成正式应用请求。

第一版确认粒度建议按候选项确认，而不是整版黑盒确认。

正式应用时优先复用现有 service 逻辑，不在主动调度模块里绕过既有写入链路单独改状态或改日程。

### 7.5 通知触达能力

通知触达能力只负责把“系统发现问题并生成了建议”这件事告诉用户。

它不负责判断怎么调度，也不负责在飞书内完成确认。

### 7.6 前后对比与回滚能力

当前版本还没有完整的前后对比机制。第二阶段如果要让用户放心确认主动调度建议，需要补齐“改前 / 改后 / 可回滚”的预览能力。

建议方向：

1. 先为当前版本补上前后对比和回滚机制。
2. 主动调度生成的候选复用这套机制，不另起一套预览协议。
3. 用户确认前只看到预览，不修改正式日程。
4. 用户确认后进入现有 service 写入链路；失败时可根据预览快照回滚或提示未应用。

---

## 8. 用户确认与预览预期

第二阶段的用户确认粒度建议按候选项确认，而不是整版黑盒确认。

预览应至少展示：

1. 为什么触发。
2. 当前风险是什么。
3. 建议改变哪一个任务。
4. 改到哪里或准备问用户什么。
5. 不调整会有什么后果。
6. 这个建议是否会影响后继任务。
7. 如果打破了用户偏好，需要明确说明打破了什么、为什么值得这样做。
8. 如果进入 rush 模式，需要明确说明两个任务分别被压缩到什么程度。

确认后才进入正式应用链路。

如果候选是 `ask_user`，用户回答后可以重新触发一次同一链路，但必须带上新的上下文和幂等键，避免重复打扰。

---

## 9. 飞书触达预期

飞书第一版只负责提醒，不承载复杂调度判断。

触达内容应该是：

```text
我发现你接下来 24 小时内有一个任务可能需要安排 / 你反馈的未完成任务需要补救。
我已经生成了一个待确认建议，请回到系统查看。
```

飞书不做：

1. 直接确认日程调整。
2. 直接标记任务完成。
3. 多轮 Agent Chat。
4. 复杂卡片交互。

飞书事件仍建议走：

```text
notification.feishu.requested
```

后续如果要做飞书聊天入口，再单独走 `agent.channel.*`，不要污染通知投递模型。

---

## 10. 完成状态语义

第二阶段采用“默认完成，用户反馈纠偏”的口径。

建议拆成两个产品概念：

```text
assumed_completed
  动态任务计划时间过去后，系统默认按已完成推进体验。

用户反馈未完成
  用户明确告诉系统任务没有完成，系统再进入补救链路。
```

规则：

1. 动态任务计划时间过去后，默认按 `assumed_completed` 处理。
2. 系统不因为缺少完成证据而主动追问用户。
3. 用户明确反馈没完成后，作为触发上下文进入补救分析；它不一定需要落成新的数据库状态。
4. 补救分析仍只生成预览或候选，不能直接改正式日程。

---

## 11. MVP 验收标准与当前状态

第一版验收按“已落地口径、阶段 5 主验收、后置能力”三类看，避免把后续演进项混进本轮收口。

已落地并作为 MVP 口径的能力：

1. 只扫描滚动 24 小时内的问题。
2. 能识别四象限“重要且紧急”池中尚未进入日程视图的任务。
3. 能输出结构化 `metrics / issues / decision / candidates`。
4. 候选项必须来自后端，不让 LLM 自由生成正式写库参数；LLM 只做解释、追问和有限选择。
5. 确认前不直接写正式 `schedule_events / schedules`，只写 `active_schedule_previews` 或触达用户。
6. 能发布 `notification.feishu.requested`，并通过 `notification_records` 记录 `sent / failed / dead / skipped` 等状态。
7. 飞书触达后进入 `/assistant/{conversation_id}`，由助手会话页承载预览、微调和确认。
8. 用户确认后才允许进入同步 apply 链路；成功写正式日程，失败回写预览状态和错误原因。
9. `unfinished_feedback` 已支持用户补充事实后的 `ask_user -> rerun -> ready_preview` 闭环。
10. 动态任务计划时间过去后默认按 `assumed_completed` 推进，不主动追问。

阶段 5 主验收重点：

1. preview 过期后 confirm 必须拒绝。
2. 同一 preview 重复 confirm 只能生效一次。
3. 错误 `candidate_id`、跨用户 `preview_id`、preview 与 session 不匹配都必须拒绝。
4. 冲突或不可写入时，apply 应失败并保留可排障状态。
5. 相同 `idempotency_key / dedupe_key` 不能重复生成有效 trigger / preview / notification。
6. notification 要覆盖未配置 webhook 的 `skipped`、临时失败的 `failed/retry`、永久失败的 `dead` 和成功的 `sent`。
7. 重复消费同一 `notification.feishu.requested` 不能重复投递或重复写 `notification_records`。
8. `api / worker / all` 三种启动边界相关 handler / job 注册不能缺失。

后置能力，不作为当前 MVP 完成阻塞：

1. 课程变化触发、疲劳反馈触发和更复杂的跨天全局重排。
2. `compress_with_next_dynamic_task` 压缩融合候选，当前只保留 schema / 口径，不默认生成。
3. apply 成功后的撤销按钮和完整回滚体验。
4. 更细颗粒的偏好打破策略，例如哪些偏好是硬约束、哪些偏好可在高风险任务前让位。

---

## 12. 已确定结论

1. 时间范围采用滚动 24 小时，不按自然日切分。
2. 主动调度不单独管理 DDL 临近阈值，只消费四象限“重要且紧急”任务池。
3. 当前四象限自动轮换是懒加载机制，这是第二阶段需要处理的工程风险。
4. 用户反馈未完成不是必须新增数据库状态；第一版可以先作为触发上下文。
5. 正式应用优先复用现有 service 逻辑，不在主动调度模块里另写一套状态更新权限。
6. 预览能力建议先补齐当前版本的前后对比和回滚机制，再由主动调度复用。
7. 后继挤压重排优先复用并魔改粗排：传入滚动 24 小时时间窗和部分 task item。
8. 局部重排中，用户偏好 weekday / slot 优先遵守；无法达成时允许打破，但要说明代价，并优先把任务压进去。
9. 压缩融合第一版默认 50% / 50%。
10. 候选项第一版限制为 1~3 个。
11. 课程变化触发后置，等调停课 / 补课 / 课程调整机制完善后再接入。

---

## 13. 已收口与后续演进问题

已收口：

1. 主动观测能力当前落在 `backend/active_scheduler` 准独立模块内，主链路走 graph / service pipeline，不进入 ReAct 工具循环。
2. 主入口统一到 `/assistant/{conversation_id}`，飞书只负责触达和跳转，不在飞书内完成复杂确认。
3. 预览独立写入 `active_schedule_previews`，不塞进 `agent_schedule_states`；正式 apply 仍走后端重校验。
4. task_pool 任务进入正式日程时使用 `schedule_events.task_source_type=task_pool`，不创建孤儿 `task_item`。

后续演进：

1. 四象限懒加载轮换仍建议继续评估后台刷新或抽公共轮换服务，避免主动调度读取到过期任务池。
2. 前后对比与回滚体验还可以继续增强，但不影响当前“确认前只预览、确认后同步 apply”的 MVP 语义。
3. 压缩融合和复杂偏好打破策略暂不打开，等主链路和失败注入脚本稳定后再评估。
4. 课程变化、疲劳反馈和跨天计划优化后置到后续版本。
