feat: 添加MinIO文件系统支持并优化外部连接器功能
- 添加MinIO用户文件系统配置选项(BEAVER_MINIO_ROOT_USER等) - 更新外部连接器配置结构,包括BASE_URL和认证令牌设置 - 改进connector provider支持更多类型(official, feishu_bot等) - 实现Mistral模型推理模式支持reasoning_effort参数 - 增强外部连接器策略配置和运行时配置管理 - 添加connector bridge事件验证和安全保护机制 - 优化任务路由逻辑,区分simple_chat和new_task场景 - 更新初始技能工具提示配置,分离authoring admin功能
This commit is contained in:
@ -19,7 +19,7 @@ import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
import { shouldPollTaskDetail, taskDetailDurationMs } from '@/lib/task-detail-refresh';
|
||||
import { buildTaskTimelineCards } from '@/lib/task-timeline';
|
||||
import { buildTaskTimelineView } from '@/lib/task-timeline-view';
|
||||
import type { BackendTask } from '@/types';
|
||||
|
||||
const TERMINAL_TASK_STATUSES = new Set(['closed', 'abandoned', 'cancelled', 'error']);
|
||||
@ -45,6 +45,7 @@ export default function TaskDetailPage() {
|
||||
const mountedRef = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
@ -89,44 +90,17 @@ export default function TaskDetailPage() {
|
||||
return () => window.clearInterval(id);
|
||||
}, [backendTask, loadBackendTask]);
|
||||
|
||||
const taskRunIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const run of backendTask?.process_runs ?? []) ids.add(run.run_id);
|
||||
for (const runId of backendTask?.run_ids ?? []) ids.add(runId);
|
||||
return ids;
|
||||
}, [backendTask]);
|
||||
|
||||
const liveRuns = useMemo(
|
||||
() => processRuns.filter((run) => taskRunIds.has(run.run_id) || run.metadata?.task_id === taskId),
|
||||
[processRuns, taskId, taskRunIds]
|
||||
);
|
||||
|
||||
const liveEvents = useMemo(
|
||||
() => processEvents.filter((event) => taskRunIds.has(event.run_id) || event.metadata?.task_id === taskId),
|
||||
[processEvents, taskId, taskRunIds]
|
||||
);
|
||||
|
||||
const liveArtifacts = useMemo(
|
||||
() => processArtifacts.filter((artifact) => taskRunIds.has(artifact.run_id) || artifact.metadata?.task_id === taskId),
|
||||
[processArtifacts, taskId, taskRunIds]
|
||||
);
|
||||
|
||||
const renderedRuns = liveRuns.length > 0 ? liveRuns : backendTask?.process_runs ?? [];
|
||||
const renderedEvents = liveEvents.length > 0 ? liveEvents : backendTask?.process_events ?? [];
|
||||
const renderedArtifacts = liveArtifacts.length > 0 ? liveArtifacts : backendTask?.process_artifacts ?? [];
|
||||
|
||||
const timelineCards = useMemo(
|
||||
const timelineView = useMemo(
|
||||
() =>
|
||||
backendTask
|
||||
? buildTaskTimelineCards({
|
||||
task: backendTask,
|
||||
processRuns: renderedRuns,
|
||||
processEvents: renderedEvents,
|
||||
processArtifacts: renderedArtifacts,
|
||||
})
|
||||
: [],
|
||||
[backendTask, renderedArtifacts, renderedEvents, renderedRuns]
|
||||
buildTaskTimelineView({
|
||||
task: backendTask,
|
||||
liveRuns: processRuns,
|
||||
liveEvents: processEvents,
|
||||
liveArtifacts: processArtifacts,
|
||||
}),
|
||||
[backendTask, processArtifacts, processEvents, processRuns]
|
||||
);
|
||||
const timelineCards = timelineView?.cards ?? [];
|
||||
|
||||
const activeLabel =
|
||||
[...timelineCards].reverse().find((card) => !['acceptance', 'task_created'].includes(card.type))?.title ?? '-';
|
||||
@ -164,13 +138,13 @@ export default function TaskDetailPage() {
|
||||
<div className="min-h-screen bg-background">
|
||||
<TaskLiveHeader task={backendTask} activeLabel={activeLabel} durationMs={durationMs} reviewTargetId={TASK_RESULT_REVIEW_ID} />
|
||||
|
||||
<main className="mx-auto grid max-w-7xl gap-6 p-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="space-y-4">
|
||||
<main className="mx-auto grid min-w-0 max-w-7xl gap-6 p-4 sm:p-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="min-w-0 space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="h-11 text-destructive hover:text-destructive"
|
||||
disabled={Boolean(actionBusy)}
|
||||
onClick={() => void deleteCurrentBackendTask()}
|
||||
>
|
||||
@ -217,7 +191,12 @@ export default function TaskDetailPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TaskSideRail task={backendTask} runs={renderedRuns} artifacts={renderedArtifacts} cards={timelineCards} />
|
||||
<TaskSideRail
|
||||
task={backendTask}
|
||||
runs={timelineView?.process.runs ?? []}
|
||||
artifacts={timelineView?.process.artifacts ?? []}
|
||||
cards={timelineCards}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
@ -225,7 +204,7 @@ export default function TaskDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-4 p-6">
|
||||
<Button asChild variant="outline" className="w-fit">
|
||||
<Button asChild variant="outline" className="h-11 w-fit">
|
||||
<Link href="/tasks">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '返回任务列表', 'Back to tasks')}
|
||||
|
||||
@ -46,6 +46,7 @@ export default function TasksPage() {
|
||||
function OrdinaryTasks() {
|
||||
const { locale } = useAppI18n();
|
||||
const [backendTasks, setBackendTasks] = useState<BackendTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const visibleTasks = useMemo(
|
||||
@ -58,17 +59,25 @@ function OrdinaryTasks() {
|
||||
|
||||
const loadBackendTasks = React.useCallback(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
listBackendTasks()
|
||||
.then((items) => {
|
||||
if (!cancelled) setBackendTasks(Array.isArray(items) ? items : []);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setBackendTasks([]);
|
||||
.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]);
|
||||
|
||||
@ -86,7 +95,18 @@ function OrdinaryTasks() {
|
||||
}
|
||||
};
|
||||
|
||||
if (visibleTasks.length === 0) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-16 text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{pickAppText(locale, '加载任务中', 'Loading tasks')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (visibleTasks.length === 0 && !error) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
|
||||
@ -113,7 +133,19 @@ function OrdinaryTasks() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
{visibleTasks.length > 0 ? (
|
||||
<>
|
||||
<div className="grid gap-3 xl:hidden">
|
||||
{visibleTasks.map((task) => (
|
||||
<OrdinaryTaskCard
|
||||
key={task.task_id}
|
||||
task={task}
|
||||
locale={locale}
|
||||
onDelete={() => void handleDeleteBackendTask(task)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Card className="hidden xl:block">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@ -154,7 +186,7 @@ function OrdinaryTasks() {
|
||||
<TableCell className="text-xs text-muted-foreground">{formatTaskRuntimeTime(task.updated_at, locale)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Button asChild size="sm" variant="outline" className="h-11">
|
||||
<Link href={`/tasks/${encodeURIComponent(task.task_id)}`}>
|
||||
{pickAppText(locale, '进入', 'Open')}
|
||||
<ArrowRight className="ml-2 h-3.5 w-3.5" />
|
||||
@ -163,8 +195,9 @@ function OrdinaryTasks() {
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
className="h-11 w-11 text-destructive hover:text-destructive"
|
||||
onClick={() => 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')}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@ -177,10 +210,80 @@ function OrdinaryTasks() {
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OrdinaryTaskCard({
|
||||
task,
|
||||
locale,
|
||||
onDelete,
|
||||
}: {
|
||||
task: BackendTask;
|
||||
locale: 'zh-CN' | 'en-US';
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const title = task.short_title || String(task.metadata?.short_title || '') || task.description || task.goal || task.task_id;
|
||||
|
||||
return (
|
||||
<Card className="rounded-md">
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h2 className="min-w-0 flex-1 text-base font-semibold">{title}</h2>
|
||||
{task.is_open ? <Badge variant="secondary">{pickAppText(locale, '进行中', 'Active')}</Badge> : null}
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||||
{task.description || task.session_id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '状态', 'Status')}</div>
|
||||
<Badge className="mt-1" variant={task.status === 'awaiting_acceptance' || task.status === 'closed' ? 'default' : 'secondary'}>
|
||||
{taskStatusLabel(task.status, locale)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '来源', 'Source')}</div>
|
||||
<div className="mt-1 text-sm">{taskSourceLabel(task, locale)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '运行 / 技能', 'Runs / skills')}</div>
|
||||
<div className="mt-1 text-sm">{task.run_ids.length} / {task.skill_names.length}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '更新时间', 'Updated')}</div>
|
||||
<div className="mt-1 text-sm">{formatTaskRuntimeTime(task.updated_at, locale)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 border-t border-border pt-3">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-11 w-11 text-destructive hover:text-destructive"
|
||||
onClick={onDelete}
|
||||
aria-label={pickAppText(locale, `删除任务 ${title}`, `Delete task ${title}`)}
|
||||
title={pickAppText(locale, '删除任务', 'Delete task')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="h-11">
|
||||
<Link href={`/tasks/${encodeURIComponent(task.task_id)}`}>
|
||||
{pickAppText(locale, '进入任务', 'Open task')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function taskStatusLabel(status: string, locale: 'zh-CN' | 'en-US') {
|
||||
const labels: Record<string, [string, string]> = {
|
||||
open: ['已创建', 'Open'],
|
||||
@ -246,6 +349,13 @@ function ScheduledTasks() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveJob = (job: CronJob) => {
|
||||
if (!window.confirm(pickAppText(locale, `删除定时任务“${job.name}”?`, `Delete scheduled task "${job.name}"?`))) {
|
||||
return;
|
||||
}
|
||||
void runJobAction(() => removeCronJob(job.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@ -254,11 +364,11 @@ function ScheduledTasks() {
|
||||
{pickAppText(locale, '每次触发会生成通知记录;需要修改时再接入 Task。', 'Each trigger creates a notification record; connect it to a Task when revision is needed.')}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={() => void loadJobs()} variant="outline" size="sm">
|
||||
<Button onClick={() => void loadJobs()} variant="outline" size="sm" className="h-11">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '刷新', 'Refresh')}
|
||||
</Button>
|
||||
<Button onClick={() => setShowAdd(true)} size="sm">
|
||||
<Button onClick={() => setShowAdd(true)} size="sm" className="h-11">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '新建定时任务', 'New scheduled task')}
|
||||
</Button>
|
||||
@ -287,7 +397,23 @@ function ScheduledTasks() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
{!loading && jobs.length > 0 ? (
|
||||
<div className="grid gap-3 xl:hidden">
|
||||
{jobs.map((job) => (
|
||||
<ScheduledJobCard
|
||||
key={job.id}
|
||||
job={job}
|
||||
locale={locale}
|
||||
formatTime={formatTime}
|
||||
onToggle={(checked) => void runJobAction(() => toggleCronJob(job.id, checked))}
|
||||
onRun={() => void runJobAction(() => runCronJob(job.id))}
|
||||
onRemove={() => handleRemoveJob(job)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Card className={!loading && jobs.length > 0 ? 'hidden xl:block' : undefined}>
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16 text-muted-foreground">
|
||||
@ -316,7 +442,11 @@ function ScheduledTasks() {
|
||||
{jobs.map((job) => (
|
||||
<TableRow key={job.id}>
|
||||
<TableCell>
|
||||
<Switch checked={job.enabled} onCheckedChange={(checked) => void runJobAction(() => toggleCronJob(job.id, checked))} />
|
||||
<Switch
|
||||
checked={job.enabled}
|
||||
onCheckedChange={(checked) => void runJobAction(() => toggleCronJob(job.id, checked))}
|
||||
aria-label={pickAppText(locale, `切换定时任务 ${job.name}`, `Toggle scheduled task ${job.name}`)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium">{job.name}</div>
|
||||
@ -330,7 +460,7 @@ function ScheduledTasks() {
|
||||
<span className="block max-w-[260px] truncate text-sm">{job.message}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button asChild size="sm" variant="outline" disabled={!job.last_scheduled_run_id && !job.last_task_id}>
|
||||
<Button asChild size="sm" variant="outline" className="h-11" disabled={!job.last_scheduled_run_id && !job.last_task_id}>
|
||||
<Link href={job.last_scheduled_run_id ? `/notifications/${encodeURIComponent(job.last_scheduled_run_id)}` : job.last_task_id ? `/tasks/${encodeURIComponent(job.last_task_id)}` : '/tasks'}>
|
||||
<FolderDown className="mr-2 h-3.5 w-3.5" />
|
||||
{formatTime(job.last_run_at_ms)}
|
||||
@ -348,10 +478,24 @@ function ScheduledTasks() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => void runJobAction(() => runCronJob(job.id))}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11"
|
||||
onClick={() => void runJobAction(() => runCronJob(job.id))}
|
||||
aria-label={pickAppText(locale, `立即运行 ${job.name}`, `Run ${job.name} now`)}
|
||||
title={pickAppText(locale, '立即运行', 'Run now')}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={() => void runJobAction(() => removeCronJob(job.id))}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 text-destructive hover:text-destructive"
|
||||
onClick={() => handleRemoveJob(job)}
|
||||
aria-label={pickAppText(locale, `删除定时任务 ${job.name}`, `Delete scheduled task ${job.name}`)}
|
||||
title={pickAppText(locale, '删除定时任务', 'Delete scheduled task')}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -367,6 +511,112 @@ function ScheduledTasks() {
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledJobCard({
|
||||
job,
|
||||
locale,
|
||||
formatTime,
|
||||
onToggle,
|
||||
onRun,
|
||||
onRemove,
|
||||
}: {
|
||||
job: CronJob;
|
||||
locale: 'zh-CN' | 'en-US';
|
||||
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 (
|
||||
<Card className="rounded-md">
|
||||
<CardContent className="space-y-4 p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-base font-semibold">{job.name}</h2>
|
||||
<p className="mt-1 break-all text-xs text-muted-foreground">{job.id}</p>
|
||||
</div>
|
||||
<label className="flex min-h-11 shrink-0 items-center gap-2 text-sm">
|
||||
<span className="sr-only">{pickAppText(locale, '启用', 'Enabled')}</span>
|
||||
<Switch
|
||||
checked={job.enabled}
|
||||
onCheckedChange={onToggle}
|
||||
aria-label={pickAppText(locale, `切换定时任务 ${job.name}`, `Toggle scheduled task ${job.name}`)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-6 text-muted-foreground">{job.message}</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3 text-xs">
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '计划', 'Schedule')}</div>
|
||||
<code className="mt-1 inline-block rounded bg-muted px-1.5 py-0.5">{job.schedule_display}</code>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '下次运行', 'Next run')}</div>
|
||||
<div className="mt-1 text-sm">{formatTime(job.next_run_at_ms)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '上次运行', 'Last run')}</div>
|
||||
<div className="mt-1 text-sm">{formatTime(job.last_run_at_ms)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">{pickAppText(locale, '状态', 'Status')}</div>
|
||||
<div className="mt-1">
|
||||
{job.last_status === 'ok' ? (
|
||||
<Badge>{pickAppText(locale, '成功', 'OK')}</Badge>
|
||||
) : job.last_status === 'error' ? (
|
||||
<Badge variant="destructive">{pickAppText(locale, '错误', 'Error')}</Badge>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 border-t border-border pt-3">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="h-11"
|
||||
disabled={!job.last_scheduled_run_id && !job.last_task_id}
|
||||
>
|
||||
<Link href={historyHref}>
|
||||
<FolderDown className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '运行历史', 'History')}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11"
|
||||
onClick={onRun}
|
||||
aria-label={pickAppText(locale, `立即运行 ${job.name}`, `Run ${job.name} now`)}
|
||||
title={pickAppText(locale, '立即运行', 'Run now')}
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 text-destructive hover:text-destructive"
|
||||
onClick={onRemove}
|
||||
aria-label={pickAppText(locale, `删除定时任务 ${job.name}`, `Delete scheduled task ${job.name}`)}
|
||||
title={pickAppText(locale, '删除定时任务', 'Delete scheduled task')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AddJobForm({
|
||||
targetSessionKey,
|
||||
onAdd,
|
||||
@ -403,7 +653,14 @@ function AddJobForm({
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{pickAppText(locale, '新建定时任务', 'New scheduled task')}</CardTitle>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onCancel}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11"
|
||||
onClick={onCancel}
|
||||
aria-label={pickAppText(locale, '关闭新建定时任务表单', 'Close new scheduled task form')}
|
||||
title={pickAppText(locale, '关闭', 'Close')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -452,8 +709,8 @@ function AddJobForm({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>{pickAppText(locale, '取消', 'Cancel')}</Button>
|
||||
<Button type="submit" disabled={!name.trim() || !message.trim()}>
|
||||
<Button type="button" variant="outline" className="h-11" onClick={onCancel}>{pickAppText(locale, '取消', 'Cancel')}</Button>
|
||||
<Button type="submit" className="h-11" disabled={!name.trim() || !message.trim()}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{pickAppText(locale, '创建', 'Create')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user