feat: 支持删除单条聊天消息(用户提问与模型回答)

- 后端新增 DELETE /api/threads/{thread_id}/messages/{message_id},从最新 checkpoint 的 messages 通道过滤掉指定 id 并写回新 checkpoint
- 前端 useDeleteMessage 钩子 + DeleteMessageButton(带确认框)组件
- 在用户问题气泡与助手回答工具栏上各加删除按钮,仅对已落库消息显示
- MessageList 本地隐藏已删消息并在切换会话时重置;只读/分享场景禁用删除
- 新增 deleteMessage 相关中英文案

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-04 14:28:11 +08:00
1 parent 925675ab54
commit 362a8268e8
8 files changed
+868 -594

No files matched your search

+41 -6
View File
@@ -350,12 +350,12 @@ export function useThreadStream({
// agent thread URL — otherwise fall back to the global new chat.
const hash = typeof window !== "undefined" ? window.location.hash : "";
const agentMatch = hash.match(/#\/page\/workspace\/agents\/([^/]+)\//);
const iframeMatch = hash.match(/#\/page\/workspace\/chats_iframe\//);
const target = agentMatch
? `/page/workspace/agents/${agentMatch[1]}/chats/new`
: iframeMatch
? "/page/workspace/chats_iframe/new"
: "/page/workspace/chats/new";
const iframeMatch = hash.match(/#\/page\/workspace\/chats_iframe\//);
const target = agentMatch
? `/page/workspace/agents/${agentMatch[1]}/chats/new`
: iframeMatch
? "/page/workspace/chats_iframe/new"
: "/page/workspace/chats/new";
// First navigate to a safe URL so the post-reload page lands on a
// valid route. Then full-reload so TanStack Query cache, context
// providers and any other state captured under the previous user's
@@ -804,6 +804,41 @@ export function useDeleteThread() {
});
}
/**
* Delete a single message (a user question or an assistant answer) from a
* thread. Hits the Gateway endpoint that filters the message out of the latest
* checkpoint. The live `useStream` cache is not auto-refreshed by this call —
* callers hide the deleted id locally (see `MessageList`) so the removal shows
* immediately; a reload then reads the persisted, already-trimmed state.
*/
export function useDeleteMessage() {
return useMutation({
mutationFn: async ({
threadId,
messageId,
}: {
threadId: string;
messageId: string;
}) => {
const response = await apiFetch(
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(
threadId,
)}/messages/${encodeURIComponent(messageId)}`,
{
method: "DELETE",
},
);
if (!response.ok) {
const error = await response
.json()
.catch(() => ({ detail: "删除消息失败。" }));
throw new Error(error.detail ?? "删除消息失败。");
}
},
});
}
export function useRenameThread() {
const queryClient = useQueryClient();
const apiClient = getAPIClient();