'use client'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import React, { useEffect, useMemo, useState } from 'react'; import { AlertCircle, ArrowRight, Clock3, FolderDown, ListTodo, Loader2, Play, Plus, RefreshCw, Trash2, X } from 'lucide-react'; import { formatOfficeTime } from '@/components/office/OfficeShared'; import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { addCronJob, deleteBackendTask, listBackendTasks, listCronJobs, removeCronJob, runCronJob, toggleCronJob } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { useChatStore } from '@/lib/store'; import type { BackendTask, CronJob } from '@/types'; export default function TasksPage() { const { locale } = useAppI18n(); const searchParams = useSearchParams(); const tab = searchParams.get('tab') === 'scheduled' ? 'scheduled' : 'ordinary'; return (

{pickAppText(locale, '任务', 'Tasks')}

{pickAppText(locale, '普通任务展示用户发起或接入的工作任务;定时任务触发后会先进入通知,需要修改时再接入 Task。', 'Ordinary tasks show user-started or engaged work. Scheduled jobs first create notifications and only become Tasks when revised.')}

{tab === 'scheduled' ? : }
); } function OrdinaryTasks() { const { locale } = useAppI18n(); const [backendTasks, setBackendTasks] = useState([]); const [error, setError] = useState(null); const visibleTasks = useMemo( () => backendTasks.filter((task) => { if (task.creator !== 'cron') return true; return Boolean(task.metadata?.user_engaged || task.metadata?.requires_followup); }), [backendTasks] ); const loadBackendTasks = React.useCallback(() => { let cancelled = false; listBackendTasks() .then((items) => { if (!cancelled) setBackendTasks(Array.isArray(items) ? items : []); }) .catch(() => { if (!cancelled) setBackendTasks([]); }); return () => { cancelled = true; }; }, []); useEffect(() => loadBackendTasks(), [loadBackendTasks]); const handleDeleteBackendTask = async (task: BackendTask) => { const title = task.short_title || task.description || task.goal || task.task_id; if (!window.confirm(pickAppText(locale, `删除任务“${title}”?`, `Delete task "${title}"?`))) { return; } setError(null); try { await deleteBackendTask(task.task_id); setBackendTasks((items) => items.filter((item) => item.task_id !== task.task_id)); } catch (err: any) { setError(err.message || pickAppText(locale, '删除任务失败', 'Failed to delete task')); } }; if (visibleTasks.length === 0) { return (

{pickAppText(locale, '暂无普通任务', 'No ordinary tasks yet')}

{pickAppText(locale, '从对话页发起复杂任务后,这里会保留普通任务列表;即使来源会话被归档,任务仍会显示。', 'Complex tasks created from chat appear here. Tasks remain visible even when their source session is archived.')}

); } return (
{error && ( {error} )} {pickAppText(locale, '任务', 'Task')} {pickAppText(locale, '状态', 'Status')} {pickAppText(locale, '来源', 'Source')} {pickAppText(locale, '运行次数', 'Runs')} {pickAppText(locale, '使用技能', 'Skills')} {pickAppText(locale, '更新时间', 'Updated')} {pickAppText(locale, '操作', 'Actions')} {visibleTasks.map((task) => (
{task.short_title || String(task.metadata?.short_title || '') || task.description || task.goal || task.task_id}
{task.is_open ? {pickAppText(locale, '进行中', 'Active')} : null}
{task.description || task.session_id} · {task.creator}
{taskStatusLabel(task.status, locale)} {taskSourceLabel(task, locale)} {task.run_ids.length} {task.skill_names.length} {formatOfficeTime(task.updated_at, locale)}
))}
); } function taskStatusLabel(status: string, locale: 'zh-CN' | 'en-US') { const labels: Record = { open: ['已创建', 'Open'], running: ['执行中', 'Running'], validating: ['验证中', 'Validating'], awaiting_feedback: ['等待反馈', 'Awaiting feedback'], needs_revision: ['需要修改', 'Needs revision'], closed: ['已完成', 'Closed'], abandoned: ['已放弃', 'Abandoned'], }; const label = labels[status]; return label ? pickAppText(locale, label[0], label[1]) : status; } function taskSourceLabel(task: BackendTask, locale: 'zh-CN' | 'en-US') { if (task.metadata?.source === 'scheduled_run') { return pickAppText(locale, '定时通知修改', 'Scheduled notification revision'); } if (task.metadata?.source === 'scheduled_cron') { return pickAppText(locale, '定时任务', 'Scheduled task'); } if (task.creator === 'cron') { return pickAppText(locale, '定时任务', 'Scheduled task'); } return pickAppText(locale, '对话任务', 'Chat task'); } function ScheduledTasks() { const { locale } = useAppI18n(); const sessionId = useChatStore((state) => state.sessionId); const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showAdd, setShowAdd] = useState(false); const targetSessionKey = sessionId.startsWith('web:') ? sessionId : 'web:default'; const loadJobs = React.useCallback(async () => { setLoading(true); setError(null); try { setJobs(await listCronJobs(true)); } catch (err: any) { setError(err.message || pickAppText(locale, '加载定时任务失败', 'Failed to load scheduled tasks')); } finally { setLoading(false); } }, [locale]); useEffect(() => { void loadJobs(); }, [loadJobs]); const formatTime = (ms: number | null) => { if (!ms) return '-'; return new Date(ms).toLocaleString(locale, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; const runJobAction = async (action: () => Promise) => { try { await action(); await loadJobs(); } catch (err: any) { setError(err.message || pickAppText(locale, '操作失败', 'Action failed')); } }; return (
{pickAppText(locale, '每次触发会生成通知记录;需要修改时再接入 Task。', 'Each trigger creates a notification record; connect it to a Task when revision is needed.')}
{error && ( {error} )} {showAdd && ( setShowAdd(false)} onAdd={(params) => runJobAction(async () => { await addCronJob({ ...params, session_key: targetSessionKey, mode: 'notification' }); setShowAdd(false); }) } /> )} {loading ? (
{pickAppText(locale, '加载中', 'Loading')}
) : jobs.length === 0 ? (

{pickAppText(locale, '暂无定时任务', 'No scheduled tasks yet')}

) : ( {pickAppText(locale, '启用', 'Enabled')} {pickAppText(locale, '名称', 'Name')} {pickAppText(locale, '计划', 'Schedule')} {pickAppText(locale, '消息', 'Message')} {pickAppText(locale, '运行历史', 'History')} {pickAppText(locale, '状态', 'Status')} {pickAppText(locale, '操作', 'Actions')} {jobs.map((job) => ( void runJobAction(() => toggleCronJob(job.id, checked))} />
{job.name}
{job.id}
{job.schedule_display}
{pickAppText(locale, '下次', 'Next')}: {formatTime(job.next_run_at_ms)}
{job.message} {job.last_status === 'ok' ? ( {pickAppText(locale, '成功', 'OK')} ) : job.last_status === 'error' ? ( {pickAppText(locale, '错误', 'Error')} ) : ( - )}
))}
)}
); } function AddJobForm({ targetSessionKey, onAdd, onCancel, }: { targetSessionKey: string; onAdd: (params: { name: string; message: string; every_seconds?: number; cron_expr?: string; at_iso?: string }) => void; onCancel: () => void; }) { const { locale } = useAppI18n(); const [name, setName] = useState(''); const [message, setMessage] = useState(''); const [scheduleType, setScheduleType] = useState<'every' | 'cron' | 'at'>('every'); const [everySeconds, setEverySeconds] = useState('3600'); const [cronExpr, setCronExpr] = useState('0 9 * * *'); const [atIso, setAtIso] = useState(''); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!name.trim() || !message.trim()) return; if (scheduleType === 'every') { onAdd({ name: name.trim(), message: message.trim(), every_seconds: Number.parseInt(everySeconds, 10) || 3600 }); return; } if (scheduleType === 'cron') { onAdd({ name: name.trim(), message: message.trim(), cron_expr: cronExpr.trim() }); return; } onAdd({ name: name.trim(), message: message.trim(), at_iso: atIso }); }; return (
{pickAppText(locale, '新建定时任务', 'New scheduled task')}
setName(event.target.value)} />
{scheduleType === 'every' ? (
setEverySeconds(event.target.value)} />
) : scheduleType === 'cron' ? (
setCronExpr(event.target.value)} />
) : (
setAtIso(event.target.value)} />
)}
setMessage(event.target.value)} />

{pickAppText(locale, '触发后会发送到固定通知 session;需要修改时再接入 Task。来源会话:', 'Triggers are sent to the fixed notification session; connect to a Task for revisions. Source session: ')} {targetSessionKey}

); }