Files
smartmate/docs/frontend-schedule-integration.md
Losita 668af5f6c0 Version: 0.9.31.dev.260419
后端:
1. 日程暂存接口——前端拖拽调整后保存到 Redis 快照
  - api/agent.go:新增 SaveScheduleState handler,解析绝对时间格式请求体,3 秒超时保护
  - routers/routers.go:注册 POST /schedule-state
  - model/agent.go:新增 SaveScheduleStatePlacedItem / SaveScheduleStateRequest 结构体
  - respond/respond.go:新增 5 个排程状态错误码(40058~40062)
  - 新增 service/agentsvc/agent_schedule_state.go:Load 快照 → ApplyPlacedItems → Save 回 Redis,校验归属
  - 新增 newAgent/conv/schedule_state_apply.go:ApplyPlacedItems 绝对坐标→相对 day_index 转换,去重/坐标/嵌入关系校验
2. SchedulePersistor 持久化层全面下线
  - 删除 newAgent/conv/schedule_persist.go(280 行,DiffScheduleState → applyChange → 事务写库整条链路)
  - model/state_store.go:移除 SchedulePersistor 接口
  - model/graph_run_state.go / node/execute.go / node/agent_nodes.go / service/agent.go / service/agent_newagent.go /
  cmd/start.go:移除 SchedulePersistor 字段、参数、注入六处
3. schedule_completed 事件推送——deliver 节点排程完毕信号
  - model/common_state.go:新增 HasScheduleChanges 标记,ResetForNextRun 清理
  - node/execute.go / node/rough_build.go:写工具和粗排成功后置 HasScheduleChanges=true
  - node/deliver.go:IsCompleted && HasScheduleChanges 时调用 EmitScheduleCompleted
  - stream/emitter.go:新增 EmitScheduleCompleted 方法
  - stream/openai.go:新增 StreamExtraKindScheduleCompleted + NewScheduleCompletedExtra
4. 预览接口补全 task_class_id
  - model/agent.go:GetSchedulePlanPreviewResponse 新增 TaskClassIDs
  - model/schedule.go:HybridScheduleEntry 新增 TaskClassID
  - conv/schedule_preview.go / service/agent_schedule_preview.go / service/schedule.go:三处透传填充
前端:
5. 排程完毕卡片 + 精排弹窗集成
  - 新增 api/schedule_agent.ts:getSchedulePreview / saveScheduleState / applyBatchIntoSchedule
  - types/dashboard.ts:新增 HybridScheduleEntry / SchedulePreviewData / PlacedItem 类型
  - components/dashboard/AssistantPanel.vue:监听 schedule_completed 事件异步拉取排程渲染卡片,集成 ScheduleResultCard + ScheduleFineTuneModal;confirm 交互从文本消息改为 resume 协议(approve/reject/cancel)
6. ToolTracePrototypeView 原型页新增日程小卡片 + 拖拽编排弹窗演示
7. DashboardView import 区域尺寸微调
2026-04-19 13:53:07 +08:00

654 lines
17 KiB
Markdown
Raw 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.
# 日程卡片前端集成文档
本文档描述前端实现"排程结果卡片 + 暂存/写库按钮"所需的全部接口、数据结构和交互流程。
---
## 一、整体交互流程
```
用户发消息 → 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 }
```