aura-web/src/pages/AgentEditor/hooks/useAgentEditor.ts

329 lines
9.3 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { FormInstance } from 'antd';
import { Agent, AgentAPI, Team, TeamAPI, AiModel, ModelAPI, ImageAPI } from '../../../api';
import { DEFAULT_AVATAR } from '../constants';
interface UseAgentEditorOptions {
id?: string;
isNew: boolean;
form: FormInstance;
message: any;
navigate: any;
}
export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentEditorOptions) {
const [agent, setAgent] = useState<Agent | null>(null);
const [saving, setSaving] = useState(false);
const [teams, setTeams] = useState<Team[]>([]);
const [models, setModels] = useState<AiModel[]>([]);
const [autoSaveStatus, setAutoSaveStatus] = useState<'saved' | 'dirty' | 'saving' | 'error'>('saved');
const [initModalOpen, setInitModalOpen] = useState(isNew);
const [selectedAvatar, setSelectedAvatar] = useState(DEFAULT_AVATAR);
const [avatarSelectorOpen, setAvatarSelectorOpen] = useState(false);
const [avatarUploading, setAvatarUploading] = useState(false);
const [agentName, setAgentName] = useState('');
const [skillEditorOpen, setSkillEditorOpen] = useState(false);
const [editingSkillId, setEditingSkillId] = useState<string | null>(null);
const pollTimer = useRef<number | null>(null);
const pollCount = useRef(0);
const hydratingRef = useRef(false);
const MAX_POLL_COUNT = 30;
const stopPolling = () => {
if (pollTimer.current) {
window.clearInterval(pollTimer.current);
pollTimer.current = null;
}
pollCount.current = 0;
};
const startPolling = () => {
pollCount.current = 0;
if (pollTimer.current) {
window.clearInterval(pollTimer.current);
}
pollTimer.current = window.setInterval(pollKnowledgeStatus, 4000);
pollKnowledgeStatus();
};
const pollKnowledgeStatus = async () => {
if (!id) return;
try {
const result = await AgentAPI.getKnowledgeStatus(id);
const files = result.files || [];
setAgent((prev) => {
if (!prev) return prev;
return { ...prev, knowledge: files };
});
const indexing = files.some((k) => k.status === 'pending' || k.status === 'indexing');
pollCount.current++;
if (!indexing || pollCount.current >= MAX_POLL_COUNT) {
stopPolling();
if (!indexing) {
refresh(false);
} else {
message.warning('部分文件处理超时,请稍后刷新页面查看结果');
}
}
} catch (e) {
pollCount.current++;
if (pollCount.current >= MAX_POLL_COUNT) {
stopPolling();
message.warning('知识库状态查询超时,请稍后刷新页面查看结果');
}
}
};
const refresh = async (force = false) => {
if (!id) return;
const data = await AgentAPI.detail(id);
setAgent(data);
if (force || autoSaveStatus !== 'dirty') {
hydratingRef.current = true;
form.setFieldsValue(data);
window.setTimeout(() => {
hydratingRef.current = false;
}, 0);
setAutoSaveStatus('saved');
setAgentName(data.name);
setSelectedAvatar(data.avatar || DEFAULT_AVATAR);
}
const indexing = data.knowledge?.some((k) => k.status === 'pending' || k.status === 'indexing');
if (indexing && !pollTimer.current) {
startPolling();
}
};
useEffect(() => {
TeamAPI.list()
.then(setTeams)
.catch(() => setTeams([]));
ModelAPI.list()
.then(setModels)
.catch(() => setModels([]));
if (isNew) {
setInitModalOpen(true);
setSelectedAvatar(DEFAULT_AVATAR);
setAgentName('');
form.setFieldsValue({
name: '',
description: '',
prompt: 'You are a helpful AI assistant.',
model: '',
temperature: 0.7,
visibility: 'private',
teamId: null,
});
} else {
setInitModalOpen(false);
refresh(true);
}
return () => {
if (pollTimer.current) {
window.clearInterval(pollTimer.current);
pollTimer.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const handleInitConfirm = async (values: any) => {
setSaving(true);
try {
const created = await AgentAPI.create({
...values,
avatar: selectedAvatar,
prompt: 'You are a helpful AI assistant.',
temperature: 0.7,
visibility: 'private',
});
message.success('初始化成功');
setInitModalOpen(false);
navigate(`/agents/${created.id}`, { replace: true });
} catch (e) {
message.error('创建失败');
} finally {
setSaving(false);
}
};
const handleSave = async (silent = false) => {
if (isNew) return;
const values = await form.validateFields();
if (!silent) setSaving(true);
setAutoSaveStatus('saving');
try {
const changedFields: Record<string, any> = {};
Object.keys(values).forEach((key) => {
const formValue = (values as any)[key];
const originalValue = (agent as any)?.[key];
if (formValue !== originalValue) {
changedFields[key] = formValue;
}
});
if (Object.keys(changedFields).length === 0) {
if (!silent) message.info('无变化');
setAutoSaveStatus('saved');
return;
}
const updatedAgent = await AgentAPI.update(id!, changedFields);
setAgent(updatedAgent);
form.setFieldsValue(updatedAgent);
if (!silent) message.success('已保存');
setAutoSaveStatus('saved');
} catch (e) {
setAutoSaveStatus('error');
if (!silent) message.error('保存失败');
} finally {
if (!silent) setSaving(false);
}
};
const uploadAvatar = async (file: File) => {
setAvatarUploading(true);
try {
const res = await ImageAPI.upload([file]);
const url = res.files?.[0]?.url;
if (!url) {
throw new Error('未获取到图片地址');
}
return url;
} finally {
setAvatarUploading(false);
}
};
const handleDeleteKnowledge = async (fileId: string) => {
if (!id) return;
try {
await AgentAPI.deleteKnowledge(id, fileId);
setAgent((prev) => {
if (!prev) return prev;
return {
...prev,
knowledge: (prev.knowledge || []).filter((k: any) => k.id !== fileId),
};
});
message.success('已删除');
} catch (e: any) {
message.error('删除失败:' + (e?.message ?? e));
}
};
const beforeUploadKnowledge = async (file: any) => {
if (!id) {
message.warning('请先保存智能体基础信息后再上传');
return false;
}
try {
const result = await AgentAPI.uploadKnowledge(id, [file as File]);
setAgent((prev) => {
if (!prev) return prev;
const existingIds = new Set((prev.knowledge || []).map((k: any) => k.id));
const uniqueNewFiles = (result.files || []).filter((f: any) => !existingIds.has(f.id));
return {
...prev,
knowledge: [...uniqueNewFiles, ...(prev.knowledge || [])],
};
});
startPolling();
message.success(`${file.name} 已上传,正在建索引…`);
} catch (e: any) {
message.error('上传失败:' + (e?.message ?? e));
}
return false;
};
const beforeUploadInitAvatar = async (file: any) => {
try {
const url = await uploadAvatar(file as File);
setSelectedAvatar(url);
message.success('头像上传成功');
} catch (e: any) {
message.error('头像上传失败:' + (e?.message ?? e));
}
return false;
};
const handleAvatarSelect = async (avatarUrl: string) => {
if (!id) return;
try {
await AgentAPI.updateAvatar(id, avatarUrl);
setAgent((prev) => {
if (!prev) return prev;
return { ...prev, avatar: avatarUrl };
});
setSelectedAvatar(avatarUrl);
message.success('头像已更新');
setAvatarSelectorOpen(false);
} catch (e: any) {
message.error('头像更新失败:' + (e?.message ?? e));
}
};
const beforeUploadEditAvatar = async (file: any) => {
if (!id) return false;
try {
const url = await uploadAvatar(file as File);
await AgentAPI.updateAvatar(id, url);
setAgent((prev) => {
if (!prev) return prev;
return { ...prev, avatar: url };
});
setSelectedAvatar(url);
message.success('头像已更新');
setAvatarSelectorOpen(false);
} catch (e: any) {
message.error('头像上传失败:' + (e?.message ?? e));
}
return false;
};
const liveAgent = { ...(agent ?? {}), ...(form.getFieldsValue() as Agent) } as Agent;
const currentName = agent?.name || liveAgent?.name || agentName || undefined;
const markDirty = () => {
if (hydratingRef.current) return;
setAutoSaveStatus('dirty');
};
return {
agent,
saving,
teams,
models,
autoSaveStatus,
initModalOpen,
setInitModalOpen,
selectedAvatar,
setSelectedAvatar,
avatarSelectorOpen,
setAvatarSelectorOpen,
avatarUploading,
agentName,
setAgentName,
skillEditorOpen,
setSkillEditorOpen,
editingSkillId,
setEditingSkillId,
hydratingRef,
refresh,
handleInitConfirm,
handleSave,
beforeUploadKnowledge,
beforeUploadInitAvatar,
beforeUploadEditAvatar,
handleAvatarSelect,
handleDeleteKnowledge,
liveAgent,
currentName,
markDirty,
};
}