fix(connector): wire frontend through ctx-axios + add sidebar entry + normalize field names

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.
This commit is contained in:
alan.cl
2026-05-27 10:51:32 +08:00
parent 6add267298
commit 9041733342
5 changed files with 55 additions and 41 deletions

View File

@@ -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() {
<EditOutlined className='text-orange-500' />
<span>{t('prompts')}</span>
</div>
<div
onClick={() => {
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'),
},
)}
>
<ApiOutlined className='text-violet-500' />
<span>{t('connectors')}</span>
</div>
<div
onClick={() => {
router.push('/construct/dbgpts');

View File

@@ -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<T>(url: string, options?: RequestInit): Promise<T> {
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<ConnectorInstance, 'id'> & { connector_id: string };
function normalizeConnector(raw: BackendConnector): ConnectorInstance {
const { connector_id, ...rest } = raw;
return { id: connector_id, ...rest } as ConnectorInstance;
}
function unwrap<T>(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<ConnectorCatalogEntry[]>(`${API_BASE}/types`)
.then(data => setTypes(data ?? []))
axios
.get(`${API_BASE}/types`)
.then(res => setTypes(unwrap<ConnectorCatalogEntry[]>(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<ConnectorInstance[]>(API_BASE)
.then(data => setConnectors(data ?? []))
axios
.get(API_BASE)
.then(res => {
const raw = unwrap<BackendConnector[]>(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<ConnectorInstance> => {
setLoading(true);
try {
return await apiFetch<ConnectorInstance>(API_BASE, {
method: 'POST',
body: JSON.stringify(data),
});
const res = await axios.post(API_BASE, data);
return normalizeConnector(unwrap<BackendConnector>(res));
} catch (err) {
console.error('Failed to create connector:', err);
throw err;
@@ -79,10 +85,8 @@ export function useUpdateConnector() {
async (id: string, data: Partial<CreateConnectorRequest>): Promise<ConnectorInstance> => {
setLoading(true);
try {
return await apiFetch<ConnectorInstance>(`${API_BASE}/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
const res = await axios.put(`${API_BASE}/${id}`, data);
return normalizeConnector(unwrap<BackendConnector>(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<void> => {
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;

View File

@@ -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',

View File

@@ -462,6 +462,7 @@ export const CommonZh: Resources['translation'] = {
app_management: '应用管理',
awel_workflow: 'AWEL 工作流',
prompts: '提示词',
connectors: '连接器管理',
// Skills page
skills_page_subtitle: '为您的智能体提供预封装且可重复的最佳实践与工具',
skills_search_placeholder: '搜索技能',

View File

@@ -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);