修复重新生成功能:改用重新发送消息链路,并确保 model_id 在请求顶层

main
sp mac bookpro 2605 2026-07-22 12:43:51 +08:00
parent 44dba476c4
commit b5d40e876b
5 changed files with 36 additions and 100 deletions

View File

@ -13,6 +13,14 @@ export interface ToolCallTrace {
result: any; result: any;
} }
export interface ModelOverrides {
model?: string;
model_id?: string;
temperature?: number;
topP?: number;
maxTokens?: number;
}
export interface ChatMessage { export interface ChatMessage {
id: string; id: string;
role: 'user' | 'assistant' | 'agent' | 'system'; role: 'user' | 'assistant' | 'agent' | 'system';
@ -48,13 +56,12 @@ export interface ChatHistoryResp {
export const ChatAPI = { export const ChatAPI = {
history: (roomId: string) => history: (roomId: string) =>
api.get<ChatHistoryResp>(`/rooms/${roomId}/messages`).then((r) => r.data), api.get<ChatHistoryResp>(`/rooms/${roomId}/messages`).then((r) => r.data),
send: (roomId: string, content: string, targetAgentId: string, model?: string, model_id?: string, imageUrls?: string[]) => send: (roomId: string, content: string, targetAgentId: string, overrides?: ModelOverrides, imageUrls?: string[]) =>
api api
.post<{ user: ChatMessage; assistant: ChatMessage }>(`/rooms/${roomId}/messages`, { .post<{ user: ChatMessage; assistant: ChatMessage }>(`/rooms/${roomId}/messages`, {
content, content,
targetAgentId, targetAgentId,
model, ...overrides,
model_id,
imageUrls imageUrls
}) })
.then((r) => r.data), .then((r) => r.data),

View File

@ -1,4 +1,4 @@
import type { ChatMessage } from './chat'; import type { ChatMessage, ModelOverrides } from './chat';
import { API_BASE_URL } from './http'; import { API_BASE_URL } from './http';
export interface StreamEvents { export interface StreamEvents {
@ -13,22 +13,13 @@ export interface StreamEvents {
onError?: (msg: string) => void; onError?: (msg: string) => void;
} }
export interface ModelOverrides {
model?: string;
model_id?: string;
temperature?: number;
topP?: number;
maxTokens?: number;
}
export async function streamChat( export async function streamChat(
roomId: string, roomId: string,
targetAgentId: string, targetAgentId: string,
content: string, content: string,
handlers: StreamEvents, handlers: StreamEvents,
signal?: AbortSignal, signal?: AbortSignal,
model?: string, overrides?: ModelOverrides,
modelId?: string,
imageUrls?: string[] imageUrls?: string[]
) { ) {
const resp = await fetch(`${API_BASE_URL}rooms/${roomId}/messages/stream`, { const resp = await fetch(`${API_BASE_URL}rooms/${roomId}/messages/stream`, {
@ -37,8 +28,7 @@ export async function streamChat(
body: JSON.stringify({ body: JSON.stringify({
content, content,
targetAgentId, targetAgentId,
model, ...overrides,
model_id: modelId,
imageUrls: imageUrls ?? [] imageUrls: imageUrls ?? []
}), }),
signal, signal,
@ -47,24 +37,6 @@ export async function streamChat(
return await consumeSSE(resp, handlers, signal); return await consumeSSE(resp, handlers, signal);
} }
export async function regenerateMessage(
agentId: string,
messageId: string,
handlers: StreamEvents,
signal?: AbortSignal,
overrides?: ModelOverrides,
attachmentsText?: string
) {
const resp = await fetch(`${API_BASE_URL}chat/${agentId}/messages/${messageId}/regenerate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({ overrides, attachmentsText }),
signal,
credentials: 'include'
});
return await consumeSSE(resp, handlers, signal);
}
async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) { async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal) {
if (!resp.ok || !resp.body) { if (!resp.ok || !resp.body) {
const txt = await resp.text().catch(() => ''); const txt = await resp.text().catch(() => '');
@ -150,4 +122,3 @@ async function consumeSSE(resp: Response, h: StreamEvents, signal?: AbortSignal)
reader.cancel().catch(() => {}); reader.cancel().catch(() => {});
} }
} }

View File

@ -117,8 +117,7 @@ export default function ChatPreview({ agent, agentId }: Props) {
} }
}, },
ctrl.signal, ctrl.signal,
model, { model, model_id: modelId }
modelId
); );
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {

View File

@ -95,7 +95,7 @@ export default function MessageItem(props: {
<Button size="small" className='actions-btn' type="text" icon={<CopyOutlined />} /> <Button size="small" className='actions-btn' type="text" icon={<CopyOutlined />} />
</Tooltip> </Tooltip>
</Dropdown> </Dropdown>
<Tooltip title="重新生成(开新分支)"> <Tooltip title="重新生成">
<Button size="small" className='actions-btn' type="text" icon={<SyncOutlined />} disabled={busy} onClick={() => onRegenerate?.(message.id)} /> <Button size="small" className='actions-btn' type="text" icon={<SyncOutlined />} disabled={busy} onClick={() => onRegenerate?.(message.id)} />
</Tooltip> </Tooltip>
</div> </div>

View File

@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { Agent, BranchInfo, ChatAttachment, ChatMessage, ModelOverrides, RetrievedSnippet, ToolCallTrace } from '../../../api'; import type { Agent, BranchInfo, ChatAttachment, ChatMessage, ModelOverrides, RetrievedSnippet, ToolCallTrace } from '../../../api';
import { ChatAPI, ChatAttachmentsAPI, ImageAPI, regenerateMessage, streamChat } from '../../../api'; import { ChatAPI, ChatAttachmentsAPI, ImageAPI, streamChat } from '../../../api';
import { buildAttachmentsText } from '../utils/attachments'; import { buildAttachmentsText } from '../utils/attachments';
import { parseAgentModels } from '../utils/agentModels'; import { parseAgentModels } from '../utils/agentModels';
@ -235,8 +235,7 @@ export function useChatSender(args: {
} }
}, },
ctrl.signal, ctrl.signal,
targetModel, { ...overrides, model: targetModel, model_id: targetModelId },
targetModelId,
imageUrls imageUrls
); );
} catch (e: any) { } catch (e: any) {
@ -284,7 +283,7 @@ export function useChatSender(args: {
const attText = buildAttachmentsText(attachments); const attText = buildAttachmentsText(attachments);
const content = attText ? `${text}\n\n${attText}` : text; const content = attText ? `${text}\n\n${attText}` : text;
try { try {
const res = await ChatAPI.send(roomId, content, targetAgentId, targetModel, targetModelId, imageUrls); const res = await ChatAPI.send(roomId, content, targetAgentId, { ...overrides, model: targetModel, model_id: targetModelId }, imageUrls);
args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), res.user, res.assistant]); args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), res.user, res.assistant]);
setSessionRefresh((t) => t + 1); setSessionRefresh((t) => t + 1);
setAttachments([]); setAttachments([]);
@ -327,65 +326,25 @@ export function useChatSender(args: {
}; };
const handleRegenerate = async (assistantId: string) => { const handleRegenerate = async (assistantId: string) => {
if (!agentId || sending) return; if (!agentId || sending || !roomId) return;
// 找到当前要重新生成的助手消息,并回溯找到它的父消息(用户消息)
const assistantMsg = args.messages.find((m) => m.id === assistantId);
const userMsgId = assistantMsg?.parentId;
const userMsg = args.messages.find((m) => m.id === userMsgId);
// 如果找不到父消息,则尝试找最后一条用户消息
const lastUserMsg = [...args.messages].reverse().find((m) => m.role === 'user');
const targetUserMsg = userMsg || lastUserMsg;
if (!targetUserMsg) {
notify.error('找不到上一条用户消息');
return;
}
setSending(true); setSending(true);
setStreaming({ active: true, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] });
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
try { try {
await regenerateMessage( await handleSendStream(targetUserMsg.content);
agentId,
assistantId,
{
onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })),
onRetry: (data) => {
setStreaming((s) => ({ ...s, retryInfo: data }));
if (data?.stage === 'fallback_model' && data?.toModel) {
setOverrides((o) => ({ ...o, model: String(data.toModel) }));
}
},
onReasoningDelta: (chunk) =>
setStreaming((s) => {
const next = { ...s, reasoningText: s.reasoningText + chunk };
scrollBottom();
return next;
}),
onDelta: (chunk) =>
setStreaming((s) => {
const next = { ...s, answerText: s.answerText + chunk };
scrollBottom();
return next;
}),
onToolCall: (data) => setStreaming((s) => ({ ...s, toolCalls: [...s.toolCalls, { name: data.name, args: data.args, result: { pending: true } }] })),
onToolResult: (data) =>
setStreaming((s) => {
const list = [...s.toolCalls];
for (let i = list.length - 1; i >= 0; i--) {
if (list[i].name === data.name && (list[i].result as any)?.pending) {
list[i] = { ...list[i], result: data.result };
break;
}
}
return { ...s, toolCalls: list };
}),
onDone: () => {
setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] });
loadMessages();
},
onAborted: () => {
setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] });
loadMessages();
},
onError: (errMsg) => {
notify.error('重新生成失败:' + errMsg);
setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retryInfo: null, retrieved: [], toolCalls: [] });
loadMessages();
}
},
ctrl.signal,
overrides
);
} finally { } finally {
setSending(false); setSending(false);
} }