Files
TERES_web_frontend/src/hooks/agent-hooks.ts
guangfei.zhao d24b371929 feat(knowledge): add pipeline support and build mode selection
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
2025-11-06 23:06:23 +08:00

300 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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