From 9041733342d5275e397ea3dc315030a47de041fb Mon Sep 17 00:00:00 2001 From: "alan.cl" <1165243776@qq.com> Date: Wed, 27 May 2026 10:51:32 +0800 Subject: [PATCH] fix(connector): wire frontend through ctx-axios + add sidebar entry + normalize field names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing integration gaps surfaced when running the dev server: 1. use-connector-api.ts used bare fetch() instead of ctx-axios, so all /api/v2/serve/connectors/* requests hit the Next.js dev server (3000) instead of the backend (5670) and returned the Next 404 HTML page. Migrate every helper to axios via @/utils/ctx-axios. 2. useConfirmPolling.ts had the same bare-fetch problem; HITL confirm polling and resolve calls would never reach the backend. 3. Backend ConnectorResponse exposes connector_id while the frontend ConnectorInstance type uses id, leaving every c.id undefined. Add a normalizeConnector boundary in use-connector-api.ts so the rest of the frontend can keep using id. 4. /construct/connectors had no entry in the side-bar settings menu — users had to type the URL by hand. Add an "ApiOutlined / 连接器管理" item next to prompts in the management group. 5. Add the new `connectors` translation key to zh/en common.ts so the sidebar label resolves and TS strict-key check passes. --- web/components/layout/side-bar.tsx | 16 +++++ web/hooks/use-connector-api.ts | 63 +++++++++---------- web/locales/en/common.ts | 1 + web/locales/zh/common.ts | 1 + .../connector/useConfirmPolling.ts | 15 ++--- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/web/components/layout/side-bar.tsx b/web/components/layout/side-bar.tsx index eaee795fa..560fd6591 100644 --- a/web/components/layout/side-bar.tsx +++ b/web/components/layout/side-bar.tsx @@ -7,6 +7,7 @@ import type { IChatDialogueSchema } from '@/types/chat'; import { STORAGE_LANG_KEY, STORAGE_THEME_KEY } from '@/utils/constants/index'; import Icon, { ApartmentOutlined, + ApiOutlined, AppstoreOutlined, DeleteOutlined, EditOutlined, @@ -238,6 +239,21 @@ function SideBar() { {t('prompts')} +
{ + router.push('/construct/connectors'); + setSettingsOpen(false); + }} + className={cls( + 'flex items-center gap-3 px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer transition-colors', + { + 'bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400': pathname.startsWith('/construct/connectors'), + }, + )} + > + + {t('connectors')} +
{ router.push('/construct/dbgpts'); diff --git a/web/hooks/use-connector-api.ts b/web/hooks/use-connector-api.ts index 0af74380e..95bd8e704 100644 --- a/web/hooks/use-connector-api.ts +++ b/web/hooks/use-connector-api.ts @@ -1,19 +1,22 @@ -import { useCallback, useEffect, useState } from 'react'; +import axios from '@/utils/ctx-axios'; import { ConnectorCatalogEntry, ConnectorInstance, CreateConnectorRequest } from '@/new-components/connector/types'; +import { useCallback, useEffect, useState } from 'react'; const API_BASE = '/api/v2/serve/connectors'; -async function apiFetch(url: string, options?: RequestInit): Promise { - const response = await fetch(url, { - headers: { 'Content-Type': 'application/json', ...options?.headers }, - ...options, - }); - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status}: ${text}`); - } - const json = await response.json(); - return json.data as T; +// Backend ConnectorResponse uses `connector_id`; frontend code uses `id`. +// Normalize at the boundary so the rest of the app can stay on `id`. +type BackendConnector = Omit & { connector_id: string }; + +function normalizeConnector(raw: BackendConnector): ConnectorInstance { + const { connector_id, ...rest } = raw; + return { id: connector_id, ...rest } as ConnectorInstance; +} + +function unwrap(payload: any): T { + // ctx-axios already unwrapped axios.response → response.data, so payload here + // is the API envelope { success, err_code, err_msg, data }. + return (payload?.data ?? payload) as T; } export function useConnectorTypes() { @@ -22,8 +25,9 @@ export function useConnectorTypes() { useEffect(() => { setLoading(true); - apiFetch(`${API_BASE}/types`) - .then(data => setTypes(data ?? [])) + axios + .get(`${API_BASE}/types`) + .then(res => setTypes(unwrap(res) ?? [])) .catch(err => console.error('Failed to fetch connector types:', err)) .finally(() => setLoading(false)); }, []); @@ -38,8 +42,12 @@ export function useConnectors() { useEffect(() => { setLoading(true); - apiFetch(API_BASE) - .then(data => setConnectors(data ?? [])) + axios + .get(API_BASE) + .then(res => { + const raw = unwrap(res) ?? []; + setConnectors(raw.map(normalizeConnector)); + }) .catch(err => console.error('Failed to fetch connectors:', err)) .finally(() => setLoading(false)); }, [version]); @@ -57,10 +65,8 @@ export function useCreateConnector() { const create = useCallback(async (data: CreateConnectorRequest): Promise => { setLoading(true); try { - return await apiFetch(API_BASE, { - method: 'POST', - body: JSON.stringify(data), - }); + const res = await axios.post(API_BASE, data); + return normalizeConnector(unwrap(res)); } catch (err) { console.error('Failed to create connector:', err); throw err; @@ -79,10 +85,8 @@ export function useUpdateConnector() { async (id: string, data: Partial): Promise => { setLoading(true); try { - return await apiFetch(`${API_BASE}/${id}`, { - method: 'PUT', - body: JSON.stringify(data), - }); + const res = await axios.put(`${API_BASE}/${id}`, data); + return normalizeConnector(unwrap(res)); } catch (err) { console.error('Failed to update connector:', err); throw err; @@ -102,11 +106,7 @@ export function useDeleteConnector() { const remove = useCallback(async (id: string): Promise => { setLoading(true); try { - const response = await fetch(`${API_BASE}/${id}`, { method: 'DELETE' }); - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status}: ${text}`); - } + await axios.delete(`${API_BASE}/${id}`); } catch (err) { console.error('Failed to delete connector:', err); throw err; @@ -124,9 +124,8 @@ export function useTestConnection() { const test = useCallback(async (id: string): Promise<{ success: boolean; message: string }> => { setLoading(true); try { - return await apiFetch<{ success: boolean; message: string }>(`${API_BASE}/${id}/test`, { - method: 'POST', - }); + const res = await axios.post(`${API_BASE}/${id}/test`); + return unwrap<{ success: boolean; message: string }>(res); } catch (err) { console.error('Failed to test connection:', err); throw err; diff --git a/web/locales/en/common.ts b/web/locales/en/common.ts index aa1ad6f47..57af10fb7 100644 --- a/web/locales/en/common.ts +++ b/web/locales/en/common.ts @@ -459,6 +459,7 @@ export const CommonEn = { app_management: 'App Management', awel_workflow: 'AWEL Workflow', prompts: 'Prompts', + connectors: 'Connectors', // Skills page skills_page_subtitle: 'Pre-packaged and reusable best practices and tools for your agents', skills_search_placeholder: 'Search skills', diff --git a/web/locales/zh/common.ts b/web/locales/zh/common.ts index 689a65959..3f7ed98af 100644 --- a/web/locales/zh/common.ts +++ b/web/locales/zh/common.ts @@ -462,6 +462,7 @@ export const CommonZh: Resources['translation'] = { app_management: '应用管理', awel_workflow: 'AWEL 工作流', prompts: '提示词', + connectors: '连接器管理', // Skills page skills_page_subtitle: '为您的智能体提供预封装且可重复的最佳实践与工具', skills_search_placeholder: '搜索技能', diff --git a/web/new-components/connector/useConfirmPolling.ts b/web/new-components/connector/useConfirmPolling.ts index a915b7f8d..95ddd1cdb 100644 --- a/web/new-components/connector/useConfirmPolling.ts +++ b/web/new-components/connector/useConfirmPolling.ts @@ -1,3 +1,4 @@ +import axios from '@/utils/ctx-axios'; import { useCallback, useEffect, useRef, useState } from 'react'; import { ConfirmActionRequest, PendingConfirmation } from './types'; @@ -23,10 +24,10 @@ export function useConfirmPolling({ const fetchPending = useCallback(async () => { try { - const res = await fetch('/api/v2/serve/connectors/pending-confirms'); - if (!res.ok) return; - const data: PendingConfirmation[] = await res.json(); - if (data.length > 0) { + const data = (await axios.get('/api/v2/serve/connectors/pending-confirms')) as + | PendingConfirmation[] + | undefined; + if (data && data.length > 0) { const first = data[0]; if (first.confirm_id !== currentConfirmIdRef.current) { currentConfirmIdRef.current = first.confirm_id; @@ -63,11 +64,7 @@ export function useConfirmPolling({ approved, }; try { - await fetch('/api/v2/serve/connectors/confirm', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); + await axios.post('/api/v2/serve/connectors/confirm', body); } catch {} currentConfirmIdRef.current = null; setPendingConfirmation(null);