删除停止(思考)
解释: 停止是原来sse留下来的, 现在思考时间相对调用各种美术工具时间短, 且美术工具调用不阻塞对话,调用前已有确认取消
This commit is contained in:
@@ -15,6 +15,14 @@
|
||||
```
|
||||
|
||||
---
|
||||
## 2026-07-17 画布 Agent 普通消息不提供客户端停止
|
||||
|
||||
- 背景:普通消息进入 LLM 前,后端已经把用户消息写入 OSS;前端中断 fetch 只能停止本地等待,不能保证后端停止规划,且会保留无法与后端消息对齐的 optimistic message。
|
||||
- 决策:移除画布 Agent 普通消息的“停止”按钮和 `stopCurrentTurn`,发送期间保持按钮禁用并等待后端响应。待确认工具调用的“取消”仍保留,不受本决策影响。
|
||||
- 影响范围:画布 Agent 对话 hook、发送区交互、前端测试和专题文档。
|
||||
- 验证方式:运行画布 Agent hook / 面板定向测试、`npm run typecheck`、`npm run check:encoding` 和 `git diff --check`。
|
||||
- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。
|
||||
|
||||
## 2026-07-10 画布 Agent 工具确认分离执行参数与展示投影
|
||||
|
||||
- 背景:画布 Agent 已在实际生成前进入 `pending_confirmation`,但 `EditorAgentToolCall.args` 只保存工具私有 JSON,其中图片参数是保护真实 data key 的 SHA-256 opaque ID。前端直接解析 raw args 只能显示内部哈希或图片数量,无法向用户准确展示即将使用的目标图、参考图和完整参数;若直接把图片 URL 或对象塞回 raw args,又会破坏确认执行反序列化和 LLM 不可见真实 data key 的安全边界。
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
4. 消息内生成结果缩略图(纯预览,不显示名称,不点击聚焦图层);
|
||||
5. 生成中的进行中动画;
|
||||
6. 错误气泡(失败/余额不足,带原因);
|
||||
7. 客户端可取消等待中的普通消息请求;已经确认入队的生成任务不追回,仍按任务状态写回画板。
|
||||
7. 普通消息请求等待期间禁用发送按钮,不提供客户端停止操作;前端持续等待后端响应,避免后端已持久化消息但前端中断请求后产生会话状态错位。
|
||||
|
||||
不做(明确排除,防止后人补齐):
|
||||
|
||||
@@ -153,6 +153,6 @@
|
||||
## 第一阶段验收补充
|
||||
|
||||
- 打开画布 Agent 后任务侧栏应关闭,再次打开任务侧栏时 Agent 面板应关闭;素材 / 图层面板与 Agent 可同时展开,互不改写开关状态。
|
||||
- 发送消息时先本地追加用户消息,再应用 JSON 响应中的 `deltaMessages`;客户端取消等待不追回已经确认入队的生成工具调用。
|
||||
- 发送消息时先本地追加用户消息,再应用 JSON 响应中的 `deltaMessages`;请求等待期间发送按钮保持禁用,前端不主动中断当前回合。
|
||||
- Agent 消息内生成结果缩略图只用于预览,不显示名称,也不点击跳转图层;轮询到任务终态并完成会话懒回填后统一刷新工程快照和素材库。
|
||||
- 对话内容可被用户选中复制;用户从输入框或对话内容点击回画布图层 / 生成器时,焦点应回到画布对象,Backspace / Delete 等画布快捷键继续生效。
|
||||
|
||||
+48
@@ -306,6 +306,54 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('disables sending while a message request is pending without showing stop', async () => {
|
||||
const client = createClient();
|
||||
let resolveSend!: (response: EditorAgentMessageResponse) => void;
|
||||
vi.mocked(client.sendMessage).mockImplementation(
|
||||
() =>
|
||||
new Promise<EditorAgentMessageResponse>((resolve) => {
|
||||
resolveSend = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<EditorAgentConversationPanelView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
client={client}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '继续规划' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: '发送' }).hasAttribute('disabled'),
|
||||
).toBe(true);
|
||||
});
|
||||
expect(screen.queryByRole('button', { name: '停止' })).toBeNull();
|
||||
|
||||
await act(async () => {
|
||||
resolveSend({
|
||||
conversation: {
|
||||
conversationId: 'conversation-1',
|
||||
projectId: 'project-1',
|
||||
title: '角色参考',
|
||||
updatedAt: '2026-07-03T00:00:20.000Z',
|
||||
},
|
||||
deltaMessages: [],
|
||||
errorMessage: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uploads pasted images as canvas attachments before sending', async () => {
|
||||
const client = createClient();
|
||||
|
||||
|
||||
+5
-16
@@ -5,7 +5,6 @@ import {
|
||||
Paperclip,
|
||||
Plus,
|
||||
Send,
|
||||
Square,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
@@ -266,7 +265,6 @@ export function EditorAgentConversationPanelView({
|
||||
selectConversation,
|
||||
refreshActiveConversation,
|
||||
sendMessage,
|
||||
stopCurrentTurn,
|
||||
confirmToolCall,
|
||||
cancelToolCall,
|
||||
deleteActiveConversation,
|
||||
@@ -344,7 +342,6 @@ export function EditorAgentConversationPanelView({
|
||||
const submitMessage = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (isWaiting) {
|
||||
stopCurrentTurn();
|
||||
return;
|
||||
}
|
||||
if (isPastingAttachment) {
|
||||
@@ -671,22 +668,14 @@ export function EditorAgentConversationPanelView({
|
||||
type="submit"
|
||||
className="inline-flex h-10 min-w-16 shrink-0 items-center justify-center gap-1.5 rounded-full bg-slate-900 px-3 text-sm font-semibold text-white disabled:opacity-45"
|
||||
disabled={
|
||||
isWaiting ||
|
||||
isToolCallActionPending ||
|
||||
(!isWaiting &&
|
||||
((!draftText.trim() && !attachments.length) || !hasProject))
|
||||
(!draftText.trim() && !attachments.length) ||
|
||||
!hasProject
|
||||
}
|
||||
>
|
||||
{isWaiting ? (
|
||||
<>
|
||||
<Square className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
停止
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
发送
|
||||
</>
|
||||
)}
|
||||
<Send className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+20
-12
@@ -144,9 +144,7 @@ describe('useEditorAgentConversation', () => {
|
||||
text: '把这个角色改成像素风',
|
||||
attachments: [],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
signal: expect.any(AbortSignal),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
expect(result.current.isWaiting).toBe(false);
|
||||
expect(result.current.activeConversation?.title).toBe(
|
||||
@@ -811,16 +809,15 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.messages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('aborts the active request and resets state on stopCurrentTurn', async () => {
|
||||
it('keeps the active request pending without exposing a stop action', async () => {
|
||||
const client = createClient();
|
||||
let capturedSignal: AbortSignal | null = null;
|
||||
let resolveSend!: (response: EditorAgentMessageResponse) => void;
|
||||
vi.mocked(client.sendMessage).mockImplementation(
|
||||
(_conversationId, _payload, options) =>
|
||||
new Promise<EditorAgentMessageResponse>((resolve, reject) => {
|
||||
new Promise<EditorAgentMessageResponse>((resolve) => {
|
||||
resolveSend = resolve;
|
||||
capturedSignal = options.signal ?? null;
|
||||
capturedSignal?.addEventListener('abort', () => {
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
});
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
@@ -840,13 +837,24 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.isWaiting).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.stopCurrentTurn();
|
||||
expect(capturedSignal).toBeNull();
|
||||
expect('stopCurrentTurn' in result.current).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
resolveSend({
|
||||
conversation: {
|
||||
conversationId: 'conversation-1',
|
||||
projectId: 'project-1',
|
||||
title: '角色参考',
|
||||
updatedAt: '2026-07-03T00:00:20.000Z',
|
||||
},
|
||||
deltaMessages: [],
|
||||
errorMessage: null,
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedSignal?.aborted).toBe(true);
|
||||
expect(result.current.isWaiting).toBe(false);
|
||||
});
|
||||
expect(result.current.isWaiting).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,16 +73,6 @@ const defaultEditorAgentConversationClient: EditorAgentConversationClient = {
|
||||
cancelToolCall: cancelEditorAgentToolCall,
|
||||
};
|
||||
|
||||
function isAbortError(error: unknown) {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
(error.name === 'AbortError' ||
|
||||
(typeof DOMException !== 'undefined' &&
|
||||
error instanceof DOMException &&
|
||||
error.name === 'AbortError'))
|
||||
);
|
||||
}
|
||||
|
||||
function createEditorAgentClientMessageId() {
|
||||
const randomId =
|
||||
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
||||
@@ -167,7 +157,6 @@ export function useEditorAgentConversation({
|
||||
const [toolCallAction, setToolCallAction] =
|
||||
useState<EditorAgentToolCallActionState>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const activeRequestAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const activeConversationIdRef = useRef<string | null>(null);
|
||||
const activeToolCallActionRef = useRef<EditorAgentToolCallActionState>(null);
|
||||
const conversationLoadRequestIdRef = useRef(0);
|
||||
@@ -382,8 +371,6 @@ export function useEditorAgentConversation({
|
||||
}
|
||||
const conversationId = await ensureConversationForSend();
|
||||
const clientMessageId = createEditorAgentClientMessageId();
|
||||
const abortController = new AbortController();
|
||||
activeRequestAbortControllerRef.current = abortController;
|
||||
setErrorMessage(null);
|
||||
setIsWaiting(true);
|
||||
const optimisticMessage = createLocalUserMessage({
|
||||
@@ -405,9 +392,7 @@ export function useEditorAgentConversation({
|
||||
text,
|
||||
attachments,
|
||||
},
|
||||
{
|
||||
signal: abortController.signal,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
setConversations((currentConversations) =>
|
||||
@@ -425,9 +410,6 @@ export function useEditorAgentConversation({
|
||||
applyDeltaMessages(response.deltaMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAbortError(error) || abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '发送画布 Agent 消息失败';
|
||||
if (activeConversationIdRef.current === conversationId) {
|
||||
@@ -438,9 +420,6 @@ export function useEditorAgentConversation({
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (activeRequestAbortControllerRef.current === abortController) {
|
||||
activeRequestAbortControllerRef.current = null;
|
||||
}
|
||||
setIsWaiting(false);
|
||||
}
|
||||
},
|
||||
@@ -454,12 +433,6 @@ export function useEditorAgentConversation({
|
||||
],
|
||||
);
|
||||
|
||||
const stopCurrentTurn = useCallback(() => {
|
||||
activeRequestAbortControllerRef.current?.abort();
|
||||
activeRequestAbortControllerRef.current = null;
|
||||
setIsWaiting(false);
|
||||
}, []);
|
||||
|
||||
const resolveToolCall = useCallback(
|
||||
async (messageId: number, action: EditorAgentToolCallAction) => {
|
||||
const conversationId = activeConversationIdRef.current;
|
||||
@@ -570,7 +543,6 @@ export function useEditorAgentConversation({
|
||||
selectConversation,
|
||||
refreshActiveConversation,
|
||||
sendMessage,
|
||||
stopCurrentTurn,
|
||||
confirmToolCall,
|
||||
cancelToolCall,
|
||||
deleteActiveConversation,
|
||||
|
||||
Reference in New Issue
Block a user