Files
web/docs/superpowers/specs/2026-04-14-delete-message-pair-submission-design.md
xuyueandClaude Opus 5.5 47d4679984 chore: 合并为单仓库,统一部署流程
- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:12 +08:00

5.7 KiB
Raw Permalink Blame History

设计文档:学生删除 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 不为空:

  1. 查找对应的 assistant Message(source="conversation",属于当前 user+task)
  2. 设置 message.submission = submission 并保存

DELETE /submission/{id}(delete_submission)

删除 Submission 前:

  1. 通过 submission.source_message(related_name)找到关联的 assistant message(若有)
  2. 找到该 assistant message 在同一 conversation 中前一条 user message(按 created 排序取紧邻的上一条)
  3. 依次删除:user message → assistant message → submission

新增 DELETE /prompt/messages/{message_id}/pair

  • 鉴权:message.conversation.user == request.user,否则 403
  • 操作:
    1. 找到 assistant message(id=message_id,role="assistant")
    2. 找到同 conversation 中紧邻的前一条 user message
    3. 若 assistant message 有关联 submission,删除它
    4. 删除 assistant message 和 user 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 时显现。点击后:

  1. 调用 Prompt.deleteMessagePair(pair.assistantMessage.id)
  2. 成功后从本地 messages 中移除这两条

交互细节:

  • 使用 Naive UI 的 useMessage() 给出成功/失败通知
  • streaming 期间禁用所有删除按钮(disabled when streaming)
  • 没有 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 消息分组 + 删除按钮