删除停止(思考)

解释: 停止是原来sse留下来的, 现在思考时间相对调用各种美术工具时间短, 且美术工具调用不阻塞对话,调用前已有确认取消
This commit is contained in:
2026-07-17 16:04:45 +08:00
parent c43836d881
commit 9b42c64300
6 changed files with 84 additions and 59 deletions
@@ -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 等画布快捷键继续生效。
@@ -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,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>
@@ -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,