Files
smartmate/docs/backend/主动调度候选生成器讨论稿.md
Losita a3eaa9b2c2 Version: 0.9.61.dev.260501
后端:
1. 主动调度 graph + session bridge 收口——把 dry-run / select / preview / confirm / rerun 串成受限 graph,新增 active_schedule_sessions 缓存与聊天拦截,ready_preview 后释放回自由聊天
2. 会话与通知链路对齐——notification 统一绑定 conversation_id,action_url 指向 /assistant/{conversation_id},会话不存在改回 404 语义,避免 wrong param type 误导排障
3. estimated_sections 写入与主动调度消费链路补齐——任务创建、quick task 与随口记入口都透传估计节数,主动调度只消费落库值

前端:
4. AssistantPanel 最小适配主动调度预览与失败态——复用主动调度卡片/微调弹窗,补历史加载失败可见提示与跨账号会话拦截

文档:
5. 更新主动调度缺口分阶段实施计划和实现方案,标记阶段 0-2 收口并同步接力状态
2026-05-01 20:48:32 +08:00

704 lines
32 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.
# 主动调度缺口补全讨论稿
本文档由“主动调度候选生成器讨论稿”扩充而来,用于记录主动调度后续补全设计。
它不是实施计划,也不替代《第二阶段主动调度 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` 是给内部调试看,还是也要直接展示给用户。