This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new fd76d808e [ISSUE #2718] fix(ai): honor the configured API base for 
streaming (#2724)
fd76d808e is described below

commit fd76d808ed6223c47b71bcfcac9675505ebac91e
Author: shown <[email protected]>
AuthorDate: Fri Sep 4 11:52:47 2026 +0800

    [ISSUE #2718] fix(ai): honor the configured API base for streaming (#2724)
    
    Signed-off-by: yuluo-yx <[email protected]>
---
 web/src/api/ai.test.ts | 34 ++++++++++++++++++++++++++++++++--
 web/src/api/ai.ts      |  8 ++++++--
 web/src/api/client.ts  | 10 +++++++---
 3 files changed, 45 insertions(+), 7 deletions(-)

diff --git a/web/src/api/ai.test.ts b/web/src/api/ai.test.ts
index 9ca698287..89b71029d 100644
--- a/web/src/api/ai.test.ts
+++ b/web/src/api/ai.test.ts
@@ -17,6 +17,7 @@
 
 import MockAdapter from 'axios-mock-adapter';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { useAiChatHistoryStore } from '../stores/aiChatHistoryStore';
 import client from './client';
 import {
   AiStreamError,
@@ -28,6 +29,10 @@ import {
   type McpTool,
 } from './ai';
 
+vi.mock('../config', () => ({
+  API_BASE_URL: 'https://backend.example.com/studio-api',
+}));
+
 const mock = new MockAdapter(client);
 const encoder = new TextEncoder();
 
@@ -45,6 +50,8 @@ function streamResponse(chunks: string[], onCancel?: () => 
void): Response {
 describe('AI API', () => {
   beforeEach(() => {
     mock.reset();
+    localStorage.clear();
+    useAiChatHistoryStore.getState().clearHistories();
   });
 
   afterEach(() => {
@@ -53,7 +60,7 @@ describe('AI API', () => {
   });
 
   describe('chatStream (SSE)', () => {
-    it('sends browser cookies without reading browser storage', async () => {
+    it('uses the configured API base URL and sends browser cookies', async () 
=> {
       const fetchMock = vi.fn().mockResolvedValue(streamResponse(['data: 
[DONE]\n\n']));
       vi.stubGlobal('fetch', fetchMock);
 
@@ -62,7 +69,7 @@ describe('AI API', () => {
       ).resolves.toBeUndefined();
 
       expect(fetchMock).toHaveBeenCalledWith(
-        '/api/ai/chat',
+        'https://backend.example.com/studio-api/ai/chat',
         expect.objectContaining({
           credentials: 'include',
           headers: { 'Content-Type': 'application/json' },
@@ -70,6 +77,29 @@ describe('AI API', () => {
       );
     });
 
+    it('clears the current session when the stream request returns 401', async 
() => {
+      localStorage.setItem('rocketmq-studio-user', 'admin');
+      localStorage.setItem('rocketmq-studio-user-admin', 'true');
+      useAiChatHistoryStore.getState().startConversation('real', 
'conversation-1');
+      vi.stubGlobal(
+        'fetch',
+        vi.fn().mockResolvedValue(
+          new Response(JSON.stringify({ message: 'Unauthorized' }), {
+            status: 401,
+            statusText: 'Unauthorized',
+          }),
+        ),
+      );
+
+      await expect(
+        chatStream({ message: 'hello', mode: 'chat', model: 'stub' }, vi.fn()),
+      ).rejects.toMatchObject({ status: 401 });
+
+      expect(localStorage.getItem('rocketmq-studio-user')).toBeNull();
+      expect(localStorage.getItem('rocketmq-studio-user-admin')).toBeNull();
+      
expect(useAiChatHistoryStore.getState().histories.real.conversations).toEqual([]);
+    });
+
     it('reassembles an event split across network chunks', async () => {
       vi.stubGlobal(
         'fetch',
diff --git a/web/src/api/ai.ts b/web/src/api/ai.ts
index 8044adc53..564af6db0 100644
--- a/web/src/api/ai.ts
+++ b/web/src/api/ai.ts
@@ -15,7 +15,8 @@
  * limitations under the License.
  */
 
-import client from './client';
+import { API_BASE_URL } from '../config';
+import client, { handleSessionUnauthorized } from './client';
 
 const MAX_SSE_EVENT_CHARS = 1024 * 1024;
 
@@ -187,7 +188,7 @@ export async function chatStream(
   signal?: AbortSignal,
   onEnhance?: (prompt: string) => void,
 ) {
-  const response = await fetch('/api/ai/chat', {
+  const response = await fetch(`${API_BASE_URL}/ai/chat`, {
     method: 'POST',
     credentials: 'include',
     headers: {
@@ -197,6 +198,9 @@ export async function chatStream(
     signal,
   });
 
+  if (response.status === 401) {
+    handleSessionUnauthorized();
+  }
   if (!response.ok) {
     throw await parseHttpError(response);
   }
diff --git a/web/src/api/client.ts b/web/src/api/client.ts
index d442c571a..0db0f5fb2 100644
--- a/web/src/api/client.ts
+++ b/web/src/api/client.ts
@@ -79,6 +79,12 @@ function isPublicAuthRequest(url?: string): boolean {
   }
 }
 
+export function handleSessionUnauthorized(): void {
+  clearAiChatHistories();
+  clearAuthSession();
+  window.location.href = '/login';
+}
+
 const client = axios.create({
   baseURL: API_BASE_URL,
   timeout: 30000,
@@ -97,9 +103,7 @@ client.interceptors.response.use(
   },
   (error) => {
     if (error.response?.status === 401 && 
!isPublicAuthRequest(error.config?.url)) {
-      clearAiChatHistories();
-      clearAuthSession();
-      window.location.href = '/login';
+      handleSessionUnauthorized();
       return Promise.reject(error);
     }
     if (isCorsRejection(error)) {

Reply via email to