329 lines
9.3 KiB
TypeScript
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,
|
|
};
|
|
}
|