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

22 KiB
Raw Blame History

主动调度缺口分阶段实施计划

本文档用于把《第二阶段主动调度 MVP 实现方案.md》《主动调度候选生成器讨论稿.md》和当前代码仓库的实际状态收口到一份可执行的推进计划里。

目标只有一个:把主动调度剩下的缺口按阶段补完,并且每个阶段都能明确验收、明确自动化边界、明确是否已经完成。后续我会在这里持续把 [ ] 改成 [x]


0. 当前仓库基线

先把现在已经有的和还缺的分开,避免后面阶段定义漂移。

已经落地的基座

  • backend/active_scheduler 已经形成准独立模块,包含 context / observe / candidate / preview / apply / service / job 等目录。
  • dry-run -> trigger -> preview -> confirm -> apply 主链路已经存在。
  • active_schedule.triggerednotification.feishu.requestednotification_records、用户级飞书 webhook 配置接口已经打通。
  • active_schedule_previewsschedule_events.task_source_type / makeup_for_event_id / active_preview_idtasks.estimated_sections 这些模型层字段已经存在。
  • api / worker / all 三种启动边界已经有实测基础。
  • important_urgent_taskunfinished_feedback 的主触发链路已经跑过一轮端到端。

近期缺口收口状态

  • UserAddTaskRequest、转换层、quick task / 随口记创建入口已完整透传 estimated_sections
  • CreatePreview 已切到 graph + 受限 selector不再是固定 top1 / Candidates[0]
  • active_schedule_sessions 已正式进入代码,并接好缓存链路。
  • 聊天入口已按 session 状态拦截,waiting_user_reply / rerunning 会接管补信息链路。
  • unfinished_feedback 的“定位 -> ask_user -> 重跑 graph”闭环还没完全做实。
  • 聊天页里的主动调度 preview 卡片 / 微调弹窗还没有最小适配。
  • 剩余极限验收项还没完全脚本化。

代码锚点

后续实施时优先看这些位置:

  • backend/model/task.goTask.EstimatedSections 已存在,普通创建请求已接入 estimated_sections
  • backend/conv/task.go:任务创建请求转模型时已透传预计节数。
  • backend/cmd/start.goquick task 创建依赖已透传预计节数;主动调度 graph runner / LLM selector / session rerun 也已在启动期装配。
  • backend/active_scheduler/preview/service.gopreview 已支持 SelectedCandidateID / ExplanationText / NotificationSummary / FallbackUsed
  • backend/active_scheduler/graph:阶段 1 graph runner 已落地;backend/active_scheduler/selection 承载 LLM selector / prompt / DTO。
  • backend/active_scheduler/service/trigger_pipeline.gotrigger workflow 已调用 graph result再写 preview 和 notification。
  • backend/service/agentsvc/agent_newagent.gobackend/service/agentsvc/agent_active_schedule_session.gobackend/api/agent.go:聊天入口和 graph 执行边界已接 session 管辖。
  • frontend/src/components/dashboard/AssistantPanel.vue:主动调度卡片与确认按钮做最小分支。
  • backend/notification:飞书 webhook provider、notification 状态机和重试逻辑的已有基座。

与 execute 链路的接缝

主动调度不复制 task_item,也不新建一套排程写入链路。

最小接入方式:

  1. 主动调度 session 只负责识别当前会话是否处于主动调度占管态,并生成待确认 preview。
  2. preview / 微调 / 确认继续复用现有会话维度的 ScheduleState 快照;前端拖拽仍走现有暂存语义,后端只更新同一份状态。
  3. 用户确认后,后端按 preview / edited changes 做重校验并正式写入 schedule_events / schedules
  4. 主动调度释放占管后,后续普通聊天直接回到 newAgent executeexecute 继续在 ScheduleState / OriginalScheduleState 上调用现有 move / swap / place / unplace 等工具。

因此,本轮新增的是 session、路由拦截、preview DTO 适配和 graph 裁决;不是新增排程引擎,也不是把待调整任务复制成新任务再排一次。

平滑接回聊天的边界:

  1. graph 不直接进入 newAgent execute,也不把主动调度包装成 ReAct 工具。
  2. graph 结果由后端转换成 conversation timeline追问写 assistant_text,预览写 business_card.card_type=active_schedule_preview
  3. waiting_user_reply / rerunning 期间chat 入口拦截用户消息并同步推进 graphready_preview 或终态后session 释放,普通聊天自然回到 newAgent。

1. 分阶段实施总表

阶段 状态 目标 验收点 自动化测试
阶段 0 [x] estimated_sections 写入入口 创建任务时能稳定写入 1~4 节,主动调度只消费落库值 可以API + DB + go test
阶段 1 [x] 补主动调度 Eino graph 和 LLM 解释 / 补全兜底 产生候选、有限裁决、输出解释、保留 fallback 可以,后端单测 + API 验证
阶段 2 [x] active_schedule_sessions、聊天拦截和缓存链路 waiting_user_reply / rerunning 拦截生效,ready_preview 释放 可以API + DB + 路由验证
阶段 3 [ ] unfinished_feedbackask_user 闭环和前端最小适配 用户在聊天页补信息后能重跑 graph 并刷新 preview 后端可自动,前端需浏览器验证
阶段 4 [ ] 收口飞书通知与会话链接 action_url 指向 /assistant/{conversation_id},通知 payload 从简 可以webhook POST + DB 验证
阶段 5 [ ] 跑完第五阶段剩余验收和失败注入脚本 冲突、过期、重复确认、重试、dead/skipped 全覆盖 可以,基本全自动

2. 阶段细则

阶段 0estimated_sections 写入入口

当前状态

model.Task.EstimatedSections 已经有了,主动调度消费侧也已经接上,这一阶段的写入侧已经补完,并完成 API、聊天 quick task 和数据库三方验收,可以收口。

已完成内容

  1. UserAddTaskRequest 增加 estimated_sections
  2. backend/conv/task.gobackend/cmd/start.go 和 quick task 创建入口里把这个值写入 model.Task
  3. 做统一归一化,缺失或非法时兜底为 1,超过上限收敛到 4
  4. 让 newAgent / 随口记创建任务时能把 LLM 估计结果带入 DB。
  5. 让任务查询 DTO 和 quick task 卡片也能回显这个字段,方便验收。

验收点

  1. 任务创建后,查询结果里能看到最终写入的 estimated_sections
  2. 缺失或越界值会被收敛到 1~4
  3. 主动调度不再在 graph 内重新猜任务耗时。
  4. 旧任务和历史数据仍能按默认 1 兼容。

验证记录

  1. 已执行 go test ./...
  2. 已清理本次测试生成的 .gocache / .gopath
  3. 普通 POST /api/v1/task/create + GET /api/v1/task/get + MySQL 对账已通过,返回和落库均为 estimated_sections=3
  4. POST /api/v1/agent/chat 已验证可创建任务,tasks 表中 id=145 的记录落库为 title=交实验报告priority=1estimated_sections=1deadline_at=2026-05-02 20:00:00
  5. 当前仓库没有保留临时 *_test.go

自动化测试

  • 可以自动跑。
  • 建议路径:任务创建 API + DB 断言 + go test ./...
  • 若当轮实现了输入输出纯函数,临时单测可写完即删,测试后清理 *_test.go

阶段 1补主动调度 Eino graph 和 LLM 解释 / 补全兜底

当前状态

现在 graph / selector 已经接上,核心链路是 BuildContext -> Observe -> GenerateCandidates -> SelectAndExplain -> CreatePreview,不再停留在固定 top1 过渡实现。

收口状态

  1. graph 已从固定 pipeline 升级为可复用 runner。
  2. LLM 只在受限候选里做有限选择,后端 fallback 仍保留。
  3. 这一阶段已经从“待做”转为“已完成”,后续只保留后续调优项。

已完成内容

  1. 把现有 pipeline 整成可扩展 graph物理位置固定在 backend/active_scheduler/graph,不放进 newAgent
  2. 把 LLM 放到“只读 / 受限工具视图”里后端仍是候选合法性、粗排和默认裁决的主责任方LLM 只做有限选择、解释生成和信息补全兜底。
  3. 给 LLM 的输入分两层:
    • 基础信息:trigger_type / target / time_window / missing_info / warnings / before-after 摘要
    • 候选层:candidate_id / candidate_type / summary / preview_change / dimensions
    • 不暴露原始全量事实快照
  4. 增加选择题协议:
{
  "action": "select_candidate",
  "selected_candidate_id": "xxx",
  "reason": "选择原因",
  "user_message_summary": "给用户看的简短解释",
  "ask_user_question": ""
}
  1. 支持 select_candidate / ask_user / notify_only / close
  2. 保留后端 fallbackLLM 超时、输出非法、候选不存在时回落到后端粗排结果;事实不足时回落到后端 ask_user 问题。
  3. 模型接入只走一次同步 JSON 调用:启动期复用 inits.InitEino() 里的 aiHub.Pro,再包装成 backend/infra/llm.Client,由 backend/active_scheduler/selection 层调用 GenerateJSON;默认不走流式、不走 ReAct、不开放工具。
  4. 候选维度只保留真正有用的最小集:
    • capacity_fit
    • risk_level
  5. confidence 不作为 LLM 可见候选维度;事实可信度只用于内部 trace / ask_user 判断,低可信事实不生成正式候选。
  6. 明确不把这些东西重新做成第一版主维度:
  • deadline_fit
  • user_preference_fit
  • disruption
  • reversibility
  • explainability

验收点

  1. dry-run 和正式 trigger 都能走到 graph。
  2. graph 能产出多个合法候选,而不是只剩一个 first-fit。
  3. LLM 不直接写正式日程参数,也不替代后端粗排;它的主要价值是解释、在接近候选间有限裁决、以及事实不足时生成更自然的追问。
  4. LLM 能看到基础上下文和候选摘要,但不会拿到原始全量快照;候选维度只公开 capacity_fit / risk_level
  5. ask_user 只有在信息不足时才出现。
  6. compress_with_next_dynamic_task 仍然默认关闭,不在这阶段打开。
  7. LLM selector 能被 fake selector 替换方便单测覆盖“选第二个候选、输出非法、fallback 命中”这几类情况。

自动化测试

  • 可以自动跑。
  • 建议路径候选过滤单测、选择题解析单测、dry-run API 验证、trigger 端到端验证。
  • 如果 Eino graph 需要调包,实现时要先对照官方文档,再落代码。

接入方向

这层 graph 放在 backend/active_scheduler/graph,由 active scheduler 自己持有业务编排;newAgent 不拥有 graph只在聊天入口被 session 拦截时同步调用 active scheduler service。后续如果拆微服务把 graph runner 换成 RPC 同步调用即可不改聊天、worker、API 三个入口的业务口径。

阶段 1 的落地顺序:

  1. 先落 graph / selection 包,把 BuildContext -> Observe -> GenerateCandidates -> SelectAndExplain 这条链串起来。
  2. 再把 TriggerWorkflowService 从固定 pipeline 切到 graph runner确保 dry-run / trigger 先吃到新裁决结果。
  3. 然后改 preview.CreatePreviewRequestpreview.Service,让 SelectedCandidateID / ExplanationText / NotificationSummary / FallbackUsed 真正进 preview。
  4. 最后在 cmd/start.go 做装配,把 aiHub.Pro -> llm.Client -> selector -> graph runner 串进启动期依赖图。
  5. 完成后先用 API dry-run 和 trigger 验证,再去看 preview detail 是否正确回显 selected candidate。

阶段 2active_schedule_sessions、聊天拦截和缓存链路

当前状态

active_schedule_sessions、session bridge、聊天入口拦截和缓存回填都已经接上waiting_user_reply / rerunning 会先由主动调度接管,ready_preview 之后再释放回普通聊天链路。

收口状态

  1. session 表已落地,session_id / user_id / conversation_id / trigger_id / current_preview_id / status / state_json 这些核心字段都在代码里。
  2. 后端 chat 路由已按 session 状态拦截,不会再把占管态消息误进自由聊天。
  3. Redis + MySQL 的双写回填链路已经打通session 状态可回源、可回填。
  4. 这一阶段可以收口,后续只继续补 unfinished_feedback 闭环和前端最小适配。

已完成内容

  1. 新增 active_schedule_sessions
  2. 先把字段压到最小够用集:
字段 含义
session_id 主动调度会话主键
user_id 归属用户
conversation_id 绑定到现有聊天会话
trigger_id 这次主动调度触发来源
current_preview_id 当前正在展示 / 等待处理的 preview
status 会话状态
state_json 轻量业务状态,例如 pending_question / missing_info / last_candidate_id / last_notification_id / expires_at / failed_reason
created_at 创建时间
updated_at 更新时间
  1. 状态只保留这几种:
    • waiting_user_reply
    • rerunning
    • ready_preview
    • applied
    • ignored
    • expired
    • failed
  2. 聊天入口在后端直接查 session 状态:
    • waiting_user_reply / rerunning:拦截,先走主动调度补信息链路。
    • ready_preview / applied / ignored / expired / failed:释放给普通聊天。
  3. session 和 conversation/timeline 分开记账:
    • timeline 记用户可见对话。
    • session 记主动调度路由管辖权。
  4. session 要接缓存链路:
    • 先查热缓存。
    • miss 再回源 DB。
    • 状态变化后同步回填。
    • 构造用户消息时把 session / preview / pending question 一起缓存好。
  5. ask_user pending 不复用 newAgent PendingInteraction 作为状态源,只借鉴交互协议。
  6. outbox 不作为用户回复重跑 graph 的主驱动;主路径必须同步完成,再给 SSE / timeline 更新。
  7. graph 结果接回现有聊天协议:
    • ask_user:写 assistant_textsession 继续 waiting_user_reply
    • select_candidate / ready_preview:写 assistant_text + business_card.card_type=active_schedule_previewsession 进入 ready_preview
    • close / notify_only / failed:写 assistant_textsession 进入终态并释放普通聊天。

验收点

  1. 同一个 conversation 进入聊天页时,能按 session 状态正确拦截或放行。
  2. waiting_user_reply / rerunning 状态下,用户消息不会直接滑进普通自由聊天链路。
  3. session 状态变化后,缓存和 DB 一致。
  4. 关键审计链能串起来:trigger_id -> preview_id -> conversation_id -> apply_id
  5. 用户刷新会话历史时,主动调度追问和 preview 卡片能从 timeline 重建,不依赖 SSE 临时状态。

自动化测试

  • 可以自动跑。
  • 建议路径API + DB 断言、路由拦截测试、缓存命中/回源测试。
  • 需要你先把对应后端服务按模式起好时,我可以直接接着跑。

阶段 3unfinished_feedbackask_user 闭环和前端最小适配

当前状态

unfinished_feedback 目前还偏向“已有目标就能做”,但“定位不稳怎么办、用户回一句怎么办、如何重跑 graph”还没有完全闭环。

要做什么

  1. 定位逻辑按这个顺序走:
    • LLM 上下文推断
    • 当前时间
    • 已排日程窗口
  2. 定位成功后直接出补做 preview不移动原任务。
  3. 定位失败时,不硬猜,直接 ask_user
  4. ask_user 的问题由 missing_info 驱动,缺什么问什么。
  5. 用户在聊天页补全的是当前主动调度缺失事实;偏好类表达不属于 ask_user 的缺失项,解锁后直接回到现有 newAgent memory / execute 链路,不在主动调度里新建记忆写入链路。
  6. 前端只做最小分支:
    • 复用 AssistantPanel.vue
    • 复用现有 ScheduleResultCard
    • 复用现有 ScheduleFineTuneModal
    • 只是把 timeline 新类型和主动调度 confirm API 接起来
  7. 后端负责把主动调度 preview DTO 转成前端容易复用的结构,前端不背脏活。

验收点

  1. 用户补完当前主动调度缺失事实后,能刷新 preview 并解除锁定;解锁后再说“我周末不想学习”这类偏好话术时,直接走现有 newAgent memory / execute 链路。
  2. ask_user 流程没走完时,不能直接进入普通聊天链路。
  3. 补做块只新建,不移动原任务。
  4. 主动调度卡片能在聊天页显示,微调后走主动调度 confirm API。

自动化测试

  • 后端部分可以自动跑。
  • 前端卡片展示和按钮分支,建议用浏览器实际打开一次做可视确认。
  • 如果只是检查 DOM / 路由 / 请求是否发对,能自动;如果要看卡片样式是否真的对齐,还是需要浏览器看一眼。

阶段 4收口飞书通知与会话链接

当前状态

用户级 webhook 配置、通知投递、测试接口已经有基础,但主入口还需要统一收口到聊天会话链接,不能再把旧的 /schedule-adjust/{preview_id} 当新目标。

要做什么

  1. 通知前先绑定或预创建 conversation_id
  2. action_url 统一走:
/assistant/{conversation_id}
  1. 本地测试和示例配置继续用 localhost,上线后再换正式域名。
  2. 业务 JSON 保持从简,只让飞书流程去编排消息,不把复杂卡片协议塞进 webhook。
  3. 维持当前通知状态机:
    • sent
    • failed
    • dead
    • skipped
    • retry 相关状态

建议 payload 形态

{
  "event": "smartflow.schedule_adjustment_ready",
  "version": "1",
  "notification_id": 123,
  "user_id": 5,
  "preview_id": "asp_xxx",
  "conversation_id": "conv_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. 通知里的跳转链接能直接进聊天页。
  2. 用户级 webhook 的保存、查询、删除、测试都能跑通。
  3. 未配置、临时失败、不可恢复失败的状态都能在 notification_records 里看见。
  4. 用户已经在聊天页时,不再强依赖飞书通知承接回复。

自动化测试

  • 可以自动跑。
  • 建议路径Webhook POST、测试接口、notification_records 状态断言、真实 webhook 收到后人工看一次消息。
  • 如果需要验证“飞书真的收到”,最终还是要看外部页面一次,但 HTTP 层和状态层可以自动。

阶段 5跑完第五阶段剩余验收和失败注入脚本

当前状态

主链路已经有了,但极限边界还需要系统化收口。

要做什么

  1. 跑通 api / worker / all 三种启动模式。
  2. 覆盖以下边界:
    • 冲突失败
    • preview 过期
    • 重复 confirm
    • trigger 幂等
    • notification retry
    • dead / skipped / failed
    • outbox 重复消费
  3. 把失败注入做成脚本化验收,不靠手工猜。
  4. 再扫一遍哪些地方还是空壳,哪些地方只是文档先行。

验收点

  1. 所有核心状态机都能串起来排障。
  2. 同一条 preview / notification / apply 不会被重复落库。
  3. 过期、冲突、篡改、失败注入都能拒绝。
  4. 最终能把这一轮主动调度缺口标成完成。

自动化测试

  • 可以自动跑,而且这一阶段基本就是为了自动化收口。
  • 绝大多数验收都能用 API + DB + 日志完成。

3. 已拍板、不再反复讨论的口径

这些口径已经在讨论里定过了,后面实施时按这个来,不再重开。

  1. 主动调度不做独立的 /schedule-adjust/{preview_id} 主入口,主链接统一到 /assistant/{conversation_id}
  2. active_schedule_sessions 要单独建,不塞进现有 conversation 表,也不只加在 active_schedule_previews 上。
  3. waiting_user_reply / rerunning 的聊天拦截在后端做,前端只做最小分支。
  4. ask_user pending 不复用 newAgent 的 PendingInteraction 作为状态源。
  5. compress_with_next_dynamic_task 第一版继续关闭,只保留 schema / 口径,不生成候选。
  6. 候选维度保持极简,deadline_fituser_preference_fit 不再作为第一版公开维度。
  7. 本轮不再预留独立的健康分支,候选公开维度直接按 capacity_fit / risk_level 执行;事实可信度只保留为内部 trace / ask_user 门控。
  8. unfinished_feedback 先定位,再 ask_user,定位成功后直接生成补做 preview不移动原任务。
  9. 用户在聊天页说偏好时,不归主动调度接管;解锁后直接走现有 newAgent memory / execute 链路。
  10. 只有后台离线自动触达才走飞书;用户已经在会话里时,不需要再先走飞书通知。

4. 自动化测试边界

可以自动跑的

  • go test ./...
  • dry-run / trigger / preview / confirm 的 API 验证
  • DB 结果核对
  • 幂等、重复提交、冲突、过期、失败注入
  • notification 状态流转
  • webhook test 接口
  • api-only / worker-only / all 的后端闭环

部分自动、需要浏览器或你配合开服务的

  • AssistantPanel.vue 的实际页面交互
  • 主动调度卡片是否真的长对了
  • 飞书真实消息是否真的落到外部页面
  • 需要切换启动模式时的服务重启

我会怎么标记进度

每个阶段完成后,我会把对应标题从 [ ] 改成 [x],并补三件事:

  1. 实际跑过的命令。
  2. 关键请求 / 响应 ID。
  3. DB 核对结果和剩余风险。