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