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

32 KiB
Raw Blame History

主动调度缺口补全讨论稿

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

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

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

1. 当前实际链路

1.1 主动调度后端链路

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

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 候选生成器定位

候选生成器应该升级为:

安全候选工厂 + 维度评估器 + 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_onlyask_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 等候选必须先完成风险模型,再打开生成开关。

候选可编辑性建议:

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 和用户看的候选维度,只保留需要取舍、比较或解释的信息。

候选建议输出维度:

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

暂不保留的维度:

  1. explainability太像主观作文评分后端不好稳定计算LLM 也容易自证合理。
  2. reversibility:当前主动调度第一版没有撤销按钮,贸然展示“可逆性”容易误导用户;后续如果实现 undo / rollback再重新设计。
  3. disruption:当前主动调度只生成“新增任务块 / 新增补做块”,不移动已有日程,扰动度几乎恒为 none,对候选选择没有区分度;等打开移动、压缩、局部重排候选时再恢复。
  4. deadline_fitdeadline / urgency window 属于硬约束,不满足的方案不进入候选;满足后无需再作为展示维度。
  5. user_preference_fitmemory / 用户近期反馈更适合交给 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_confidenceevidence_level,但它不进入 LLM 候选维度:

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

3.4 LLM 选择题协议

LLM 可见信息建议分两层:

  1. 基础上下文:trigger_typetargettime_windowmissing_infowarningsbefore/after 摘要。
  2. 候选层:candidate_idcandidate_typesummarypreview_changedimensions,其中 dimensions 只包含 capacity_fit / risk_level
  3. 已有 memory 可以作为上下文注入,但不作为 ask_user 的缺失信息采集目标。
  4. 不暴露原始全量事实快照,避免把后端内脏直接端给模型。

LLM 输入建议:

{
  "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 输出必须限制为:

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

允许的 action

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 任务。

压缩融合的业务定义:

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

换句话说:

  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 两种进入聊天页状态

信息完整:

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

信息不完整:

飞书点击
  -> 打开聊天页
  -> 进入主动调度 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但这不代表主动调度一直拥有聊天路由管辖权。

最小需要保存:

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

状态建议:

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

飞书入口已拍板:

/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 是两本账:

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

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

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

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

路由管辖边界:

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. 用户补信息后的主流程必须同步完成:
写入 timeline
  -> 更新 session.status = rerunning
  -> 解析补充信息 / 更新本轮上下文
  -> 重跑 active scheduler graph
  -> 生成新 preview
  -> SSE 推送新方案卡片
  -> 更新 session.status = ready_preview
  1. outbox 只用于已有异步副作用或兜底可靠性,例如:
    • timeline 持久化。
    • memory 长期抽取 / 写入。
    • agent 状态快照。
    • notification 投递。
    • 同步处理失败后的 failed 审计或后续人工重放。
  2. session 也要接入缓存链路chat 路由按 user_id + conversation_id 先查 session 热缓存miss 再回源 DB并在 status / current_preview_id / conversation_id 变化后同步回填缓存。
  3. 构造用户消息时,要把 active_schedule_sessions + preview + pending_question 组装成可直接复用的消息快照并写入缓存,避免每次都从 DB 重组同一份主动调度上下文。
  4. session 创建、绑定 conversation_id、释放路由管辖权都优先同步写表,不单独事件化。
  5. 后续如果要补完整审计,可以再考虑 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 形态可以保留:

{
  "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_taskunfinished_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 是给内部调试看,还是也要直接展示给用户。