From 239e09473e81d946444f87589add524c2da3a577 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Tue, 17 Mar 2026 16:41:08 +0800 Subject: [PATCH] fix(web): preserve public workflow SSE reconnect after pause (#33552) --- .../workflow-stream-handlers.spec.ts | 12 ++++++--- .../hooks/__tests__/use-result-sender.spec.ts | 25 +++++++++++++++++++ .../result/hooks/use-result-sender.ts | 3 ++- .../result/workflow-stream-handlers.ts | 4 +++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts index 369f78eb76..45d5ded302 100644 --- a/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts +++ b/web/app/components/share/text-generation/result/__tests__/workflow-stream-handlers.spec.ts @@ -328,7 +328,7 @@ describe('createWorkflowStreamHandlers', () => { vi.clearAllMocks() }) - const setupHandlers = (overrides: { isTimedOut?: () => boolean } = {}) => { + const setupHandlers = (overrides: { isPublicAPI?: boolean, isTimedOut?: () => boolean } = {}) => { let completionRes = '' let currentTaskId: string | null = null let isStopping = false @@ -359,6 +359,7 @@ describe('createWorkflowStreamHandlers', () => { const handlers = createWorkflowStreamHandlers({ getCompletionRes: () => completionRes, getWorkflowProcessData: () => workflowProcessData, + isPublicAPI: overrides.isPublicAPI ?? false, isTimedOut: overrides.isTimedOut ?? (() => false), markEnded, notify, @@ -391,7 +392,7 @@ describe('createWorkflowStreamHandlers', () => { } it('should process workflow success and paused events', () => { - const setup = setupHandlers() + const setup = setupHandlers({ isPublicAPI: true }) const handlers = setup.handlers as Required> act(() => { @@ -546,7 +547,11 @@ describe('createWorkflowStreamHandlers', () => { resultText: 'Hello', status: WorkflowRunningStatus.Succeeded, })) - expect(sseGetMock).toHaveBeenCalledWith('/workflow/run-1/events', {}, expect.any(Object)) + expect(sseGetMock).toHaveBeenCalledWith( + '/workflow/run-1/events', + {}, + expect.objectContaining({ isPublicAPI: true }), + ) expect(setup.messageId()).toBe('run-1') expect(setup.onCompleted).toHaveBeenCalledWith('{"answer":"Hello"}', 3, true) expect(setup.setRespondingFalse).toHaveBeenCalled() @@ -647,6 +652,7 @@ describe('createWorkflowStreamHandlers', () => { const handlers = createWorkflowStreamHandlers({ getCompletionRes: () => '', getWorkflowProcessData: () => existingProcess, + isPublicAPI: false, isTimedOut: () => false, markEnded: vi.fn(), notify: setup.notify, diff --git a/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts b/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts index 58b47789c1..74d8908d54 100644 --- a/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts +++ b/web/app/components/share/text-generation/result/hooks/__tests__/use-result-sender.spec.ts @@ -351,6 +351,7 @@ describe('useResultSender', () => { await waitFor(() => { expect(createWorkflowStreamHandlersMock).toHaveBeenCalledWith(expect.objectContaining({ getCompletionRes: harness.runState.getCompletionRes, + isPublicAPI: true, resetRunState: harness.runState.resetRunState, setWorkflowProcessData: harness.runState.setWorkflowProcessData, })) @@ -373,6 +374,30 @@ describe('useResultSender', () => { expect(harness.runState.clearMoreLikeThis).not.toHaveBeenCalled() }) + it('should configure workflow handlers for installed apps as non-public', async () => { + const harness = createRunStateHarness() + + const { result } = renderSender({ + appSourceType: AppSourceTypeEnum.installedApp, + isWorkflow: true, + runState: harness.runState, + }) + + await act(async () => { + expect(await result.current.handleSend()).toBe(true) + }) + + expect(createWorkflowStreamHandlersMock).toHaveBeenCalledWith(expect.objectContaining({ + isPublicAPI: false, + })) + expect(sendWorkflowMessageMock).toHaveBeenCalledWith( + { inputs: { name: 'Alice' } }, + expect.any(Object), + AppSourceTypeEnum.installedApp, + 'app-1', + ) + }) + it('should stringify non-Error workflow failures', async () => { const harness = createRunStateHarness() sendWorkflowMessageMock.mockRejectedValue('workflow failed') diff --git a/web/app/components/share/text-generation/result/hooks/use-result-sender.ts b/web/app/components/share/text-generation/result/hooks/use-result-sender.ts index 3bae2b02f8..e2a6b19277 100644 --- a/web/app/components/share/text-generation/result/hooks/use-result-sender.ts +++ b/web/app/components/share/text-generation/result/hooks/use-result-sender.ts @@ -1,11 +1,11 @@ import type { ResultInputValue } from '../result-request' import type { ResultRunStateController } from './use-result-run-state' import type { PromptConfig } from '@/models/debug' -import type { AppSourceType } from '@/service/share' import type { VisionFile, VisionSettings } from '@/types/app' import { useCallback, useEffect, useRef } from 'react' import { TEXT_GENERATION_TIMEOUT_MS } from '@/config' import { + AppSourceType, sendCompletionMessage, sendWorkflowMessage, } from '@/service/share' @@ -117,6 +117,7 @@ export const useResultSender = ({ const otherOptions = createWorkflowStreamHandlers({ getCompletionRes: runState.getCompletionRes, getWorkflowProcessData: runState.getWorkflowProcessData, + isPublicAPI: appSourceType === AppSourceType.webApp, isTimedOut: () => isTimeout, markEnded: () => { isEnd = true diff --git a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts index 843bac9e2c..48b132587e 100644 --- a/web/app/components/share/text-generation/result/workflow-stream-handlers.ts +++ b/web/app/components/share/text-generation/result/workflow-stream-handlers.ts @@ -13,6 +13,7 @@ type Translate = (key: string, options?: Record) => string type CreateWorkflowStreamHandlersParams = { getCompletionRes: () => string getWorkflowProcessData: () => WorkflowProcess | undefined + isPublicAPI: boolean isTimedOut: () => boolean markEnded: () => void notify: Notify @@ -255,6 +256,7 @@ const serializeWorkflowOutputs = (outputs: WorkflowFinishedResponse['data']['out export const createWorkflowStreamHandlers = ({ getCompletionRes, getWorkflowProcessData, + isPublicAPI, isTimedOut, markEnded, notify, @@ -287,6 +289,7 @@ export const createWorkflowStreamHandlers = ({ } const otherOptions: IOtherOptions = { + isPublicAPI, onWorkflowStarted: ({ workflow_run_id, task_id }) => { const workflowProcessData = getWorkflowProcessData() if (workflowProcessData?.tracing.length) { @@ -378,6 +381,7 @@ export const createWorkflowStreamHandlers = ({ }, onWorkflowPaused: ({ data }) => { tempMessageId = data.workflow_run_id + // WebApp workflows must keep using the public API namespace after pause/resume. void sseGet(`/workflow/${data.workflow_run_id}/events`, {}, otherOptions) setWorkflowProcessData(applyWorkflowPaused(getWorkflowProcessData())) },