Version: 0.9.45.dev.260427
后端: 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永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
653
docs/frontend/frontend-schedule-integration.md
Normal file
653
docs/frontend/frontend-schedule-integration.md
Normal file
@@ -0,0 +1,653 @@
|
||||
# 日程卡片前端集成文档
|
||||
|
||||
本文档描述前端实现"排程结果卡片 + 暂存/写库按钮"所需的全部接口、数据结构和交互流程。
|
||||
|
||||
---
|
||||
|
||||
## 一、整体交互流程
|
||||
|
||||
```
|
||||
用户发消息 → 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 兼容协议:
|
||||
|
||||
```json
|
||||
{
|
||||
"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 错误事件
|
||||
|
||||
```json
|
||||
{
|
||||
"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 事件
|
||||
|
||||
```json
|
||||
{
|
||||
"extra": {
|
||||
"kind": "status",
|
||||
"block_id": "execute.status",
|
||||
"stage": "execute",
|
||||
"display_mode": "card",
|
||||
"status": {
|
||||
"code": "planning",
|
||||
"summary": "正在智能排程..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 tool_call 事件(工具调用开始)
|
||||
|
||||
```json
|
||||
{
|
||||
"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 事件(工具调用结果)
|
||||
|
||||
```json
|
||||
{
|
||||
"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 事件(用户确认)
|
||||
|
||||
```json
|
||||
{
|
||||
"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 个任务安排到日程中?"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端需要:**
|
||||
1. 保存 `interaction_id`
|
||||
2. 展示确认卡片(title + summary + 确认/拒绝按钮)
|
||||
3. 用户点击后发送 resume 请求(见第五节)
|
||||
|
||||
### 3.6 schedule_completed 事件(排程完毕信号)
|
||||
|
||||
```json
|
||||
{
|
||||
"extra": {
|
||||
"kind": "schedule_completed",
|
||||
"block_id": "deliver.schedule",
|
||||
"stage": "deliver",
|
||||
"display_mode": "card"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**前端收到后:** 用当前 `conversation_id` 调用 `GET /agent/schedule-preview` 拉取排程数据。
|
||||
|
||||
### 3.7 finish 事件
|
||||
|
||||
```json
|
||||
{
|
||||
"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 |
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"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 流程说明
|
||||
|
||||
当后端需要用户确认时:
|
||||
1. SSE 推送 `kind=confirm_request` 事件
|
||||
2. 前端展示确认卡片
|
||||
3. 用户点击"确认"或"拒绝"
|
||||
4. 前端发送 resume 请求回同一聊天接口
|
||||
|
||||
### 5.2 请求格式
|
||||
|
||||
**POST `/api/v1/agent/chat`**(复用聊天入口)
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{
|
||||
"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 中的任务保持原样。
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"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),防止重复点击。
|
||||
|
||||
**请求体:**
|
||||
|
||||
```json
|
||||
{
|
||||
"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 |
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"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-Key` header 防重
|
||||
|
||||
---
|
||||
|
||||
## 九、前端实现建议
|
||||
|
||||
### 9.1 日程卡片渲染
|
||||
|
||||
1. 用 `hybrid_entries` 作为主数据源渲染周视图
|
||||
2. `status=existing` 的条目渲染为**只读**色块(灰色/蓝色课程块)
|
||||
3. `status=suggested` 的条目渲染为**可拖拽**色块(绿色/橙色任务块)
|
||||
4. 拖拽时校验:目标位置是否与 `block_for_suggested=true` 的条目冲突
|
||||
5. 嵌入:拖拽到 `can_be_embedded=true` 的课程块上时,设置 `embed_course_event_id`
|
||||
|
||||
### 9.2 items 数组维护
|
||||
|
||||
```typescript
|
||||
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 }
|
||||
```
|
||||
Reference in New Issue
Block a user