- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件 - 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow: 构建前端上传到 build/,再在服务器上 git pull 并重建容器 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.7 KiB
设计文档:学生删除 AI 对话轮次及关联提交
日期: 2026-04-14
状态: 已批准
需求
学生在挑战页的「AI 对话」tab 中,可以删除某一轮对话(一问一答),同时删除该 AI 回答自动生成的提交。反向也成立:在提交列表中删除一条提交,同时删除对应的那对消息。
数据模型
prompt/Message 新增字段:
submission = models.OneToOneField(
"submission.Submission",
null=True, blank=True,
on_delete=models.SET_NULL,
related_name="source_message"
)
OneToOneField:一条 assistant message 最多对应一个提交SET_NULL:FK 层面保底,防止外部操作引发 crash- 业务级联删除在接口层显式处理(不依赖
CASCADE)
需新建 migration。
后端
Schema 变更
api/submission/schemas.py 中 SubmissionIn 新增:
message_id: Optional[UUID] = None
接口变更
POST /submission/(create_submission)
创建 Submission 后,若 payload.message_id 不为空:
- 查找对应的 assistant Message(
source="conversation",属于当前 user+task) - 设置
message.submission = submission并保存
DELETE /submission/{id}(delete_submission)
删除 Submission 前:
- 通过
submission.source_message(related_name)找到关联的 assistant message(若有) - 找到该 assistant message 在同一 conversation 中前一条 user message(按
created排序取紧邻的上一条) - 依次删除:user message → assistant message → submission
新增 DELETE /prompt/messages/{message_id}/pair
- 鉴权:
message.conversation.user == request.user,否则 403 - 操作:
- 找到 assistant message(
id=message_id,role="assistant") - 找到同 conversation 中紧邻的前一条 user message
- 若 assistant message 有关联 submission,删除它
- 删除 assistant message 和 user message
- 找到 assistant message(
- 返回
{"deleted": true, "submission_deleted": bool}
为何用 assistant message id 而非 user message id:
complete事件由后端发出,assistant message 在那一刻刚被保存,id 唯一确定;user message 已在之前保存,前端也持有,但以 assistant message 为锚点更自然。
WebSocket 变更(prompt/consumers.py)
complete 事件新增 message_id:
await self.send(text_data=json.dumps({
"type": "complete",
"code": code,
"message_id": str(assistant_message.id), # 新增
}))
init 事件的消息列表每条带 id:
{
"id": str(m.id), # 新增
"role": m.role,
"content": m.content,
"code": {...} if m.role == "assistant" else None,
"created": m.created.isoformat(),
}
前端
store/prompt.ts
PromptMessage 接口新增:
id?: string // assistant message 的后端 id,用于删除
complete 事件处理:从事件中提取 message_id,存入 push 进 messages 的那条 assistant message。
_onCodeComplete 回调签名扩展,传入 messageId:
type OnCodeComplete = (code: { html: string | null; css: string | null; js: string | null }, messageId: string) => void
init 事件处理:将后端返回的 id 存入每条消息。
api.ts
Submission.create() 新增可选参数:
async create(taskId, code, messageId?: string)
// 请求体加上 message_id: messageId ?? null
Prompt 新增方法:
async deleteMessagePair(messageId: string): Promise<{ deleted: boolean; submission_deleted: boolean }>
ChallengeDetail.vue
setOnCodeComplete 的回调改为接收 messageId,并将其传给 Submission.create():
setOnCodeComplete(async (code, messageId) => {
await Submission.create(taskId.value, { ...code }, messageId)
})
PromptPanel.vue
消息分组: 将 messages 按 user+assistant 顺序两两分组,形成 pairs(computed)。不完整的末尾单条消息(仍在等待 AI 回复)不显示删除按钮。
删除 UI: 每个 pair 右上角显示一个垃圾桶图标(lucide:trash-2),hover 时显现。点击后:
- 调用
Prompt.deleteMessagePair(pair.assistantMessage.id) - 成功后从本地
messages中移除这两条
交互细节:
- 使用 Naive UI 的
useMessage()给出成功/失败通知 - streaming 期间禁用所有删除按钮(
disabledwhenstreaming) - 没有
id的消息(历史消息加载前的边缘情况)不显示删除按钮
不在本次范围内
- 手动提交(ExternalAIPanel)产生的消息:它们以
source="manual"存储,不在 PromptPanel 显示,无需处理 - 提交列表页的实时刷新:删除后消息已从后端移除,下次打开挑战页重连 WebSocket 时自然反映
- 管理员代替学生删除:本次仅限学生删自己的
文件改动清单
| 文件 | 类型 |
|---|---|
api/prompt/models.py |
新增 submission 字段 |
api/prompt/migrations/0005_*.py |
新建 migration |
api/prompt/api.py |
新增 DELETE /messages/{id}/pair 端点 |
api/prompt/consumers.py |
complete/init 事件带 message_id |
api/submission/schemas.py |
SubmissionIn 加 message_id |
api/submission/api.py |
create 关联消息;delete 级联删消息 |
client/src/store/prompt.ts |
PromptMessage 加 id;回调传 messageId |
client/src/api.ts |
Submission.create 加 messageId;Prompt.deleteMessagePair |
client/src/pages/ChallengeDetail.vue |
setOnCodeComplete 传 messageId |
client/src/components/ai/PromptPanel.vue |
消息分组 + 删除按钮 |