From 92ac9c48d6ea76147cf155626ee8365a74e47957 Mon Sep 17 00:00:00 2001 From: Senor-Liu <71912659+Senor-Liu@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:13:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E8=AF=9D=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sections/superAgent/container/index.jsx | 40 +++++-------------- .../superAgent/hooks/useSuperAgentStream.js | 18 +++------ web/tests/superAgentFlowContract.test.mjs | 12 +++++- 3 files changed, 26 insertions(+), 44 deletions(-) diff --git a/web/client/src/sections/superAgent/container/index.jsx b/web/client/src/sections/superAgent/container/index.jsx index 51efb9a..bf8b368 100644 --- a/web/client/src/sections/superAgent/container/index.jsx +++ b/web/client/src/sections/superAgent/container/index.jsx @@ -32,9 +32,6 @@ import useSuperAgentSession from "../hooks/useSuperAgentSession"; import useSuperAgentStream from "../hooks/useSuperAgentStream"; import useSuperAgentProfile from "../hooks/useSuperAgentProfile"; import useSuperAgentWorkspace from "../hooks/useSuperAgentWorkspace"; -import { - cancelSuperAgentSession, -} from "../actions/api"; import { getCompatibleAuthUserInfo } from "../../../utils/authCompat"; import { CHAT_WELCOME_CONTENT, @@ -1544,33 +1541,16 @@ const SuperAgent = () => { }; const handleStopAndCancel = async () => { - const sessionId = activeSessionId || activeTaskId; - const stopOperations = [stream.stopRun()]; - if (sessionId && currentUserId) { - stopOperations.push( - cancelSuperAgentSession({ - sessionId, - userId: currentUserId, - }) - ); - } - - const results = await Promise.allSettled(stopOperations); - const failedOperation = results.find((result) => result.status === "rejected"); - if (failedOperation) { - message.error(failedOperation.reason?.message || "停止运行失败"); - return; - } - if (sessionId) { - try { - await stream.refreshState(); - } catch (error) { - superAgentError("container.stop", "state.refresh_failed", { - sessionId, - error, - }); - message.error(error?.message || "停止后状态同步失败"); - } + try { + // useStream 持有当前 thread_id/run_id;stop() 会取消该运行并中止 SSE。 + await stream.stopRun(); + await stream.refreshState(); + } catch (error) { + superAgentError("container.stop", "failed", { + sessionId: activeSessionId, + error, + }); + message.error(error?.message || "停止运行失败"); } }; diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js index 6a906f6..be8526f 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js @@ -254,8 +254,10 @@ class SingleSubscriberSseTransportAdapter extends ProtocolSseTransportAdapter { async getState() { const requestVersion = ++this.stateRequestVersion; const requestedThreadId = String(this.threadId || "").trim(); - const state = await super.getState(); - // getState 可能在切换 thread 后才返回;旧请求不能覆盖新 thread 的快照。 + // Agent 当前未提供 /threads/:threadId/state;useStream hydrate 只能使用 + // 已从事件流获得的快照,不能为新会话触发一个必然失败的 HTTP 请求。 + const state = this.latestState; + // 旧 state 快照不能在切换 thread 后覆盖新会话。 if ( requestVersion === this.stateRequestVersion && String(this.threadId || "").trim() === requestedThreadId @@ -379,13 +381,6 @@ BUSINESS_CHANNELS.forEach((channel) => { if (!ROOT_PUMP_CHANNELS.includes(channel)) ROOT_PUMP_CHANNELS.push(channel); }); -const createClientMessageId = () => { - if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { - return crypto.randomUUID(); - } - return `client-${Date.now()}-${Math.random().toString(16).slice(2)}`; -}; - const getEventPayload = (event) => event?.data ?? event?.payload ?? @@ -529,7 +524,7 @@ const useSuperAgentStream = ({ defaultHeaders, callerOptions, messagesKey: "messages", - optimistic: true, + optimistic: false, transport: eventTransport, }); @@ -864,8 +859,7 @@ const useSuperAgentStream = ({ try { await stream.submit({ messages: [{ - type: "human", - id: createClientMessageId(), + role: "user", content: String(content || ""), }], display_content: String(displayContent ?? content ?? ""), diff --git a/web/tests/superAgentFlowContract.test.mjs b/web/tests/superAgentFlowContract.test.mjs index e2aee89..57a29dd 100644 --- a/web/tests/superAgentFlowContract.test.mjs +++ b/web/tests/superAgentFlowContract.test.mjs @@ -27,7 +27,13 @@ assert.doesNotMatch(streamHook, /onCustomEvent/); assert.match(streamHook, /ROOT_PUMP_CHANNELS/); assert.match(streamHook, /"custom"/); assert.match(streamHook, /attachment_ids:/); +assert.match(streamHook, /stream\.submit\(/); +assert.match(streamHook, /stream\.respond\(/); +assert.match(streamHook, /role:\s*"user"/); +assert.doesNotMatch(streamHook, /type:\s*"human"/); +assert.doesNotMatch(streamHook, /super\.getState\(\)/); assert.match(streamHook, /const stopRun =/); +assert.match(streamHook, /await stream\.stop\(\)/); assert.match(streamHook, /disconnect:\s*stream\.disconnect/); assert.match(streamHook, /eventRequestControllersRef/); assert.match(streamHook, /isLifecycleWatcherRequest/); @@ -38,6 +44,7 @@ assert.match(streamHook, /runtimeInterrupts/); assert.doesNotMatch(container, /useSuperAgentChat/); assert.doesNotMatch(container, /respondSuperAgentCheckpoint/); assert.doesNotMatch(container, /sendSuperAgentChat/); +assert.doesNotMatch(container, /cancelSuperAgentSession/); const generatedDecisionStart = container.indexOf("const response = {"); const generatedDecisionEnd = container.indexOf("};", generatedDecisionStart); const generatedDecision = container.slice(generatedDecisionStart, generatedDecisionEnd); @@ -63,5 +70,6 @@ assert.match(container, /syncSessionProgressSections/); assert.match(conversation, /toolCalls=\{item\.toolCalls\}/); assert.match(jsonRender, /toolCall\?\.name === "publish_ui"/); assert.doesNotMatch(jsonRender, /```json-render|JSON_RENDER_FENCE/); -assert.match(apiRoutes, /'\/api\/chat'/); -assert.match(apiRoutes, /'\/api\/checkpoint\/respond'/); +assert.match(apiRoutes, /'\/super-agent\/threads\/:threadId\/commands'/); +assert.match(apiRoutes, /'\/super-agent\/threads\/:threadId\/stream\/events'/); +assert.match(apiRoutes, /'\/super-agent\/threads\/:threadId\/runs\/:runId\/cancel'/);