后端: 1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜 2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写 3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态 4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分 5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定 前端: 6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作 仓库: 7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件 PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
17 KiB
17 KiB
日程卡片前端集成文档
本文档描述前端实现"排程结果卡片 + 暂存/写库按钮"所需的全部接口、数据结构和交互流程。
一、整体交互流程
用户发消息 → SSE 流式返回 → 收到 schedule_completed 事件
↓
调用 schedule-preview 接口拉取排程数据
↓
渲染日程卡片(可拖拽调整位置)
↓
┌─────────────────┴─────────────────┐
│ │
"暂存 state"按钮 "写库"按钮
POST /agent/schedule-state PUT /task-class/apply-batch-into-schedule
(暂存到 Redis 快照) (真正写入 MySQL 日程表)
二、SSE 事件格式
2.1 基础 SSE 壳
所有 SSE data 行都是 JSON,格式遵循 OpenAI 兼容协议:
{
"id": "request-id",
"object": "chat.completion.chunk",
"created": 1745036800,
"model": "worker",
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"content": "正文内容",
"reasoning_content": "思考内容"
},
"finish_reason": null
}
],
"extra": { ... }
}
choices可以为空数组(纯结构化事件时)extra为可选字段,旧事件不含 extra
2.2 心跳保活
: ping
SSE 标准注释行,每 5 秒一次。前端 JSON.parse 失败后丢弃即可。
2.3 流结束标记
data: [DONE]
2.4 错误事件
{
"error": {
"message": "错误描述",
"type": "server_error",
"code": "5xxxx"
}
}
三、extra 结构化事件类型
前端通过 extra.kind 判断事件类型。
3.1 事件类型枚举
| kind | 含义 | display_mode | 说明 |
|---|---|---|---|
reasoning_text |
思考文字 | append |
逐块追加 |
assistant_text |
回复正文 | append |
逐块追加 |
status |
阶段状态 | card |
如"正在排程" |
tool_call |
工具调用开始 | card |
如"正在查询任务" |
tool_result |
工具调用结果 | card |
如"找到 3 个任务" |
confirm_request |
待确认事件 | card |
需要用户确认 |
interrupt |
中断/追问 | card |
ask_user 追问 |
schedule_completed |
排程完毕 | card |
前端拉取排程数据的信号 |
finish |
流结束 | replace |
收尾 |
3.2 status 事件
{
"extra": {
"kind": "status",
"block_id": "execute.status",
"stage": "execute",
"display_mode": "card",
"status": {
"code": "planning",
"summary": "正在智能排程..."
}
}
}
3.3 tool_call 事件(工具调用开始)
{
"extra": {
"kind": "tool_call",
"block_id": "execute.tool.1",
"stage": "execute",
"display_mode": "card",
"tool": {
"name": "smart_planning",
"status": "start",
"summary": "正在为任务类智能排程",
"arguments_preview": "任务类: [高数作业, 英语阅读]"
}
}
}
3.4 tool_result 事件(工具调用结果)
{
"extra": {
"kind": "tool_result",
"block_id": "execute.tool.1",
"stage": "execute",
"display_mode": "card",
"tool": {
"name": "smart_planning",
"status": "done",
"summary": "成功生成排程方案",
"arguments_preview": "已排 3 个任务"
}
}
}
tool.status取值:start|done|blocked|failed
3.5 confirm_request 事件(用户确认)
{
"choices": [{
"index": 0,
"delta": { "role": "assistant", "content": "请确认是否应用排程结果...\n" }
}],
"extra": {
"kind": "confirm_request",
"block_id": "execute.confirm.1",
"stage": "execute",
"display_mode": "card",
"confirm": {
"interaction_id": "confirm_abc123",
"title": "确认应用排程结果",
"summary": "是否将 3 个任务安排到日程中?"
}
}
}
前端需要:
- 保存
interaction_id - 展示确认卡片(title + summary + 确认/拒绝按钮)
- 用户点击后发送 resume 请求(见第五节)
3.6 schedule_completed 事件(排程完毕信号)
{
"extra": {
"kind": "schedule_completed",
"block_id": "deliver.schedule",
"stage": "deliver",
"display_mode": "card"
}
}
前端收到后: 用当前 conversation_id 调用 GET /agent/schedule-preview 拉取排程数据。
3.7 finish 事件
{
"choices": [{
"index": 0,
"delta": {},
"finish_reason": "stop"
}],
"extra": {
"kind": "finish",
"block_id": "deliver.finish",
"stage": "deliver",
"display_mode": "replace"
}
}
四、排程预览接口
收到 schedule_completed 事件后调用此接口获取排程数据。
GET /api/v1/agent/schedule-preview
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
conversation_id |
string | 是 | 会话 ID |
响应:
{
"status": "10000",
"info": "success",
"data": {
"conversation_id": "1655dd9b-...",
"trace_id": "trace-...",
"summary": "已为你安排了 3 个任务",
"candidate_plans": [
{
"week": 1,
"events": [
{
"id": 10,
"order": 1,
"day_of_week": 1,
"name": "高等数学",
"start_time": "08:00",
"end_time": "09:30",
"location": "教学楼A",
"type": "course",
"span": 2,
"status": "normal",
"embedded_task_info": {
"id": 100,
"name": "数学作业",
"type": "task"
}
},
{
"id": 11,
"order": 2,
"day_of_week": 1,
"name": "编程练习",
"start_time": "10:00",
"end_time": "11:30",
"location": "",
"type": "task",
"span": 2,
"status": "normal",
"embedded_task_info": {}
}
]
},
{
"week": 2,
"events": [...]
}
],
"hybrid_entries": [
{
"week": 1,
"day_of_week": 1,
"section_from": 3,
"section_to": 4,
"name": "英语阅读",
"type": "task",
"status": "suggested",
"task_item_id": 101,
"task_class_id": 5,
"event_id": 0,
"can_be_embedded": false,
"block_for_suggested": true,
"context_tag": "Memory"
},
{
"week": 1,
"day_of_week": 2,
"section_from": 1,
"section_to": 2,
"name": "高等数学",
"type": "course",
"status": "existing",
"task_item_id": 0,
"task_class_id": 0,
"event_id": 10,
"can_be_embedded": true,
"block_for_suggested": false,
"context_tag": ""
}
],
"task_class_ids": [5, 6],
"generated_at": "2026-04-19T10:00:00Z"
}
}
数据结构说明
HybridScheduleEntry(混合日程条目)
前端渲染日程卡片的核心数据结构。课程和任务统一到同一个列表中。
| 字段 | 类型 | 说明 |
|---|---|---|
week |
int | 学期周数 |
day_of_week |
int | 星期几(1=周一,7=周日) |
section_from |
int | 起始节次(1-based) |
section_to |
int | 结束节次(1-based) |
name |
string | 名称 |
type |
string | "course"(课程)或 "task"(任务) |
status |
string | "existing"(已确定)或 "suggested"(建议) |
task_item_id |
int | 任务项 ID(仅 type=task 且 status=suggested 时有值) |
task_class_id |
int | 任务类 ID(仅 type=task 且 status=suggested 时有值,对应写库接口的 task_class_id) |
event_id |
int | 日程事件 ID(仅 existing 时有值) |
can_be_embedded |
bool | 课程是否允许嵌入任务 |
block_for_suggested |
bool | 是否阻塞建议任务占位 |
context_tag |
string | 认知类型标签:"High-Logic" / "Memory" / "Review" / "General" |
渲染建议:
status=existing→ 已有课程/日程,渲染为固定色块(不可拖拽)status=suggested→ AI 建议的任务,渲染为可拖拽色块can_be_embedded=true的课程 → 任务可嵌入到其时段内
UserWeekSchedule(按周视图的已有日程)
| 字段 | 类型 | 说明 |
|---|---|---|
week |
int | 周数 |
events |
WeeklyEventBrief[] | 该周事件列表 |
WeeklyEventBrief
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | ScheduleEvent.ID |
order |
int | 天内显示顺序 |
day_of_week |
int | 星期几 |
name |
string | 名称 |
start_time |
string | 开始时间(如 "08:00") |
end_time |
string | 结束时间 |
location |
string | 地点 |
type |
string | "course" / "task" |
span |
int | 跨越节数(渲染高度) |
status |
string | "normal" / "interrupted" |
embedded_task_info |
TaskBrief | 嵌入的任务信息(可选) |
TaskBrief
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int | 关联 ID |
name |
string | 任务名称 |
type |
string | "task" |
五、用户确认流程(confirm / resume)
5.1 流程说明
当后端需要用户确认时:
- SSE 推送
kind=confirm_request事件 - 前端展示确认卡片
- 用户点击"确认"或"拒绝"
- 前端发送 resume 请求回同一聊天接口
5.2 请求格式
POST /api/v1/agent/chat(复用聊天入口)
{
"conversation_id": "1655dd9b-2c4c-4b56-a712-f34c11b2634d",
"message": "",
"extra": {
"resume": {
"interaction_id": "confirm_abc123",
"type": "confirm",
"action": "approve"
}
}
}
5.3 resume 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
interaction_id |
string | 是 | 从 confirm_request 事件中获取 |
type |
string | 否 | 默认为 "confirm",也可为 "ask_user" 或 "connection_recover" |
action |
string | 是 | 见下表 |
confirm 类型的 action:
| action | 含义 |
|---|---|
approve |
用户同意,继续执行 |
reject |
用户拒绝 |
cancel |
用户取消 |
ask_user 类型的 action:
| action | 含义 |
|---|---|
reply |
用户回答追问(回答内容放在顶层 message 字段) |
cancel |
用户取消 |
六、"暂存 state"按钮
将用户在卡片上拖拽调整后的任务位置暂存到 Redis 快照。不写 MySQL,不触发 LLM。
POST /api/v1/agent/schedule-state
请求体:
{
"conversation_id": "1655dd9b-2c4c-4b56-a712-f34c11b2634d",
"items": [
{
"task_item_id": 101,
"week": 1,
"day_of_week": 1,
"start_section": 3,
"end_section": 4
},
{
"task_item_id": 102,
"week": 2,
"day_of_week": 3,
"start_section": 5,
"end_section": 6,
"embed_course_event_id": 20
}
]
}
SaveScheduleStatePlacedItem 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
task_item_id |
int | 是 | 任务项 ID(来自 HybridScheduleEntry 的 task_item_id) |
week |
int | 是 | 学期周数(≥1) |
day_of_week |
int | 是 | 星期几(1-7) |
start_section |
int | 是 | 起始节次(≥1) |
end_section |
int | 是 | 结束节次(≥1,须 ≥ start_section) |
embed_course_event_id |
int | 否 | 嵌入目标课程的 event_id(来自 HybridScheduleEntry 的 event_id) |
安全保证: 只修改 type=task 的建议任务,课程数据永远不变。不在 items 中的任务保持原样。
成功响应
{
"status": "10000",
"info": "success",
"data": null
}
错误码
| 错误码 status | 含义 |
|---|---|
40004 |
缺少 conversation_id |
40005 |
请求体格式错误 |
40058 |
排程快照不存在或已过期(需重新对话) |
40059 |
week/day_of_week 坐标超出排程窗口范围 |
40060 |
task_item_id 在快照中不存在 |
40061 |
embed_course_event_id 在快照课程中不存在 |
40062 |
请求中包含重复的 task_item_id |
七、"写库"按钮
将任务真正写入 MySQL 日程表。需要按任务类(task_class)分组调用。
PUT /api/v1/task-class/apply-batch-into-schedule
注意: 该接口需要幂等性 Key(Idempotency-Key header),防止重复点击。
请求体:
{
"task_class_id": 123,
"items": [
{
"task_item_id": 101,
"week": 1,
"day_of_week": 1,
"start_section": 3,
"end_section": 4
},
{
"task_item_id": 102,
"week": 2,
"day_of_week": 3,
"start_section": 5,
"end_section": 6,
"embed_course_event_id": 20
}
]
}
请求字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
task_class_id |
int | 是 | 任务类 ID(来自 HybridScheduleEntry 关联的任务类) |
items |
SingleTaskClassItem[] | 是 | 放置项列表 |
SingleTaskClassItem 字段说明
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
task_item_id |
int | 是 | 任务项 ID |
week |
int | 是 | 学期周数(≥1) |
day_of_week |
int | 是 | 星期几(1-7) |
start_section |
int | 是 | 起始节次(≥1) |
end_section |
int | 是 | 结束节次(≥1,须 ≥ start_section) |
embed_course_event_id |
int | 否 | 嵌入目标课程的 ScheduleEvent.ID |
成功响应
{
"status": "10000",
"info": "success"
}
错误码
| 错误码 status | 含义 |
|---|---|
40005 |
请求体格式错误 |
40037 |
缺少 Idempotency-Key header |
40038 |
请求正在处理中(幂等性防重) |
40026 |
日程冲突 |
40025 |
课程已被其他任务块嵌入 |
40034 |
任务项已安排 |
40048 |
任务项不属于该任务类 |
40049 |
任务项时间超出学期范围 |
八、两个按钮的 items 格式完全一致
关键设计: "暂存 state"和"写库"两个按钮的 items 数据格式完全相同。
前端只需维护一份 items 数组:
- 用户拖拽调整位置 → 更新 items
- 点击"暂存" → POST /agent/schedule-state { items }
- 点击"写库" → PUT /task-class/apply-batch-into-schedule { task_class_id, items }
区别:
- "暂存"接口需要
conversation_id,"写库"接口需要task_class_id task_class_id来自HybridScheduleEntry.task_class_id(每个 suggested 任务条目都有),也可从响应顶层task_class_ids数组获取- 写库时需按
task_class_id分组:相同task_class_id的 items 放在同一个请求中 - "暂存"写 Redis 快照(可恢复),"写库"写 MySQL 日程表(持久化)
- "写库"需要
Idempotency-Keyheader 防重
九、前端实现建议
9.1 日程卡片渲染
- 用
hybrid_entries作为主数据源渲染周视图 status=existing的条目渲染为只读色块(灰色/蓝色课程块)status=suggested的条目渲染为可拖拽色块(绿色/橙色任务块)- 拖拽时校验:目标位置是否与
block_for_suggested=true的条目冲突 - 嵌入:拖拽到
can_be_embedded=true的课程块上时,设置embed_course_event_id
9.2 items 数组维护
interface PlacedItem {
task_item_id: number;
week: number;
day_of_week: number;
start_section: number;
end_section: number;
embed_course_event_id?: number;
}
// 从 hybrid_entries 中筛选 suggested 任务,构建 items
function buildItemsFromEntries(entries: HybridScheduleEntry[]): PlacedItem[] {
return entries
.filter(e => e.status === 'suggested' && e.task_item_id)
.map(e => ({
task_item_id: e.task_item_id,
week: e.week,
day_of_week: e.day_of_week,
start_section: e.section_from,
end_section: e.section_to,
embed_course_event_id: e.event_id || undefined,
}));
}
9.3 SSE 连接管理
- 使用
fetch+ReadableStream(非EventSource,因为需要 POST body) - 心跳
: ping行不是data:开头,JSON.parse会失败,直接忽略 - 错误事件格式为
{ "error": { ... } },注意与正常事件区分 - 流结束标记为
data: [DONE] X-Conversation-ID响应头包含服务端分配的 conversation_id
9.4 完整交互时序
1. 用户发送消息 → POST /agent/chat
2. SSE 流返回 thinking + 工具事件 + 正文
3. 收到 extra.kind === "schedule_completed"
4. GET /agent/schedule-preview?conversation_id=xxx
5. 渲染日程卡片
6. 用户拖拽调整任务位置 → 更新本地 items 数组
7a. 点击"暂存" → POST /agent/schedule-state { conversation_id, items }
7b. 点击"写库" → PUT /task-class/apply-batch-into-schedule { task_class_id, items }