# 主动调度缺口补全讨论稿

本文档由“主动调度候选生成器讨论稿”扩充而来，用于记录主动调度后续补全设计。

它不是实施计划，也不替代《第二阶段主动调度 MVP 实现方案》。这里主要保存三类内容：

1. 候选生成器、LLM 选择题和 `ask_user` 的设计共识。
2. 飞书通知进入聊天页后的主动调度合流方案。
3. 第四、第五阶段已经实现链路里仍然存在的空壳、占位和待验收点。

## 1. 当前实际链路

### 1.1 主动调度后端链路

当前主动调度主链路已经形成：

```text
active_schedule.triggered
  -> BuildContext
  -> Observe
  -> GenerateCandidates
  -> CreatePreview
  -> notification.feishu.requested
  -> WebhookFeishuProvider
  -> 用户回系统确认
  -> ConfirmPreview
  -> ApplyActiveScheduleChanges
```

这个链路的主体已经不是空壳：trigger、preview、notification、confirm apply、幂等、retry、api-only / worker-only / all 启动边界都已经被本地验收过。

这里必须把三件事拆开看，避免把“触发”和“通知”混成一条链：

1. 触发来源：后台 worker 自动触发、API 验收 / 后续产品内用户主动入口、`ask_user` 回复后的同步重跑。
2. 业务目标：`important_urgent_task` 负责把 task_pool 任务放进日程；`unfinished_feedback` 负责给已排动态任务新增补做块。
3. 投递方式：后台离线触达才需要飞书 webhook；用户已经在聊天页主动发起或回复 `ask_user` 时，直接通过 timeline / SSE 返回新 preview，不再先走飞书通知。

同一套 active scheduler graph 负责这两类业务目标，区别只在入口和投递方式，不是拆成两套逻辑。

### 1.2 当前没有真正做到的部分

当前仍然缺少这些关键能力：

1. `GenerateCandidates` 仍然是确定性 first-fit 候选生成，不是 topN 候选搜索器。
2. `Observation.Decision.LLMSelectionRequired=true` 已经写进结构，但没有真正的 LLM selector。
3. `CreatePreview` 明确写着“MVP 没有 LLM 选择器”，固定使用 `Candidates[0]`。
4. 本轮主动调度不再额外扩展独立评分子系统，候选裁决只保留轻量、可直接解释的维度。
5. memory 偏好没有进入 active scheduler 的候选裁决。
6. `ask_user` 在 active scheduler 里目前只是候选 / decision 类型，没有用户回复、重跑 graph、重新出结果的闭环。
7. 飞书通知只是离线自动触达，不能承接用户回复；用户主动补充、`ask_user` 回复和后续自由协作都必须回系统内聊天页完成。
8. 飞书 action_url 的当前目标已经收口到现有助手会话路由；前端仍在补适配，但不能继续依赖旧的 `/schedule-adjust/{preview_id}` 详情页口径。

## 2. 总体补全方向

### 2.1 候选生成器定位

候选生成器应该升级为：

```text
安全候选工厂 + 维度评估器 + fallback 排序器
```

它负责：

1. 枚举合法候选。
2. 在只读事实快照上模拟候选实施后的局部结果。
3. 输出可解释的维度评估。
4. 给出后端 fallback 顺序。
5. 在无法安全生成候选时稳定降级为 `ask_user / notify_only / close`。

它不负责：

1. 结合 memory 做最终主观裁决。
2. 生成最终用户话术。
3. 绕过 preview 直接写正式日程。
4. 在事实不足时猜测用户偏好。

### 2.2 LLM 定位

LLM 不应该自由构造日程写库参数。

LLM 应该做：

1. 在后端给出的候选中选择一个。
2. 读取已注入的 memory 上下文、用户近期反馈、候选维度，做软裁决；这些信息不是 `ask_user` 要现场补采的内容。
3. 决定是否需要追问用户。
4. 输出面向用户的解释文案。

LLM 不应该做：

1. 自己发明 slot、event_id、task_id 或写库 change。
2. 修改 hard constraint 判断。
3. 在 `ask_user` 未完成时强行进入普通聊天链路。

### 2.3 newAgent / Eino 接入方向

当前倾向是把主动调度接到 newAgent 入口附近，而不是把主动调度塞进普通 ReAct 工具循环里。

建议边界：

1. active scheduler graph 仍然是独立业务 graph，负责事实读取、候选生成、preview、apply 边界。
2. newAgent 负责承接用户自由表达、现有 memory / execute 链路、页面交互和后续微调。
3. 短期在同一后端进程内同步调用 active scheduler service。
4. 后续拆微服务后，把 active scheduler graph 变成 RPC 同步调用。
5. newAgent 可以把“主动调度会话”作为上下文注入，但不能绕过 active preview / confirm API 直接写正式日程。

## 3. 六个候选生成讨论维度

### 3.1 候选生成器职责边界

已形成的结论：

1. 后端负责硬约束：归属校验、时间窗、容量、deadline、slot 合法性、候选 change 可执行性。
2. 后端负责 fallback 排序：LLM 失败、超时或输出非法时，使用后端 top1。
3. LLM 负责软裁决：已注入的 memory 偏好、近期反馈、风险取舍、用户可接受度。
4. 单个候选生成失败时丢弃该候选并写 trace，不让一个坏候选拖垮整轮。
5. 整体候选生成失败时按原因分流：
   - 业务事实不足：`ask_user`。
   - 没有安全候选：`notify_only` 或 `ask_user`。
   - 系统异常：trigger failed，等待 worker retry 或人工排障。

待补全点：

1. 当前 `GenerateCandidates` 仍然是 first-fit，不是“枚举多个合法候选”。
2. 当前 `rankCandidates` 只是粗排序，没有稳定的维度评分。
3. 当前 `ask_user` 没有和用户回复闭环打通。

### 3.2 候选类型集合

当前已开放：

1. `add_task_pool_to_schedule`：把重要且紧急的 task_pool 任务加入滚动 24 小时空闲节次。
2. `create_makeup`：为未完成反馈新增补做块，不移动原任务。
3. `ask_user`：事实不足时追问。
4. `notify_only`：没有安全候选时只提醒。
5. `close`：触发条件已经失效时关闭。

当前明确关闭：

1. `compress_with_next_dynamic_task`：只保留 schema 和常量，第一版不生成。
2. 局部重排 / 多任务交换：还没有进入 active scheduler 候选集合。

建议下一轮补全：

1. `important_urgent_task` 不只取第一个连续空位，而是枚举 topN：
   - 最早可用空位。
   - deadline 前更稳的空位。
   - 对用户偏好更友好的空位。
   - 对日程节奏影响更低的空位。
2. `unfinished_feedback` 先解决“到底是哪条日程没完成”：
   - 结合随口记上下文、当前时间和已排日程定位具体 schedule event。
   - 定位成功后再生成补做候选；如果缺关键事实，直接进入 `ask_user`，不把比例推断作为主路径。
3. `compress_with_next_dynamic_task` 等候选必须先完成风险模型，再打开生成开关。

候选可编辑性建议：

```text
add_task_pool_to_schedule      可拖动时间，但不能改 target_id
create_makeup                  可拖动时间，可调整补做节数上限
compress_with_next_dynamic_task 暂不开放
ask_user                       不写日程，不可确认 apply
notify_only                    不写日程，不可确认 apply
close                          不写日程，不可确认 apply
```

### 3.3 评估维度与分数体系

当前倾向：先不做单一总分。

原因：

1. 主动调度早期更需要可解释，而不是一个看似精确的总分。
2. LLM 的决策价值要主动收窄：后端负责合法性、粗排和默认裁决，LLM 主要负责解释、接近候选间的有限裁决，以及信息不足时更自然地追问。
3. 后端 fallback 可以用简单稳定顺序，不需要过早引入复杂权重。

候选输出原则：

1. 未通过硬约束的方案不进入候选列表。
2. 硬约束失败只写入 trace / debug / invalid_reason，供排障使用。
3. 给 LLM 和用户看的候选维度，只保留需要取舍、比较或解释的信息。

候选建议输出维度：

```text
capacity_fit            是否满足 estimated_sections
risk_level              low / medium / high
```

暂不保留的维度：

1. `explainability`：太像主观作文评分，后端不好稳定计算，LLM 也容易自证合理。
2. `reversibility`：当前主动调度第一版没有撤销按钮，贸然展示“可逆性”容易误导用户；后续如果实现 undo / rollback，再重新设计。
3. `disruption`：当前主动调度只生成“新增任务块 / 新增补做块”，不移动已有日程，扰动度几乎恒为 `none`，对候选选择没有区分度；等打开移动、压缩、局部重排候选时再恢复。
4. `deadline_fit`：deadline / urgency window 属于硬约束，不满足的方案不进入候选；满足后无需再作为展示维度。
5. `user_preference_fit`：memory / 用户近期反馈更适合交给 LLM 在选择题环节阅读和裁决，不伪装成后端可精确计算的评分。
6. `confidence`：不作为 LLM 可见候选维度。事实可信度只用于内部 trace 和 `ask_user` 门控；如果事实不足以支撑正式候选，就直接 `ask_user`，不要生成“低 confidence 候选”。

各维度的计算口径必须尽量简单：

1. `capacity_fit`：只看候选 slot 数是否覆盖 `estimated_sections`；如果容量不足则不进入候选，保留该字段主要用于区分“刚好够 / 有余量”。
2. `risk_level`：由 candidate_type、候选是否移动 / 压缩 / 重排已有日程、slot 稳定性和非致命 warning 汇总成低/中/高，不单独引入玄学评分。当前第一版只新增不移动，所以多数正式候选会是 `low`；后续接入粗排整体重排时，这个字段才会明显拉开差异。

内部 trace 可保留 `fact_confidence` 或 `evidence_level`，但它不进入 LLM 候选维度：

1. `high`：target 明确、`estimated_sections` 已落库、slot 来自确定课表空档、没有关键缺失信息。
2. `medium`：硬事实齐全，但部分定位来自推断或有非致命 warning。
3. `low`：核心目标不明确或缺关键事实；这种状态应转成 `ask_user`，不生成正式变更候选。

### 3.4 LLM 选择题协议

LLM 可见信息建议分两层：

1. 基础上下文：`trigger_type`、`target`、`time_window`、`missing_info`、`warnings`、`before/after` 摘要。
2. 候选层：`candidate_id`、`candidate_type`、`summary`、`preview_change`、`dimensions`，其中 `dimensions` 只包含 `capacity_fit / risk_level`。
3. 已有 memory 可以作为上下文注入，但不作为 `ask_user` 的缺失信息采集目标。
4. 不暴露原始全量事实快照，避免把后端内脏直接端给模型。

LLM 输入建议：

```json
{
  "trigger": {
    "trigger_id": "ast_xxx",
    "trigger_type": "important_urgent_task",
    "target_type": "task_pool",
    "target_id": 82
  },
  "context_summary": {
    "window": "rolling_24h",
    "missing_info": [],
    "warnings": []
  },
  "memory": {
    "preferences": ["周末不想学习"],
    "recent_feedback": ["晚上不适合高强度任务"]
  },
  "candidates": [
    {
      "candidate_id": "xxx",
      "candidate_type": "add_task_pool_to_schedule",
      "summary": "放到周四第3节",
      "dimensions": {
        "capacity_fit": "exact",
        "risk_level": "low"
      }
    }
  ]
}
```

LLM 输出必须限制为：

```json
{
  "action": "select_candidate",
  "selected_candidate_id": "xxx",
  "reason": "选择这个候选的原因",
  "user_message_summary": "给用户看的简短解释",
  "ask_user_question": ""
}
```

允许的 `action`：

```text
select_candidate
ask_user
notify_only
close
```

兜底规则：

1. LLM 超时：使用后端 fallback candidate。
2. LLM 选了不存在的 candidate：重试一次；仍失败则 fallback。
3. LLM 输出 `ask_user` 但问题为空：后端用候选生成器给出的 missing_info 生成兜底问题。
4. LLM 输出任何写库参数：丢弃写库参数，只保留合法 action / candidate_id。

### 3.5 未完成反馈补做链路

当前状态：

1. 若 trigger 直接携带 `schedule_event` target，后端可以定位未完成对象。
2. 若无法定位目标，observe 会降级为 `ask_user`。
3. 第一版补做只生成新补做块，不移动原任务。

需要补全：

1. 随口记链路要能把“我这个没做完”定位到具体 schedule event。
2. 定位优先靠 LLM 上下文推断 + 当前时间 + 已排日程窗口；定位不稳时直接 `ask_user` 问“是哪一条没做完”。
3. 补做节数最好前置到 task 写入环节，由 `tasks.estimated_sections` 承接；当前 `model.Task` 里已经有这个字段，主动调度也会消费它，但普通任务创建接口和 quick task 入口还没完全透传，所以现在仍要保留 `1` 的兜底。
4. `ask_user` 追问不要设计成固定问法，而是由 `missing_info` 驱动，缺什么就问什么。
5. 用户在聊天页说“我周末不想学习”这类话时，如果已经解除主动调度锁定，就直接进入现有 newAgent memory / execute 链路，不回到主动调度 graph。

### 3.6 压缩融合与局部重排

当前结论：

1. `compress_with_next_dynamic_task` 第一版继续关闭。
2. 打开前必须先解决候选安全性和风险解释。
3. 压缩融合不能由 LLM 自由生成，必须由后端模拟和校验。
4. 压缩融合只允许“同一任务链路内部消化”，不把 A 任务压进无关 B 任务。

压缩融合的业务定义：

```text
谁污染谁治理：哪个任务发生未完成/超时问题，就只在这个任务自己的后继块里消化。
```

换句话说：

1. 如果 A 任务没完成，只允许把 A 的剩余内容压缩进 A 的后继任务块。
2. 不允许因为 B 看起来空余，就把 A 的剩余内容塞进 B。
3. 如果 B 真的长期空余，那是 B 自己的估时或安排问题，不应该用来替 A 兜底。
4. 这样可以避免主动调度把问题跨任务传染，保持候选解释简单、责任边界清楚。

打开条件建议：

1. 只处理同一任务自己的动态后继块，不压缩固定课程、外部事件或其它任务。
2. 被压缩任务必须保留最小节数，不能为了补救把后继块压到失真。
3. 不额外引入抽象风险评分；压缩融合的主要风险不是抽象节奏分，而是这个任务自身仍然完不成。
4. 用户确认页必须清楚展示“哪个任务自己的后继块被压缩、从几节压到几节、腾出的时间补哪里”。
5. 压缩失败时优先 `notify_only`，事实不足才 `ask_user`。

压缩融合的风险口径：

1. 该任务自己的后继块被压缩后，任务整体仍可能完不成。
2. 如果后继块被压得过短，可能需要继续追加补做，而不是继续污染其它任务。
3. 用户可能不接受压缩某类自有后继块，例如复习、运动或休息。
4. 除上述情况外，不额外引入抽象 health 风险。

## 4. 主动调度进入聊天页的合流设计

### 4.1 为什么不做孤立表单页

飞书 webhook 是单向通知，不能指望飞书直接承接 `ask_user` 回复。

既然用户必须回系统内回复，就应该复用聊天页：

1. 复用 newAgent 的自由表达能力。
2. 复用已有日程预览、微调、确认、自动拖拽保存等体验。
3. 用户在主动调度解锁后可以直接说“我周末不想学习”，这类偏好由现有 newAgent memory / execute 链路处理，不在主动调度里单独新建记忆链路。
4. 避免做一个只能补字段的孤立页面。

### 4.2 两种进入聊天页状态

信息完整：

```text
飞书点击
  -> 打开聊天页
  -> 加载 active preview / trigger 上下文
  -> 展示主动调度建议卡片
  -> 用户可确认、拖动微调、提出异议
  -> 正常进入 newAgent 自由链路
```

信息不完整：

```text
飞书点击
  -> 打开聊天页
  -> 进入主动调度 ask_user 锁定态
  -> 用户回复缺失信息
  -> 后端更新事实 / memory
  -> 重跑 active scheduler graph
  -> 生成新的 preview
  -> 解除锁定，回到正常聊天链路
```

关键约束：

1. `ask_user` 未完成前，不允许直接进入普通聊天链路。
2. 用户回复不是普通闲聊，而是当前主动调度 session 的补信息输入。
3. 补信息后必须重跑 active scheduler graph，而不是拿旧候选硬套。
4. 新结果出来后，才允许 Agent 基于新 preview 继续自由协作。
5. 聊天消息仍然正常写入 conversation / timeline；锁定的是后端路由控制权，不是聊天记录写入。

### 4.3 主动调度会话抽象

已拍板：单独新增 `active_schedule_sessions`，不把主动调度状态塞进现有 conversation 表，也不只在 `active_schedule_previews` 上加 `conversation_id`。

原因：

1. 主动调度先于聊天发生：后台 trigger / preview 发出时，用户可能还没有打开聊天页；但在飞书通知真正发出前，后端会预创建或绑定 `conversation_id`，让最终入口落到现有会话路由。
2. 主动调度和聊天生命周期不同：preview 会过期、重跑、确认、忽略；conversation 只是承载用户可见对话。
3. `ask_user` 的恢复语义不同：active scheduler 收到回复后只补当前缺失业务事实并重跑 graph，而不是恢复 newAgent 的 plan / execute 节点，也不新建 memory 写入链路。
4. 审计链路需要串起 `trigger_id -> preview_id -> notification_id -> conversation_id -> apply_id`，单独 session 更清楚。
5. 一个聊天会话后续可以继续讨论同一个主动调度 preview，但这不代表主动调度一直拥有聊天路由管辖权。

最小需要保存：

```text
session_id
user_id
conversation_id            # 创建时可空，通知前必须绑定
trigger_id
current_preview_id
status
state_json                  # 轻量业务状态：pending_question / missing_info / last_candidate_id / last_notification_id / expires_at / failed_reason 等
created_at
updated_at
```

状态建议：

```text
waiting_user_reply   等待用户补信息
rerunning            已收到回复，正在重跑 graph
ready_preview        已有可展示 preview，已解除硬拦截
applied              用户已确认应用
ignored              用户忽略
expired              会话过期
failed               重跑或绑定失败
```

飞书入口已拍板：

```text
/assistant/{conversation_id}
```

不使用 `/assistant?active_preview_id=xxx&trigger_id=xxx` 作为主入口。`preview_id / trigger_id` 由后端通过 session 查询，避免前端 URL 长期承担业务状态拼装。

`ask_user` pending 已拍板：

1. 不复用 newAgent `PendingInteraction` 作为状态源。
2. active scheduler 的 pending 放在 `active_schedule_sessions` 里管理。
3. newAgent `PendingInteraction` 可以借鉴交互协议和 UI 体验，但不能决定主动调度 graph 如何恢复。

### 4.4 聊天管辖边界

主动调度 session 和聊天 timeline 是两本账：

```text
conversation / timeline：
记录用户和 AI 看得见的对话内容。

active_schedule_sessions：
记录这段对话对主动调度流程意味着什么状态变化。
```

因此，在 `waiting_user_reply / rerunning` 阶段会出现“双写”，但不是重复保存同一份消息：

1. timeline 写入用户可见消息。
2. session 写入主动调度状态流转。
3. 用户正在聊天页等待新方案时，session 推进、graph 重跑、preview regenerated 必须走当前请求内同步 / SSE 主路径，不能只丢 outbox 后结束请求。

路由管辖边界：

```text
waiting_user_reply / rerunning：
    active_schedule_sessions 有路由管辖权。
  用户输入不进入普通 newAgent，先用于补信息、重跑主动调度；偏好类表达留到解除锁定后再走 newAgent 的 memory / execute 链路。

ready_preview：
  解除硬管辖。
  用户输入进入普通 newAgent，但请求里可以携带 active_session 上下文。

applied / ignored / expired / failed：
  session 只做审计和历史引用，不再拦截后续聊天。
```

也就是说：

1. `active pending` 只表示是否拦截聊天输入。
2. `active context` 表示是否把这次主动调度 preview / session 注入给 newAgent 参考。
3. pending 结束后，context 可以继续存在，但它没有路由管辖权。

session 表的 outbox 口径：

1. 第一版不新增 `active_schedule.session.reply_received` 作为主驱动事件。
2. 用户补信息后的主流程必须同步完成：

```text
写入 timeline
  -> 更新 session.status = rerunning
  -> 解析补充信息 / 更新本轮上下文
  -> 重跑 active scheduler graph
  -> 生成新 preview
  -> SSE 推送新方案卡片
  -> 更新 session.status = ready_preview
```

3. outbox 只用于已有异步副作用或兜底可靠性，例如：
   - timeline 持久化。
   - memory 长期抽取 / 写入。
   - agent 状态快照。
   - notification 投递。
   - 同步处理失败后的 failed 审计或后续人工重放。
4. session 也要接入缓存链路：chat 路由按 `user_id + conversation_id` 先查 session 热缓存，miss 再回源 DB，并在 `status / current_preview_id / conversation_id` 变化后同步回填缓存。
5. 构造用户消息时，要把 `active_schedule_sessions + preview + pending_question` 组装成可直接复用的消息快照并写入缓存，避免每次都从 DB 重组同一份主动调度上下文。
6. session 创建、绑定 `conversation_id`、释放路由管辖权都优先同步写表，不单独事件化。
7. 后续如果要补完整审计，可以再考虑 session event log，但不作为 MVP 主链路依赖。

### 4.5 前端需要补的适配层

当前 `AssistantPanel.vue` 已有：

1. SSE `extra` 事件处理。
2. `confirm_request` 覆盖层。
3. `schedule_completed` 卡片。
4. `ScheduleResultCard`。
5. `ScheduleFineTuneModal`。
6. `extra.resume` 发送协议。

但它现在缺少：

1. 从 URL / route query 初始化 active schedule session。
2. 拉取 `GET /api/v1/active-schedule/preview/:preview_id`。
3. 把 `ActiveSchedulePreviewDetail` 转成前端可展示的主动调度卡片。
4. 主动调度 preview 和 newAgent `SchedulePreviewData` 的 DTO 适配边界。
5. `ask_user` 锁定态输入框提示和发送协议。
6. 主动调度 confirm 应调用 `/active-schedule/preview/:preview_id/confirm`，不能走 newAgent 普通 schedule preview 保存接口。

### 4.6 后端需要补的合流点

后端需要补：

1. 飞书 action_url 指向现有聊天页，而不是不存在的 `/schedule-adjust/{preview_id}`。
2. `notification.FeishuNotificationRequestedPayload.Validate` 不应长期硬编码 `/schedule-adjust/`，应允许 `/assistant/{conversation_id}`。
3. active scheduler session 需要能在通知前绑定 `conversation_id`。
4. newAgent 入口需要识别 active scheduler 上下文：
   - 信息完整：注入 preview / candidate / constraints。
   - 等待补信息：拦截普通聊天，先完成 active `ask_user`。
 5. 用户在聊天中给出的偏好，如果已经处于自由聊天链路，就交给现有 newAgent memory / execute 处理；只有主动调度所需的缺失事实才会让 session 继续占管。

### 4.7 与现有 execute 链路的接缝

主动调度不复制 `task_item`，也不另起一套排程写入链路。

最小接法：

1. 主动调度只负责在会话层生成 preview 和确认态，不直接改正式日程。
2. 前端拖拽和确认继续复用现有会话快照语义，后端只更新同一份 `ScheduleState`。
3. 确认通过后，再把这份状态的 diff 落成正式 `schedule_events / schedules`。
4. 会话释放后，普通聊天回到 newAgent `execute`，继续在同一份 `ScheduleState / OriginalScheduleState` 上做 `move / swap / place / unplace`。

所以新增量是 session、路由拦截、preview DTO 适配和 graph 裁决，不是新建排程引擎，也不是把待调整任务复制成新任务再排一遍。

## 5. 飞书 Webhook 与消息拼装

当前真实飞书走“用户级 Webhook 触发器”，后端向用户配置的 webhook POST 极简业务 JSON。

当前 payload 形态可以保留：

```json
{
  "event": "smartflow.schedule_adjustment_ready",
  "version": "1",
  "notification_id": 123,
  "user_id": 5,
  "preview_id": "asp_xxx",
  "trigger_id": "ast_xxx",
  "trigger_type": "important_urgent_task",
  "target_type": "task_pool",
  "target_id": 81,
  "message": {
    "title": "SmartFlow 日程调整建议",
    "summary": "把重要且紧急任务放入滚动 24 小时内的空闲节次。",
    "action_text": "查看并确认调整",
    "action_url": "http://localhost:5173/assistant/conv_xxx"
  },
  "trace_id": "trace_xxx",
  "sent_at": "2026-04-30T17:34:52+08:00"
}
```

消息拼装原则：

1. 飞书流程只需要读 `message.title / summary / action_text / action_url`。
2. 业务分支读顶层 `event / version / trigger_type / preview_id / trigger_id`。
3. 不把复杂卡片协议塞进 webhook payload。
4. 私聊、群聊、机器人卡片由飞书流程自行编排。
5. 本地开发用 `notification.frontendBaseURL=http://localhost:5173`。
6. 未上线前，示例配置也应写 localhost；上线后再替换正式域名。

## 6. 第四、第五阶段链路扫点

### 6.1 已经比较实的部分

1. `active_schedule.triggered` handler 已能消费 outbox 并推进 trigger 状态。
2. due job scanner 已能把到期 job 转成正式 trigger。
3. preview 写入、详情查询、confirm apply 已有完整后端链路。
4. notification service 已有记录表、状态机、去重、retry、dead/skipped 分类。
5. WebhookFeishuProvider 已经接用户级 webhook 配置，并能真实 POST 飞书 webhook。
6. API-only / worker-only / all 启动边界已经验过。
7. `important_urgent_task` 和 `unfinished_feedback` 的基础端到端已经跑通。

### 6.2 仍是空壳或半空壳的部分

1. LLM selector 空壳：
   - `LLMSelectionRequired=true` 只是标志。
   - 没有选择题 prompt、模型调用、解析、重试和 fallback 分支。
2. LLM summary 空壳：
   - notification summary 当前来自 selected candidate summary。
   - 没有真正“LLM 生成通知摘要，模板兜底”的实现。
3. 候选搜索空壳：
   - 当前基本是 first-fit top1。
   - 没有 topN、维度评分、health before/after。
4. memory 接入空壳：
   - active scheduler 不读取 memory。
   - 用户偏好只会在 newAgent 普通聊天链路里发挥作用。
5. `ask_user` 闭环空壳：
   - active scheduler 能产出 `ask_user` 类型，但没有用户回复入口、session、重跑 graph。
6. 聊天页合流空壳：
   - 前端没有 active preview route/query 初始化。
   - 没有主动调度 preview DTO 到聊天卡片 / 微调弹窗的适配。
7. 飞书 action_url / 校验口径待收口：
   - 主入口已经统一到 `/assistant/{conversation_id}`，不再把 `/schedule-adjust/{preview_id}` 当成新目标。
   - 后端仍要保证在发通知前能从 session 生成最终会话链接。
   - shared event 校验和前端适配仍需继续收尾，不能把旧路径当成主链路。
8. 未完成反馈自然语言定位空壳：
   - 当前更依赖 trigger 直接给 `schedule_event` target。
   - 随口记“哪个没做完”的定位链路还没接。
9. `unfinished_feedback` 补做节数口径仍粗：
    - 比例推断不再作为当前主路径。
   - 当前更应先补“定位目标 + 必要时 ask_user”，补做节数先按原任务长度、用户明确剩余内容和可用容量保守估算。
10. 压缩融合空壳：
    - schema / 常量存在。
    - 生成逻辑明确关闭。
11. 前端主动调度确认体验空壳：
    - 后端 confirm API 存在。
    - 但聊天页还没把主动调度卡片的确认按钮接到该 API。
12. 剩余验收缺口：
    - confirm apply 冲突失败。
    - preview 过期拒绝。
    - 更系统的失败注入脚本化。
    - 测试数据隔离策略。

### 6.3 不是空壳，但需要改口径的地方

1. 当前实施口径已经统一为“进入聊天页承接主动调度会话”；文档或历史记录里若出现 `/schedule-adjust/{preview_id}`，只能作为旧口径残留，不能作为新实现目标。
2. 通知事件 `target_url` 的校验规则要允许站内聊天页路径，主入口使用 `/assistant/{conversation_id}`，不要再新增独立 schedule-adjust 页面。
3. 前端 `ScheduleFineTuneModal` 当前绑定 newAgent preview 数据结构，不能直接复用 active preview DTO，必须加适配层。

### 6.4 本轮前置条件：estimated_sections 写入链路补齐

这件事属于任务创建 / 随口记写入链路，不属于 active scheduler graph 内部逻辑；但它是本轮缺口补齐方案的前置条件，应该单独拎出来做。

目标口径：

1. LLM 在创建 task_pool 任务时，除了 title、priority、deadline、urgency_threshold_at，也要给出预计占用节数。
2. 预计占用节数写入 `tasks.estimated_sections`，范围仍按 MVP 约定限制为 1~4。
3. 主动调度只消费 `tasks.estimated_sections`，不在调度阶段重新推断任务复杂度。
4. 如果 LLM 没给、解析失败或超出范围，写入链路兜底为 1 节，并记录 trace / warning，避免阻断任务创建。

当前代码现状：

1. `model.Task` 已经有 `EstimatedSections` 字段。
2. active scheduler 已经读取并消费该字段。
3. 普通任务创建请求 `UserAddTaskRequest`、转换层和 quick task 创建入口还没完全透传该字段。

本轮建议改造点：

1. 给 `UserAddTaskRequest` 增加 `estimated_sections` 入参，并在转换层写入 `model.Task.EstimatedSections`。
2. 给 quick task 创建依赖增加 estimated sections 参数，让 newAgent / 随口记创建任务时能把 LLM 判断结果带进 DB。
3. 对入口值做 1~4 的统一归一化，避免每条写入链路各自截断。
4. 更新相关响应或查询 DTO 时，至少让调试 / 验收能看到任务最终写入的 estimated sections。

## 7. 下一轮讨论顺序

上面两项只是聊天页合流和验收尾项，不代表完整实施顺序。完整开工顺序必须先把主动调度 graph 补回来，否则只是给当前固定 pipeline 包一层 UI。

完整实施顺序建议：

1. 先补 `estimated_sections` 写入入口，保证 task 创建 / 随口记创建任务时已经带预计节数。
2. 补主动调度 Eino graph：把现有 `BuildContext -> Observe -> GenerateCandidates -> CreatePreview` 包成可继续扩展的 graph，并新增 LLM 选择题 / ask_user 分支。
3. 升级候选生成与裁决：从 first-fit top1 走向 topN 候选、维度信息、memory 输入和后端 fallback。
4. 补 `active_schedule_sessions` 和聊天入口拦截，让 `ask_user` 回复能同步重跑 graph。
5. 补 active preview 到聊天页卡片 / 微调弹窗的前端 DTO 适配。
6. 跑第五阶段剩余验收项：冲突、过期、失败注入脚本化。

对应到《第二阶段主动调度 MVP 实现方案》里，1 属于本轮前置条件，2-4 属于第六阶段主动调度 graph 与会话桥，5 属于第六阶段聊天页适配，6 属于第十四章剩余验收。

`unfinished_feedback` 的主口径也已经基本拍板，不再作为下一轮主讨论项：

1. 定位靠 LLM 上下文推断 + 当前时间 + 已排日程窗口。
2. 定位不稳就 `ask_user`，由 `missing_info` 决定缺什么问什么。
3. 定位成功后生成补做 preview，不直接移动原任务。

`estimated_sections` 写入入口补齐是本轮前置条件，见 6.4；它单独属于任务创建 / 随口记写入链路，不混进主动调度 graph 设计里。

`waiting_user_reply / rerunning` 阻塞态的聊天入口拦截协议已经拍板：

1. 后端在 chat 路由按 `active_schedule_sessions` 状态拦截。
2. `waiting_user_reply / rerunning` 时，用户消息先进入主动调度补信息链路，不进入普通 newAgent 自由聊天。
3. `ready_preview / applied / ignored / expired / failed` 才释放回普通聊天链路。
4. 这一块后续只补接口返回码、错误提示和前端文案，不再作为下一轮主讨论项。

`estimated_sections` 写入入口补齐优先补 `UserAddTaskRequest` 和 quick task 创建入口对该字段的透传，不要让主动调度侧重新猜一遍。

已基本定稿、不再占用下一轮主讨论的内容：

1. `active_schedule_sessions` 表字段和同步状态机，当前先按极简版 `session_id / user_id / conversation_id / trigger_id / current_preview_id / status / state_json / created_at / updated_at` 落地。
2. 候选公开维度字段，当前先按 `capacity_fit / risk_level` 执行；事实可信度只作为内部 trace / `ask_user` 门控，不进入 LLM 候选维度。
3. LLM 选择题 JSON 协议，当前先按 `action / selected_candidate_id / reason / user_message_summary / ask_user_question` 执行。

如果你想继续把某一项再收紧，优先补拍板：

1. `state_json` 里面到底要不要再拆成独立列。
2. `ask_user_question` 是否必须非空。
3. `reason` 是给内部调试看，还是也要直接展示给用户。
