refactor(configuration): reorganize naive config form with pipeline selector feat(form): add RadioFormField component for build mode selection docs: add ahooks usage guide for common patterns style: update app title and favicon chore: clean up unused agent interfaces
300 lines
9.6 KiB
TypeScript
300 lines
9.6 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
||
import agentService from '@/services/agent_service';
|
||
import type { IAgent, IGraph } from '@/interfaces/database/agent';
|
||
import type { IAgentPaginationParams, IAgentCreateRequestBody, IAgentSettingRequestBody } from '@/interfaces/request/agent';
|
||
import logger from '@/utils/logger';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { useSnackbar } from '@/hooks/useSnackbar';
|
||
|
||
export interface UseAgentListState {
|
||
agents: IAgent[];
|
||
total: number;
|
||
loading: boolean;
|
||
error: string | null;
|
||
currentPage: number;
|
||
pageSize: number;
|
||
keywords: string;
|
||
orderby?: string;
|
||
desc?: boolean;
|
||
}
|
||
|
||
export interface UseAgentListReturn extends UseAgentListState {
|
||
fetchAgents: (params?: IAgentPaginationParams) => Promise<void>;
|
||
setKeywords: (keywords: string) => void;
|
||
setCurrentPage: (page: number) => void;
|
||
setPageSize: (size: number) => void;
|
||
setOrder: (orderby?: string, desc?: boolean) => void;
|
||
refresh: () => Promise<void>;
|
||
}
|
||
|
||
/**
|
||
* 智能体列表钩子
|
||
* @param initialParams 初始参数
|
||
*/
|
||
export const useAgentList = (initialParams?: IAgentPaginationParams) => {
|
||
const [agents, setAgents] = useState<IAgent[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [currentPage, setCurrentPage] = useState(initialParams?.page || 1);
|
||
const [pageSize, setPageSize] = useState(initialParams?.page_size || 10);
|
||
const [keywords, setKeywords] = useState(initialParams?.keywords || '');
|
||
|
||
const fetchAgentList = useCallback(async (params?: IAgentPaginationParams) => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const envMode = import.meta.env.MODE;
|
||
let response: any = null;
|
||
if (envMode === 'flask') {
|
||
response = await agentService.teamlistCanvas(params);
|
||
} else {
|
||
response = await agentService.listCanvas(params);
|
||
}
|
||
const res = response.data || {};
|
||
logger.info('useAgentList fetchAgentList', res);
|
||
const data = res.data
|
||
setAgents(data.canvas || []);
|
||
setTotal(data.total || 0);
|
||
} catch (err: any) {
|
||
setError(err.message || 'Failed to fetch agent list');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const refresh = useCallback(() => fetchAgentList({
|
||
keywords,
|
||
page: currentPage,
|
||
page_size: pageSize,
|
||
}), [keywords, currentPage, pageSize]);
|
||
|
||
useEffect(() => {
|
||
refresh();
|
||
}, [refresh]);
|
||
|
||
return {
|
||
agents,
|
||
total,
|
||
loading,
|
||
error,
|
||
currentPage,
|
||
pageSize,
|
||
keywords,
|
||
fetchAgents: fetchAgentList,
|
||
setKeywords,
|
||
setCurrentPage,
|
||
setPageSize,
|
||
refresh,
|
||
};
|
||
};
|
||
|
||
export function useAgentOperations() {
|
||
const { t } = useTranslation();
|
||
const { showMessage } = useSnackbar();
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const createAgent = useCallback(async (body: IAgentCreateRequestBody) => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const res = await agentService.setCanvas(body);
|
||
const newId = res?.data?.data?.id || res?.data?.id;
|
||
showMessage.success(t('agent.createAgentSuccess'));
|
||
return { success: true, id: newId } as const;
|
||
} catch (err: any) {
|
||
const errorMessage = err?.response?.data?.message || err?.message || t('agent.createAgentFailed');
|
||
setError(errorMessage);
|
||
showMessage.error(errorMessage);
|
||
return { success: false, error: errorMessage } as const;
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [t, showMessage]);
|
||
|
||
const editAgent = useCallback(async (data: Partial<IAgentSettingRequestBody>) => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const res = await agentService.settingAgent(data);
|
||
const code = res?.data?.code;
|
||
if (code === 0) {
|
||
showMessage.success(t('message.updated'));
|
||
return { success: true } as const;
|
||
}
|
||
const msg = res?.data?.message || t('common.operationFailed');
|
||
showMessage.error(msg);
|
||
return { success: false, error: msg } as const;
|
||
} catch (err: any) {
|
||
const errorMessage = err?.response?.data?.message || err?.message || t('common.operationFailed');
|
||
setError(errorMessage);
|
||
showMessage.error(errorMessage);
|
||
return { success: false, error: errorMessage } as const;
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [t, showMessage]);
|
||
|
||
const deleteAgents = useCallback(async (ids: string[]) => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const res = await agentService.removeCanvas(ids);
|
||
const code = res?.data?.code;
|
||
if (code === 0) {
|
||
showMessage.success(t('message.deleted'));
|
||
return { success: true } as const;
|
||
}
|
||
const msg = res?.data?.message || t('common.operationFailed');
|
||
showMessage.error(msg);
|
||
return { success: false, error: msg } as const;
|
||
} catch (err: any) {
|
||
const errorMessage = err?.response?.data?.message || err?.message || t('common.operationFailed');
|
||
setError(errorMessage);
|
||
showMessage.error(errorMessage);
|
||
return { success: false, error: errorMessage } as const;
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [t, showMessage]);
|
||
|
||
const clearError = useCallback(() => setError(null), []);
|
||
|
||
return { loading, error, createAgent, editAgent, deleteAgents, clearError };
|
||
}
|
||
|
||
/**
|
||
* 智能体详情钩子:拉取 Canvas 详情并解析出图(nodes/edges)
|
||
*/
|
||
export function useAgentDetail(id?: string) {
|
||
const [agent, setAgent] = useState<IAgent | null>(null);
|
||
const [graph, setGraph] = useState<IGraph | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const fetchDetail = useCallback(async () => {
|
||
if (!id) {
|
||
setError('Invalid agent id');
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const res = await agentService.getCanvas(id);
|
||
const body = res?.data ?? {};
|
||
// 服务端常见返回结构:{ code, data: { id, title, dsl } }
|
||
const data = (body.data ?? body) as IAgent;
|
||
const dsl = (data as any)?.dsl ?? {};
|
||
const g: IGraph | null = dsl?.graph;
|
||
setAgent(data);
|
||
setGraph(g);
|
||
logger.info('useAgentDetail fetched', { id, title: data?.title, nodes: g?.nodes?.length, edges: g?.edges?.length });
|
||
} catch (err: any) {
|
||
const msg = err?.response?.data?.message || err?.message || 'Failed to fetch agent detail';
|
||
setError(msg);
|
||
logger.error('useAgentDetail error', err);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [id]);
|
||
|
||
useEffect(() => {
|
||
fetchDetail();
|
||
}, [fetchDetail]);
|
||
|
||
return { agent, graph, loading, error, refresh: fetchDetail } as const;
|
||
};
|
||
|
||
export interface UseAgentCanvasOptions {
|
||
agent?: IAgent | null;
|
||
graph?: IGraph | null;
|
||
onReady?: (api: { fitView: () => void }) => void;
|
||
onAutoSaved?: (date: Date) => void;
|
||
disableNoteClick?: boolean;
|
||
}
|
||
|
||
export function useAgentCanvas({ agent, graph, onReady, onAutoSaved, disableNoteClick = true }: UseAgentCanvasOptions) {
|
||
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
||
const [hoverNode, setHoverNode] = useState<any | null>(null);
|
||
const [sanitized, setSanitized] = useState<IGraph | null>(null);
|
||
const instRef = ({} as any) as { current?: any };
|
||
|
||
const sanitizeGraph = useCallback((g?: IGraph | null): IGraph | null => {
|
||
if (!g) return null;
|
||
const nodes = (g.nodes || []).map((n: any) => ({ ...n, draggable: false, selectable: true }));
|
||
const edges = (g.edges || []).map((e: any) => {
|
||
const { sourceHandle, targetHandle, ...rest } = e || {};
|
||
return { ...rest };
|
||
});
|
||
return { nodes, edges };
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
setSanitized(sanitizeGraph(graph));
|
||
}, [graph, sanitizeGraph]);
|
||
|
||
const onInit = useCallback((inst: any) => {
|
||
try {
|
||
(instRef as any).current = inst;
|
||
onReady?.({ fitView: () => inst?.fitView?.({ padding: 0.2 }) });
|
||
inst?.fitView?.({ padding: 0.2 });
|
||
} catch (err) {
|
||
logger.error('useAgentCanvas onInit error', err);
|
||
}
|
||
}, [onReady]);
|
||
|
||
const onNodeClick = useCallback((_: any, node: any) => {
|
||
if (disableNoteClick && node?.type === 'noteNode') return;
|
||
setSelectedNode(node);
|
||
}, [disableNoteClick]);
|
||
|
||
const onPaneClick = useCallback(() => setSelectedNode(null), []);
|
||
const onNodeMouseEnter = useCallback((_: any, node: any) => setHoverNode(node), []);
|
||
|
||
// 30秒自动保存(基于当前画布),确保在卸载与依赖变更时清理定时器
|
||
const autoSaveTimerRef = useRef<number | null>(null);
|
||
useEffect(() => {
|
||
// 清理已有定时器,避免重复
|
||
if (autoSaveTimerRef.current) {
|
||
clearInterval(autoSaveTimerRef.current);
|
||
autoSaveTimerRef.current = null;
|
||
}
|
||
|
||
// 数据不完整时不创建定时器(Canvas 不可用或退出)
|
||
if (!agent?.id || !agent?.title || !sanitized) {
|
||
return;
|
||
}
|
||
|
||
autoSaveTimerRef.current = window.setInterval(async () => {
|
||
try {
|
||
const payload = {
|
||
id: agent.id,
|
||
title: agent.title,
|
||
dsl: { ...(agent.dsl || {}), graph: sanitized },
|
||
};
|
||
await agentService.setAgentDSL(payload as any);
|
||
onAutoSaved?.(new Date());
|
||
} catch (err) {
|
||
logger.error('autoSave failed', err);
|
||
}
|
||
}, 30000);
|
||
|
||
return () => {
|
||
if (autoSaveTimerRef.current) {
|
||
clearInterval(autoSaveTimerRef.current);
|
||
autoSaveTimerRef.current = null;
|
||
}
|
||
};
|
||
}, [agent?.id, agent?.title, agent?.dsl, sanitized, onAutoSaved]);
|
||
|
||
return {
|
||
sanitized,
|
||
selectedNode,
|
||
hoverNode,
|
||
onInit,
|
||
onNodeClick,
|
||
onPaneClick,
|
||
onNodeMouseEnter,
|
||
} as const;
|
||
} |