diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TaskCenterApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TaskCenterApplicationServiceImpl.java index 48fa2f71..cf0e90be 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TaskCenterApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/TaskCenterApplicationServiceImpl.java @@ -9,6 +9,7 @@ import com.xspaceagi.agent.core.adapter.dto.TryReqDto; import com.xspaceagi.agent.core.adapter.dto.WorkflowExecuteRequestDto; import com.xspaceagi.agent.core.adapter.dto.config.ModelConfigDto; import com.xspaceagi.agent.core.adapter.dto.config.workflow.WorkflowConfigDto; +import com.xspaceagi.agent.core.adapter.repository.entity.AgentComponentConfig; import com.xspaceagi.agent.core.adapter.repository.entity.Published; import com.xspaceagi.agent.core.infra.component.agent.dto.AgentExecuteResult; import com.xspaceagi.system.application.dto.SendNotifyMessageDto; @@ -84,7 +85,7 @@ public class TaskCenterApplicationServiceImpl implements TaskExecuteService { } private void executeWorkflowTask(MonoSink sink, ScheduleTaskDto scheduleTask) { - WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(Long.parseLong(scheduleTask.getTargetId()), scheduleTask.getSpaceId(), true); + WorkflowConfigDto workflowConfigDto = workflowApplicationService.queryPublishedWorkflowConfig(Long.parseLong(scheduleTask.getTargetId()), null, true); if (workflowConfigDto == null) { sink.error(BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentWorkflowOffline)); return; @@ -133,13 +134,22 @@ public class TaskCenterApplicationServiceImpl implements TaskExecuteService { } }); } + mergeSelectedComponents(selectedComponents, params.get("selectedComponents")); TryReqDto tryReqDto = new TryReqDto(); tryReqDto.setDebug(false); tryReqDto.setConversationId(conversation.getId()); tryReqDto.setMessage(params.get("message").toString()); tryReqDto.setSelectedComponents(selectedComponents); + List skillIds = toLongList(params.get("skillIds")); + if (!skillIds.isEmpty()) { + tryReqDto.setSkillIds(skillIds); + } + Long modelId = toLong(params.get("modelId")); + if (modelId != null) { + tryReqDto.setModelId(modelId); + } try { - if (YesOrNoEnum.Y.getKey().equals(conversation.getAgent().getAllowOtherModel())) { + if (tryReqDto.getModelId() == null && YesOrNoEnum.Y.getKey().equals(conversation.getAgent().getAllowOtherModel())) { List modelConfigDtos = agentApplicationService.queryUserCanSelectModelListForAgent(conversation.getUserId(), conversation.getAgentId()); if (!CollectionUtils.isEmpty(modelConfigDtos)) { tryReqDto.setModelId(modelConfigDtos.get(0).getId()); @@ -154,6 +164,94 @@ public class TaskCenterApplicationServiceImpl implements TaskExecuteService { executeAgentTask0(sink, requestContext, tryReqDto, scheduleTask, 0); } + private void mergeSelectedComponents(List selectedComponents, Object rawSelectedComponents) { + if (!(rawSelectedComponents instanceof Collection rawList)) { + return; + } + Set existing = new HashSet<>(); + for (TryReqDto.SelectedComponentDto item : selectedComponents) { + if (item.getId() != null && item.getType() != null) { + existing.add(item.getType().name() + ":" + item.getId()); + } + } + for (Object rawItem : rawList) { + TryReqDto.SelectedComponentDto selectedComponentDto = parseSelectedComponent(rawItem); + if (selectedComponentDto == null || selectedComponentDto.getId() == null || selectedComponentDto.getType() == null) { + continue; + } + String key = selectedComponentDto.getType().name() + ":" + selectedComponentDto.getId(); + if (existing.add(key)) { + selectedComponents.add(selectedComponentDto); + } + } + } + + private TryReqDto.SelectedComponentDto parseSelectedComponent(Object rawItem) { + if (rawItem instanceof TryReqDto.SelectedComponentDto selectedComponentDto) { + return selectedComponentDto.getId() != null && selectedComponentDto.getType() != null ? selectedComponentDto : null; + } + if (!(rawItem instanceof Map map)) { + return null; + } + Long id = toLong(map.get("id")); + AgentComponentConfig.Type type = toComponentType(map.get("type")); + if (id == null || type == null) { + return null; + } + TryReqDto.SelectedComponentDto selectedComponentDto = new TryReqDto.SelectedComponentDto(); + selectedComponentDto.setId(id); + selectedComponentDto.setType(type); + return selectedComponentDto; + } + + private AgentComponentConfig.Type toComponentType(Object value) { + if (value instanceof AgentComponentConfig.Type type) { + return type; + } + if (value == null) { + return null; + } + String typeName = value.toString(); + for (AgentComponentConfig.Type type : AgentComponentConfig.Type.values()) { + if (type.name().equalsIgnoreCase(typeName)) { + return type; + } + } + return null; + } + + private List toLongList(Object value) { + if (!(value instanceof Collection collection)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Object item : collection) { + Long parsed = toLong(item); + if (parsed != null) { + result.add(parsed); + } + } + return result; + } + + private Long toLong(Object value) { + if (value instanceof Number number) { + return number.longValue(); + } + if (value == null) { + return null; + } + String text = value.toString(); + if (text.isBlank()) { + return null; + } + try { + return Long.parseLong(text); + } catch (NumberFormatException e) { + return null; + } + } + private void executeAgentTask0(MonoSink sink, RequestContext requestContext, TryReqDto tryReqDto, ScheduleTaskDto scheduleTask, int times) { RequestContext.set(requestContext); AtomicBoolean retry = new AtomicBoolean(false); diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/TaskCenterController.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/TaskCenterController.java index 283cd5d1..d5e0a55c 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/TaskCenterController.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-ui/src/main/java/com/xspaceagi/agent/web/ui/controller/TaskCenterController.java @@ -78,9 +78,6 @@ public class TaskCenterController { if (publishedDto == null) { return ReqResult.error("Target object does not exist or has been removed"); } - if (!publishedDto.getSpaceId().equals(scheduleTaskAddDto.getSpaceId())) { - return ReqResult.error("Target object space ID does not match task space ID"); - } ScheduleTaskDto scheduleTaskDto = new ScheduleTaskDto(); BeanUtils.copyProperties(scheduleTaskAddDto, scheduleTaskDto); scheduleTaskDto.setTaskId(UUID.randomUUID().toString().replace("-", "")); diff --git a/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AutomationPage.tsx b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AutomationPage.tsx new file mode 100644 index 00000000..f90b6539 --- /dev/null +++ b/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/AutomationPage.tsx @@ -0,0 +1,1446 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Button, + ConfigProvider, + DatePicker, + Drawer, + Dropdown, + Empty, + Form, + Input, + Modal, + Popconfirm, + Segmented, + Select, + Spin, + Switch, + Tooltip, + message, +} from "antd"; +import type { MenuProps } from "antd"; +import { + BellOutlined, + CalendarOutlined, + CheckSquareOutlined, + ClockCircleOutlined, + DeleteOutlined, + EditOutlined, + FileTextOutlined, + HistoryOutlined, + MoreOutlined, + PlayCircleOutlined, + PlusOutlined, + ReloadOutlined, + RobotOutlined, + ThunderboltOutlined, +} from "@ant-design/icons"; +import dayjs, { Dayjs } from "dayjs"; +import "dayjs/locale/zh-cn"; +import zhCN from "antd/locale/zh_CN"; +import { + AutomationAgentDetail, + AutomationArgDetail, + AutomationCronGroup, + AutomationRunLog, + AutomationSelectedComponent, + AutomationSpace, + AutomationTarget, + AutomationTargetType, + AutomationTask, + AutomationTaskCreateParams, + AutomationTemplate, + AutomationWorkflowDetail, + createAutomationTask, + deleteAutomationTask, + disableAutomationTask, + enableAutomationTask, + executeAutomationTask, + fetchAutomationAgentDetail, + fetchAutomationCronList, + fetchAutomationRunLogDetail, + fetchAutomationRunLogs, + fetchAutomationSpaces, + fetchAutomationTargets, + fetchAutomationTasks, + fetchAutomationWorkflowDetail, + updateAutomationTask, +} from "../../services/core/automation"; +import { + AiManualComponent, + AiSkillMention, + fetchAtSkills, +} from "../../services/core/aiChat"; +import styles from "../../styles/components/AutomationPage.module.css"; + +dayjs.locale("zh-cn"); + +type AutomationViewMode = "list" | "templates"; +type FrequencyMode = "cycle" | "interval" | "once"; + +const TARGET_PAGE_SIZE = 80; + +const FALLBACK_CRON_GROUPS: AutomationCronGroup[] = [ + { + typeName: "每天", + items: [ + { cron: "0 0 9 * * ?", desc: "09:00" }, + { cron: "0 0 12 * * ?", desc: "12:00" }, + { cron: "0 0 18 * * ?", desc: "18:00" }, + { cron: "0 0 21 * * ?", desc: "21:00" }, + ], + }, + { + typeName: "每周", + items: [ + { cron: "0 0 9 ? * MON", desc: "周一 09:00" }, + { cron: "0 0 9 ? * FRI", desc: "周五 09:00" }, + { cron: "0 0 10 ? * SUN", desc: "周日 10:00" }, + ], + }, + { + typeName: "每月", + items: [ + { cron: "0 0 9 1 * ?", desc: "每月 1 日 09:00" }, + { cron: "0 0 9 15 * ?", desc: "每月 15 日 09:00" }, + ], + }, + { + typeName: "按间隔", + items: [ + { cron: "0 0/5 * * * ?", desc: "每 5 分钟" }, + { cron: "0 0/10 * * * ?", desc: "每 10 分钟" }, + { cron: "0 0/30 * * * ?", desc: "每 30 分钟" }, + { cron: "0 0 * * * ?", desc: "每小时" }, + ], + }, +]; + +const AUTOMATION_TEMPLATES: AutomationTemplate[] = [ + { + id: "daily-ai-news", + name: "每日 AI 新闻推送", + icon: "news", + cronHint: "daily", + description: "关注当天 AI 领域的重要动态,侧重 AI 产品、模型和行业进展。", + prompt: + "请整理今天 AI 领域的重要新闻,按重要程度列出 5 条,并分别给出简短解读和可能影响。", + }, + { + id: "daily-english-words", + name: "每日 5 个英语单词", + icon: "language", + cronHint: "daily", + description: "每天推荐 5 个高频实用英语单词,包含例句和记忆提示。", + prompt: + "请推荐 5 个高频实用英语单词,给出中文释义、英文例句、使用场景和记忆提示。", + }, + { + id: "bedtime-story", + name: "每日儿童睡前故事", + icon: "moon", + cronHint: "daily", + description: "生成 3-5 分钟可读的温和睡前故事,适合儿童阅读。", + prompt: + "请生成一篇适合儿童睡前阅读的温和故事,控制在 3 到 5 分钟内读完,结尾要积极温暖。", + }, + { + id: "weekly-work-report", + name: "每周工作周报", + icon: "report", + cronHint: "weekly", + description: "每周汇总工作进展、风险、下周计划和需要协作的事项。", + prompt: + "请帮我整理本周工作周报,包括完成事项、关键进展、风险问题、下周计划和需要协作的事项。", + }, + { + id: "classic-movie", + name: "经典电影推荐", + icon: "movie", + cronHint: "weekly", + description: "推荐一部高分经典电影,简要介绍剧情、亮点和适合人群。", + prompt: + "请推荐一部高分经典电影,介绍剧情概要、推荐理由、适合人群,并补充一句观影提示。", + }, + { + id: "today-history", + name: "历史上的今天", + icon: "calendar", + cronHint: "daily", + description: "从科技、电影、音乐等领域挑选一件历史事件进行介绍。", + prompt: + "请从科技、电影、音乐、文化等领域挑选一件历史上的今天发生的重要事件,并用通俗语言介绍背景和影响。", + }, + { + id: "daily-why", + name: "每日一个为什么", + icon: "question", + cronHint: "daily", + description: "每天抛出一个有趣问题,先提问再解答,适合知识启发。", + prompt: + "请提出一个有趣的“为什么”问题,然后用清晰、有趣、适合普通读者理解的方式解答。", + }, + { + id: "family-reminder", + name: "父母联系提醒", + icon: "alarm", + cronHint: "weekly", + description: "定期提醒联系家人,并给出适合聊天的话题。", + prompt: "请提醒我联系父母,并给出 3 个适合今天聊天的话题,语气自然、温暖。", + }, + { + id: "health-check", + name: "体检预约提醒", + icon: "hospital", + cronHint: "once", + description: "在指定时间提醒确认体检预约、材料和注意事项。", + prompt: + "请提醒我确认体检预约信息,并列出体检前需要准备的材料、注意事项和当天安排。", + }, + { + id: "interview-prep", + name: "面试准备提醒", + icon: "chat", + cronHint: "daily", + description: "工作日提醒复习面试题、项目经历和模拟问答。", + prompt: + "请帮我准备今天的面试复习计划,包括技术题、项目经历梳理、自我介绍和模拟问答。", + }, + { + id: "meeting-prep", + name: "会议前准备", + icon: "checklist", + cronHint: "daily", + description: "会议前整理议题、目标、风险和需要确认的问题。", + prompt: + "请帮我整理会议前准备清单,包括会议目标、关键议题、需要确认的问题、风险和预期产出。", + }, + { + id: "pet-wallpaper", + name: "可爱萌宠手机壁纸", + icon: "image", + cronHint: "weekly", + description: "随机从不同风格中挑选一种,生成适合手机壁纸的创意提示。", + prompt: + "请随机挑选一种可爱萌宠壁纸风格,并生成适合手机壁纸创作的详细描述提示词。", + }, +]; + +function getTargetId(target?: AutomationTarget | null): number { + return Number(target?.targetId || target?.id || 0); +} + +function getTaskSortTime(task: AutomationTask): number { + const value = + task.modified || task.lockTime || task.latestExecTime || task.created; + if (!value) return 0; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? 0 : date.getTime(); +} + +function formatDateTime(value?: string | null): string { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + return date.toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatFullDateTime(value?: string | null): string { + if (!value) return "-"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + return date.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); +} + +function formatNextTime(value?: string | null): string { + if (!value) return "等待调度"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return String(value); + const diffMs = date.getTime() - Date.now(); + if (diffMs <= 0) return "即将开始"; + const minutes = Math.ceil(diffMs / 60000); + if (minutes < 60) return `${minutes}分钟后开始`; + const hours = Math.ceil(minutes / 60); + if (hours < 24) return `${hours}小时后开始`; + const days = Math.ceil(hours / 24); + if (days <= 7) return `${days}天后开始`; + return formatDateTime(value); +} + +function getStatusMeta(status?: string) { + const normalized = (status || "").toUpperCase(); + switch (normalized) { + case "EXECUTING": + return { label: "执行中", tone: "running", ended: false }; + case "CREATE": + return { label: "已安排", tone: "waiting", ended: false }; + case "CONTINUE": + return { label: "待下次执行", tone: "success", ended: false }; + case "FAIL": + return { label: "执行失败", tone: "failed", ended: false }; + case "CANCEL": + return { label: "已停用", tone: "ended", ended: true }; + case "COMPLETE": + case "OVERFLOW_MAX_EXEC_TIMES": + return { label: "已结束", tone: "ended", ended: true }; + default: + return { label: status || "未知", tone: "waiting", ended: false }; + } +} + +function isIntervalGroup(group: AutomationCronGroup): boolean { + return /固定|间隔|Fixed/i.test(group.typeName || ""); +} + +function getCronMode(cron?: string | null, groups: AutomationCronGroup[] = []) { + const group = groups.find((item) => + item.items?.some((cronItem) => cronItem.cron === cron), + ); + if (!group) return "cycle" as FrequencyMode; + return isIntervalGroup(group) ? "interval" : "cycle"; +} + +function getCronLabel( + cron?: string | null, + groups: AutomationCronGroup[] = [], +) { + if (!cron) return "单次"; + const group = groups.find((item) => + item.items?.some((cronItem) => cronItem.cron === cron), + ); + const item = group?.items?.find((cronItem) => cronItem.cron === cron); + if (!group || !item) return cron; + return `${group.typeName} ${item.desc || ""}`.trim(); +} + +function findDefaultCron( + groups: AutomationCronGroup[], + mode: FrequencyMode, + hint?: AutomationTemplate["cronHint"], +): string | undefined { + const candidates = groups.filter((group) => + mode === "interval" ? isIntervalGroup(group) : !isIntervalGroup(group), + ); + if (mode === "cycle" && hint === "daily") { + const daily = candidates.find((group) => + /每天|每日|Daily/i.test(group.typeName), + ); + const nine = daily?.items?.find((item) => item.cron?.startsWith("0 0 9 ")); + return nine?.cron || daily?.items?.[0]?.cron; + } + if (mode === "cycle" && hint === "weekly") { + const weekly = candidates.find((group) => + /每周|Weekly/i.test(group.typeName), + ); + return ( + weekly?.items?.find((item) => /FRI|五/.test(item.cron || item.desc || "")) + ?.cron || weekly?.items?.[0]?.cron + ); + } + return candidates[0]?.items?.[0]?.cron || groups[0]?.items?.[0]?.cron; +} + +function getArgName(arg: AutomationArgDetail): string { + return ( + arg.name || + arg.displayName || + (arg.key !== undefined ? String(arg.key) : "") || + "" + ); +} + +function getArgLabel(arg: AutomationArgDetail): string { + return arg.displayName || arg.name || getArgName(arg) || "参数"; +} + +function shouldRequireArg(arg: AutomationArgDetail): boolean { + return Boolean(arg.require || arg.required); +} + +function cleanObject(value?: Record): Record { + const result: Record = {}; + Object.entries(value || {}).forEach(([key, item]) => { + if (item === undefined || item === null || item === "") return; + result[key] = item; + }); + return result; +} + +function parseVariableValue( + value: unknown, + arg?: AutomationArgDetail, +): unknown { + const type = String(arg?.dataType || arg?.inputType || "").toLowerCase(); + if (typeof value !== "string") return value; + if (!value.trim()) return value; + if (type.includes("object") || type.includes("array")) { + try { + return JSON.parse(value); + } catch { + return value; + } + } + return value; +} + +function parseSelectedComponentValue( + value: string, +): AutomationSelectedComponent | null { + const [type, id] = value.split(":"); + const numericId = Number(id); + if (!type || !Number.isFinite(numericId)) return null; + return { type, id: numericId }; +} + +function getComponentValue(component: AiManualComponent): string { + return `${component.type}:${component.id}`; +} + +function getCronOptions(items?: AutomationCronGroup["items"]) { + return (items || []) + .filter((item) => item.cron) + .map((item) => ({ + value: item.cron as string, + label: item.desc || item.cron, + })); +} + +function filterSelectableComponents(components?: AiManualComponent[]) { + const allowed = new Set(["mcp", "workflow", "agent", "plugin"]); + return (components || []).filter((component) => + allowed.has(String(component.type || "").toLowerCase()), + ); +} + +function getModalPopupContainer(trigger?: HTMLElement): HTMLElement { + return trigger?.closest(".ant-modal-content") || document.body; +} + +function TemplateIcon({ icon }: { icon: string }) { + const iconMap: Record = { + news: , + language: , + moon: , + report: , + movie: , + calendar: , + question: , + alarm: , + hospital: , + chat: , + checklist: , + image: , + }; + return <>{iconMap[icon] || }; +} + +function VariableFields({ args }: { args: AutomationArgDetail[] }) { + if (!args.length) return null; + + return ( +
+
参数配置
+ {args.map((arg, index) => { + const name = getArgName(arg) || `param_${index}`; + const type = String(arg.dataType || arg.inputType || ""); + const isBoolean = /boolean|bool/i.test(type); + const isLarge = /object|array|json/i.test(type); + return ( + + {isBoolean ? ( + + )} + + ); + })} +
+ ); +} + +interface AutomationTaskModalProps { + open: boolean; + spaces: AutomationSpace[]; + cronGroups: AutomationCronGroup[]; + editingTask?: AutomationTask | null; + template?: AutomationTemplate | null; + onCancel: () => void; + onSubmit: (params: AutomationTaskCreateParams, id?: number) => Promise; +} + +function AutomationTaskModal({ + open, + spaces, + cronGroups, + editingTask, + template, + onCancel, + onSubmit, +}: AutomationTaskModalProps) { + const [form] = Form.useForm(); + const [targets, setTargets] = useState([]); + const [targetLoading, setTargetLoading] = useState(false); + const [detailLoading, setDetailLoading] = useState(false); + const [agentDetail, setAgentDetail] = useState( + null, + ); + const [workflowDetail, setWorkflowDetail] = + useState(null); + const [skills, setSkills] = useState([]); + const [skillsLoading, setSkillsLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const targetType = Form.useWatch("targetType", form) as + | AutomationTargetType + | undefined; + const targetId = Form.useWatch("targetId", form) as number | undefined; + const frequencyMode = Form.useWatch("frequencyMode", form) as + | FrequencyMode + | undefined; + const cronGroupName = Form.useWatch("cronGroupName", form) as + | string + | undefined; + const availableCronGroups = + cronGroups.length > 0 ? cronGroups : FALLBACK_CRON_GROUPS; + + const cycleGroups = useMemo( + () => availableCronGroups.filter((group) => !isIntervalGroup(group)), + [availableCronGroups], + ); + const intervalGroups = useMemo( + () => availableCronGroups.filter((group) => isIntervalGroup(group)), + [availableCronGroups], + ); + const activeCronGroups = + frequencyMode === "interval" ? intervalGroups : cycleGroups; + const currentCronItems = useMemo(() => { + return ( + activeCronGroups.find((group) => group.typeName === cronGroupName) + ?.items || + activeCronGroups[0]?.items || + [] + ); + }, [activeCronGroups, cronGroupName]); + const currentCronOptions = useMemo( + () => getCronOptions(currentCronItems), + [currentCronItems], + ); + const defaultTaskSpaceId = useMemo( + () => + spaces + .map((space) => Number(space.id)) + .find((spaceId) => Number.isFinite(spaceId) && spaceId > 0), + [spaces], + ); + + const currentArgs = useMemo(() => { + if (targetType === "Agent") return agentDetail?.variables || []; + if (targetType === "Workflow") return workflowDetail?.inputArgs || []; + return []; + }, [agentDetail, targetType, workflowDetail]); + + const selectableComponents = useMemo( + () => filterSelectableComponents(agentDetail?.manualComponents), + [agentDetail], + ); + + const loadTargets = useCallback( + async (keyword = "") => { + if (!open || !targetType) return; + setTargetLoading(true); + try { + const result = await fetchAutomationTargets({ + targetType, + kw: keyword, + page: 1, + pageSize: TARGET_PAGE_SIZE, + }); + setTargets(result.records || []); + } catch (error) { + console.error("[AutomationPage] Failed to load targets:", error); + setTargets([]); + } finally { + setTargetLoading(false); + } + }, + [open, targetType], + ); + + const loadTargetDetail = useCallback(async () => { + if (!open || !targetId || !targetType) return; + setDetailLoading(true); + setAgentDetail(null); + setWorkflowDetail(null); + try { + if (targetType === "Agent") { + const detail = await fetchAutomationAgentDetail(Number(targetId)); + setAgentDetail(detail); + } else { + const detail = await fetchAutomationWorkflowDetail(Number(targetId)); + setWorkflowDetail(detail); + } + } catch (error) { + console.error("[AutomationPage] Failed to load target detail:", error); + message.error("任务目标详情加载失败"); + } finally { + setDetailLoading(false); + } + }, [open, targetId, targetType]); + + const loadSkills = useCallback(async (keyword = "") => { + setSkillsLoading(true); + try { + const result = await fetchAtSkills({ + type: "all", + kw: keyword, + page: 1, + pageSize: 40, + }); + setSkills(result || []); + } catch (error) { + console.error("[AutomationPage] Failed to load skills:", error); + setSkills([]); + } finally { + setSkillsLoading(false); + } + }, []); + + useEffect(() => { + if (!open) return; + const nextTargetType = editingTask?.targetType || "Agent"; + const params = (editingTask?.params || {}) as Record; + const nextFrequencyMode = + editingTask?.maxExecTimes === 1 + ? "once" + : getCronMode(editingTask?.cron, availableCronGroups); + const nextCron = + editingTask?.cron || + findDefaultCron( + availableCronGroups, + nextFrequencyMode, + template?.cronHint, + ); + const nextGroup = availableCronGroups.find((group) => + group.items?.some((item) => item.cron === nextCron), + ); + + form.setFieldsValue({ + spaceId: editingTask?.spaceId || defaultTaskSpaceId, + targetType: nextTargetType, + targetId: editingTask?.targetId + ? Number(editingTask.targetId) + : undefined, + taskName: editingTask?.taskName || template?.name || "", + message: String(params.message || template?.prompt || ""), + keepConversation: Number(params.keepConversation || 0) === 1, + skillIds: Array.isArray(params.skillIds) ? params.skillIds : [], + selectedComponents: Array.isArray(params.selectedComponents) + ? (params.selectedComponents as AutomationSelectedComponent[]) + .map((item) => `${item.type}:${item.id}`) + .filter(Boolean) + : [], + variables: params.variables || {}, + frequencyMode: nextFrequencyMode, + cronGroupName: nextGroup?.typeName, + cron: nextCron, + lockTime: + editingTask?.maxExecTimes === 1 && editingTask.lockTime + ? dayjs(editingTask.lockTime) + : template?.cronHint === "once" + ? dayjs().add(1, "day").hour(9).minute(0).second(0) + : null, + }); + setAgentDetail(null); + setWorkflowDetail(null); + loadSkills(); + }, [ + availableCronGroups, + defaultTaskSpaceId, + editingTask, + form, + loadSkills, + open, + template, + ]); + + useEffect(() => { + if (!open) return; + loadTargets(); + }, [loadTargets, open]); + + useEffect(() => { + if (!open || !frequencyMode || frequencyMode === "once") return; + const groups = frequencyMode === "interval" ? intervalGroups : cycleGroups; + const currentGroup = groups.find( + (group) => group.typeName === cronGroupName, + ); + const nextGroup = currentGroup || groups[0]; + const currentCron = form.getFieldValue("cron"); + const currentCronExists = nextGroup?.items?.some( + (item) => item.cron === currentCron, + ); + if (nextGroup && (!currentGroup || !currentCronExists)) { + form.setFieldsValue({ + cronGroupName: nextGroup.typeName, + cron: getCronOptions(nextGroup.items)[0]?.value, + }); + } + }, [cronGroupName, cycleGroups, form, frequencyMode, intervalGroups, open]); + + useEffect(() => { + loadTargetDetail(); + }, [loadTargetDetail]); + + const handleSpaceOrTypeChange = () => { + form.setFieldsValue({ + spaceId: undefined, + targetId: undefined, + selectedComponents: [], + variables: {}, + }); + setTargets([]); + setAgentDetail(null); + setWorkflowDetail(null); + }; + + const buildVariables = ( + rawVariables: Record, + args: AutomationArgDetail[], + ) => { + const argsByName = new Map(args.map((arg) => [getArgName(arg), arg])); + return Object.fromEntries( + Object.entries(cleanObject(rawVariables)).map(([key, value]) => [ + key, + parseVariableValue(value, argsByName.get(key)), + ]), + ); + }; + + const handleSubmit = async () => { + try { + const values = (await form.validateFields()) as { + spaceId: number; + targetType: AutomationTargetType; + targetId: number; + taskName: string; + message?: string; + keepConversation?: boolean; + skillIds?: number[]; + selectedComponents?: string[]; + variables?: Record; + frequencyMode: FrequencyMode; + cron?: string; + lockTime?: Dayjs; + }; + + if (values.frequencyMode === "once") { + if (!values.lockTime) { + message.warning("请选择单次执行时间"); + return; + } + if (values.lockTime.isBefore(dayjs())) { + message.warning("单次执行时间不能早于当前时间"); + return; + } + } + + if (!values.spaceId) { + message.warning("所选任务目标缺少发布空间,请重新选择任务目标"); + return; + } + + const variables = buildVariables(values.variables || {}, currentArgs); + const isAgent = values.targetType === "Agent"; + const selectedComponents = (values.selectedComponents || []) + .map(parseSelectedComponentValue) + .filter(Boolean) as AutomationSelectedComponent[]; + + const payload: AutomationTaskCreateParams = { + spaceId: values.spaceId, + targetType: values.targetType, + targetId: String(values.targetId), + taskName: values.taskName.trim(), + cron: values.frequencyMode === "once" ? null : values.cron || null, + lockTime: + values.frequencyMode === "once" ? values.lockTime?.valueOf() : null, + maxExecTimes: values.frequencyMode === "once" ? 1 : null, + keepConversation: isAgent && values.keepConversation ? 1 : 0, + params: isAgent + ? { + message: (values.message || "").trim(), + variables, + selectedComponents, + skillIds: values.skillIds || [], + } + : variables, + }; + + setSubmitting(true); + await onSubmit(payload, editingTask?.id); + form.resetFields(); + } finally { + setSubmitting(false); + } + }; + + return ( + + 取消 + , + , + ]} + > + +
+ + + + + + +
+ + + + + ({ + value: Number(skill.targetId || skill.id), + label: skill.name, + })) + .filter((option) => option.value > 0)} + /> + + + ({ + value: group.typeName, + label: group.typeName, + }))} + /> + + +