'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 { formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
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 [loading, setLoading] = useState(true);
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;
setLoading(true);
setError(null);
listBackendTasks()
.then((items) => {
if (!cancelled) setBackendTasks(Array.isArray(items) ? items : []);
})
.catch((err: any) => {
if (!cancelled) {
setBackendTasks([]);
setError(err.message || pickAppText(locale, '加载任务失败', 'Failed to load tasks'));
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [locale]);
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 (loading) {
return (
{pickAppText(locale, '加载任务中', 'Loading tasks')}
);
}
if (visibleTasks.length === 0 && !error) {
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.')}
{pickAppText(locale, '回到对话', 'Back to chat')}
);
}
return (
{error && (
{error}
)}
{visibleTasks.length > 0 ? (
<>
{visibleTasks.map((task) => (
void handleDeleteBackendTask(task)}
/>
))}
{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}
{formatTaskRuntimeTime(task.updated_at, locale)}
{pickAppText(locale, '进入', 'Open')}
void handleDeleteBackendTask(task)}
aria-label={pickAppText(locale, `删除任务 ${task.short_title || task.task_id}`, `Delete task ${task.short_title || task.task_id}`)}
title={pickAppText(locale, '删除任务', 'Delete task')}
>
))}
>
) : null}
);
}
function OrdinaryTaskCard({
task,
locale,
onDelete,
}: {
task: BackendTask;
locale: string;
onDelete: () => void;
}) {
const title = task.short_title || String(task.metadata?.short_title || '') || task.description || task.goal || task.task_id;
return (
{title}
{task.is_open ? {pickAppText(locale, '进行中', 'Active')} : null}
{task.description || task.session_id}
{pickAppText(locale, '状态', 'Status')}
{taskStatusLabel(task.status, locale)}
{pickAppText(locale, '来源', 'Source')}
{taskSourceLabel(task, locale)}
{pickAppText(locale, '运行 / 技能', 'Runs / skills')}
{task.run_ids.length} / {task.skill_names.length}
{pickAppText(locale, '更新时间', 'Updated')}
{formatTaskRuntimeTime(task.updated_at, locale)}
{pickAppText(locale, '进入任务', 'Open task')}
);
}
function taskStatusLabel(status: string, locale: string) {
const labels: Record = {
open: ['已创建', 'Open'],
running: ['执行中', 'Running'],
awaiting_acceptance: ['等待验收', 'Awaiting acceptance'],
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: string) {
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'));
}
};
const handleRemoveJob = (job: CronJob) => {
if (!window.confirm(pickAppText(locale, `删除定时任务“${job.name}”?`, `Delete scheduled task "${job.name}"?`))) {
return;
}
void runJobAction(() => removeCronJob(job.id));
};
return (
{pickAppText(locale, '每次触发会生成通知记录;需要修改时再接入 Task。', 'Each trigger creates a notification record; connect it to a Task when revision is needed.')}
void loadJobs()} variant="outline" size="sm" className="h-11">
{pickAppText(locale, '刷新', 'Refresh')}
setShowAdd(true)} size="sm" className="h-11">
{pickAppText(locale, '新建定时任务', 'New scheduled task')}
{error && (
{error}
)}
{showAdd && (
setShowAdd(false)}
onAdd={(params) =>
runJobAction(async () => {
await addCronJob({ ...params, session_key: targetSessionKey, mode: 'notification' });
setShowAdd(false);
})
}
/>
)}
{!loading && jobs.length > 0 ? (
{jobs.map((job) => (
void runJobAction(() => toggleCronJob(job.id, checked))}
onRun={() => void runJobAction(() => runCronJob(job.id))}
onRemove={() => handleRemoveJob(job)}
/>
))}
) : null}
0 ? 'hidden xl:block' : undefined}>
{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))}
aria-label={pickAppText(locale, `切换定时任务 ${job.name}`, `Toggle scheduled task ${job.name}`)}
/>
{job.name}
{job.id}
{job.schedule_display}
{pickAppText(locale, '下次', 'Next')}: {formatTime(job.next_run_at_ms)}
{job.message}
{formatTime(job.last_run_at_ms)}
{job.last_status === 'ok' ? (
{pickAppText(locale, '成功', 'OK')}
) : job.last_status === 'error' ? (
{pickAppText(locale, '错误', 'Error')}
) : (
-
)}
void runJobAction(() => runCronJob(job.id))}
aria-label={pickAppText(locale, `立即运行 ${job.name}`, `Run ${job.name} now`)}
title={pickAppText(locale, '立即运行', 'Run now')}
>
handleRemoveJob(job)}
aria-label={pickAppText(locale, `删除定时任务 ${job.name}`, `Delete scheduled task ${job.name}`)}
title={pickAppText(locale, '删除定时任务', 'Delete scheduled task')}
>
))}
)}
);
}
function ScheduledJobCard({
job,
locale,
formatTime,
onToggle,
onRun,
onRemove,
}: {
job: CronJob;
locale: string;
formatTime: (ms: number | null) => string;
onToggle: (checked: boolean) => void;
onRun: () => void;
onRemove: () => void;
}) {
const historyHref = job.last_scheduled_run_id
? `/notifications/${encodeURIComponent(job.last_scheduled_run_id)}`
: job.last_task_id
? `/tasks/${encodeURIComponent(job.last_task_id)}`
: '/tasks';
return (
{pickAppText(locale, '启用', 'Enabled')}
{job.message}
{pickAppText(locale, '计划', 'Schedule')}
{job.schedule_display}
{pickAppText(locale, '下次运行', 'Next run')}
{formatTime(job.next_run_at_ms)}
{pickAppText(locale, '上次运行', 'Last run')}
{formatTime(job.last_run_at_ms)}
{pickAppText(locale, '状态', 'Status')}
{job.last_status === 'ok' ? (
{pickAppText(locale, '成功', 'OK')}
) : job.last_status === 'error' ? (
{pickAppText(locale, '错误', 'Error')}
) : (
-
)}
{pickAppText(locale, '运行历史', 'History')}
);
}
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')}
);
}