@nextclaw/ui 0.5.5 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-BKd1WzmD.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-D3I3sUSu.css">
9
+ <script type="module" crossorigin src="/assets/index-CsMwBztg.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-B3foa-xK.css">
11
11
  </head>
12
12
 
13
13
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/App.tsx CHANGED
@@ -5,6 +5,7 @@ import { ProvidersList } from '@/components/config/ProvidersList';
5
5
  import { ChannelsList } from '@/components/config/ChannelsList';
6
6
  import { RuntimeConfig } from '@/components/config/RuntimeConfig';
7
7
  import { SessionsConfig } from '@/components/config/SessionsConfig';
8
+ import { CronConfig } from '@/components/config/CronConfig';
8
9
  import { MarketplacePage } from '@/components/marketplace/MarketplacePage';
9
10
  import { useWebSocket } from '@/hooks/useWebSocket';
10
11
  import { Toaster } from 'sonner';
@@ -33,6 +34,7 @@ function AppContent() {
33
34
  <Route path="/channels" element={<ChannelsList />} />
34
35
  <Route path="/runtime" element={<RuntimeConfig />} />
35
36
  <Route path="/sessions" element={<SessionsConfig />} />
37
+ <Route path="/cron" element={<CronConfig />} />
36
38
  <Route path="/marketplace" element={<MarketplacePage />} />
37
39
  <Route path="/" element={<Navigate to="/model" replace />} />
38
40
  <Route path="*" element={<Navigate to="/model" replace />} />
package/src/api/config.ts CHANGED
@@ -11,7 +11,11 @@ import type {
11
11
  ConfigActionExecuteResult,
12
12
  SessionsListView,
13
13
  SessionHistoryView,
14
- SessionPatchUpdate
14
+ SessionPatchUpdate,
15
+ CronListView,
16
+ CronEnableRequest,
17
+ CronRunRequest,
18
+ CronActionResult
15
19
  } from './types';
16
20
 
17
21
  // GET /api/config
@@ -162,3 +166,44 @@ export async function deleteSession(key: string): Promise<{ deleted: boolean }>
162
166
  }
163
167
  return response.data;
164
168
  }
169
+
170
+ // GET /api/cron
171
+ export async function fetchCronJobs(params?: { all?: boolean }): Promise<CronListView> {
172
+ const query = new URLSearchParams();
173
+ if (params?.all) {
174
+ query.set('all', '1');
175
+ }
176
+ const suffix = query.toString();
177
+ const response = await api.get<CronListView>(suffix ? '/api/cron?' + suffix : '/api/cron');
178
+ if (!response.ok) {
179
+ throw new Error(response.error.message);
180
+ }
181
+ return response.data;
182
+ }
183
+
184
+ // DELETE /api/cron/:id
185
+ export async function deleteCronJob(id: string): Promise<{ deleted: boolean }> {
186
+ const response = await api.delete<{ deleted: boolean }>(`/api/cron/${encodeURIComponent(id)}`);
187
+ if (!response.ok) {
188
+ throw new Error(response.error.message);
189
+ }
190
+ return response.data;
191
+ }
192
+
193
+ // PUT /api/cron/:id/enable
194
+ export async function setCronJobEnabled(id: string, data: CronEnableRequest): Promise<CronActionResult> {
195
+ const response = await api.put<CronActionResult>(`/api/cron/${encodeURIComponent(id)}/enable`, data);
196
+ if (!response.ok) {
197
+ throw new Error(response.error.message);
198
+ }
199
+ return response.data;
200
+ }
201
+
202
+ // POST /api/cron/:id/run
203
+ export async function runCronJob(id: string, data: CronRunRequest): Promise<CronActionResult> {
204
+ const response = await api.post<CronActionResult>(`/api/cron/${encodeURIComponent(id)}/run`, data);
205
+ if (!response.ok) {
206
+ throw new Error(response.error.message);
207
+ }
208
+ return response.data;
209
+ }
package/src/api/types.ts CHANGED
@@ -92,6 +92,56 @@ export type SessionPatchUpdate = {
92
92
  clearHistory?: boolean;
93
93
  };
94
94
 
95
+ export type CronScheduleView =
96
+ | { kind: "at"; atMs?: number | null }
97
+ | { kind: "every"; everyMs?: number | null }
98
+ | { kind: "cron"; expr?: string | null; tz?: string | null };
99
+
100
+ export type CronPayloadView = {
101
+ kind?: "system_event" | "agent_turn";
102
+ message: string;
103
+ deliver?: boolean;
104
+ channel?: string | null;
105
+ to?: string | null;
106
+ };
107
+
108
+ export type CronJobStateView = {
109
+ nextRunAt?: string | null;
110
+ lastRunAt?: string | null;
111
+ lastStatus?: "ok" | "error" | "skipped" | null;
112
+ lastError?: string | null;
113
+ };
114
+
115
+ export type CronJobView = {
116
+ id: string;
117
+ name: string;
118
+ enabled: boolean;
119
+ schedule: CronScheduleView;
120
+ payload: CronPayloadView;
121
+ state: CronJobStateView;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ deleteAfterRun: boolean;
125
+ };
126
+
127
+ export type CronListView = {
128
+ jobs: CronJobView[];
129
+ total: number;
130
+ };
131
+
132
+ export type CronEnableRequest = {
133
+ enabled: boolean;
134
+ };
135
+
136
+ export type CronRunRequest = {
137
+ force?: boolean;
138
+ };
139
+
140
+ export type CronActionResult = {
141
+ job: CronJobView | null;
142
+ executed?: boolean;
143
+ };
144
+
95
145
  export type RuntimeConfigUpdate = {
96
146
  agents?: {
97
147
  defaults?: {
@@ -0,0 +1,292 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { CronJobView } from '@/api/types';
3
+ import { useConfirmDialog } from '@/hooks/useConfirmDialog';
4
+ import { useCronJobs, useDeleteCronJob, useToggleCronJob, useRunCronJob } from '@/hooks/useConfig';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8
+ import { Card, CardContent } from '@/components/ui/card';
9
+ import { cn } from '@/lib/utils';
10
+ import { t } from '@/lib/i18n';
11
+ import { AlarmClock, RefreshCw, Trash2, Play, Power } from 'lucide-react';
12
+
13
+ type StatusFilter = 'all' | 'enabled' | 'disabled';
14
+
15
+ function formatDate(value?: string | null): string {
16
+ if (!value) {
17
+ return '-';
18
+ }
19
+ const date = new Date(value);
20
+ if (Number.isNaN(date.getTime())) {
21
+ return value;
22
+ }
23
+ return date.toLocaleString();
24
+ }
25
+
26
+ function formatDateFromMs(value?: number | null): string {
27
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
28
+ return '-';
29
+ }
30
+ const date = new Date(value);
31
+ if (Number.isNaN(date.getTime())) {
32
+ return '-';
33
+ }
34
+ return date.toLocaleString();
35
+ }
36
+
37
+ function formatEveryDuration(ms?: number | null): string {
38
+ if (typeof ms !== 'number' || !Number.isFinite(ms)) {
39
+ return '-';
40
+ }
41
+ const seconds = Math.round(ms / 1000);
42
+ if (seconds < 60) return `${seconds}s`;
43
+ const minutes = Math.round(seconds / 60);
44
+ if (minutes < 60) return `${minutes}m`;
45
+ const hours = Math.round(minutes / 60);
46
+ if (hours < 24) return `${hours}h`;
47
+ const days = Math.round(hours / 24);
48
+ return `${days}d`;
49
+ }
50
+
51
+ function describeSchedule(job: CronJobView): string {
52
+ const schedule = job.schedule;
53
+ if (schedule.kind === 'cron') {
54
+ return schedule.expr ? `cron ${schedule.expr}` : 'cron';
55
+ }
56
+ if (schedule.kind === 'every') {
57
+ return `every ${formatEveryDuration(schedule.everyMs)}`;
58
+ }
59
+ if (schedule.kind === 'at') {
60
+ return `at ${formatDateFromMs(schedule.atMs)}`;
61
+ }
62
+ return '-';
63
+ }
64
+
65
+ function describeDelivery(job: CronJobView): string {
66
+ if (!job.payload.deliver) {
67
+ return '-';
68
+ }
69
+ const channel = job.payload.channel ?? '-';
70
+ const target = job.payload.to ?? '-';
71
+ return `${channel}:${target}`;
72
+ }
73
+
74
+ function matchQuery(job: CronJobView, query: string): boolean {
75
+ const q = query.trim().toLowerCase();
76
+ if (!q) return true;
77
+ const haystack = [
78
+ job.id,
79
+ job.name,
80
+ job.payload.message,
81
+ job.payload.channel ?? '',
82
+ job.payload.to ?? ''
83
+ ].join(' ').toLowerCase();
84
+ return haystack.includes(q);
85
+ }
86
+
87
+ function filterByStatus(job: CronJobView, status: StatusFilter): boolean {
88
+ if (status === 'all') return true;
89
+ if (status === 'enabled') return job.enabled;
90
+ return !job.enabled;
91
+ }
92
+
93
+ export function CronConfig() {
94
+ const [query, setQuery] = useState('');
95
+ const [status, setStatus] = useState<StatusFilter>('all');
96
+ const cronQuery = useCronJobs({ all: true });
97
+ const deleteCronJob = useDeleteCronJob();
98
+ const toggleCronJob = useToggleCronJob();
99
+ const runCronJob = useRunCronJob();
100
+ const { confirm, ConfirmDialog } = useConfirmDialog();
101
+
102
+ const jobs = useMemo(() => {
103
+ const data = cronQuery.data?.jobs ?? [];
104
+ return data
105
+ .filter((job) => matchQuery(job, query))
106
+ .filter((job) => filterByStatus(job, status));
107
+ }, [cronQuery.data, query, status]);
108
+
109
+ const handleDelete = async (job: CronJobView) => {
110
+ const confirmed = await confirm({
111
+ title: `${t('cronDeleteConfirm')}?`,
112
+ description: job.name ? `${job.name} (${job.id})` : job.id,
113
+ variant: 'destructive',
114
+ confirmLabel: t('delete')
115
+ });
116
+ if (!confirmed) return;
117
+ deleteCronJob.mutate({ id: job.id });
118
+ };
119
+
120
+ const handleToggle = async (job: CronJobView) => {
121
+ const nextEnabled = !job.enabled;
122
+ const confirmed = await confirm({
123
+ title: nextEnabled ? `${t('cronEnableConfirm')}?` : `${t('cronDisableConfirm')}?`,
124
+ description: job.name ? `${job.name} (${job.id})` : job.id,
125
+ variant: nextEnabled ? 'default' : 'destructive',
126
+ confirmLabel: nextEnabled ? t('cronEnable') : t('cronDisable')
127
+ });
128
+ if (!confirmed) return;
129
+ toggleCronJob.mutate({ id: job.id, enabled: nextEnabled });
130
+ };
131
+
132
+ const handleRun = async (job: CronJobView) => {
133
+ const force = !job.enabled;
134
+ const confirmed = await confirm({
135
+ title: force ? `${t('cronRunForceConfirm')}?` : `${t('cronRunConfirm')}?`,
136
+ description: job.name ? `${job.name} (${job.id})` : job.id,
137
+ confirmLabel: t('cronRunNow')
138
+ });
139
+ if (!confirmed) return;
140
+ runCronJob.mutate({ id: job.id, force });
141
+ };
142
+
143
+ return (
144
+ <div className="h-[calc(100vh-80px)] w-full max-w-[1200px] mx-auto animate-fade-in flex flex-col pt-6 pb-2">
145
+ <div className="flex items-center justify-between mb-6 shrink-0">
146
+ <div>
147
+ <h2 className="text-2xl font-bold text-gray-900 tracking-tight">{t('cronPageTitle')}</h2>
148
+ <p className="text-sm text-gray-500 mt-1">{t('cronPageDescription')}</p>
149
+ </div>
150
+ <Button
151
+ variant="ghost"
152
+ size="icon"
153
+ className="h-9 w-9 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100"
154
+ onClick={() => cronQuery.refetch()}
155
+ >
156
+ <RefreshCw className={cn('h-4 w-4', cronQuery.isFetching && 'animate-spin')} />
157
+ </Button>
158
+ </div>
159
+
160
+ <Card hover={false} className="mb-6">
161
+ <CardContent className="pt-6">
162
+ <div className="flex flex-wrap gap-3 items-center">
163
+ <div className="relative flex-1 min-w-[240px]">
164
+ <Input
165
+ value={query}
166
+ onChange={(e) => setQuery(e.target.value)}
167
+ placeholder={t('cronSearchPlaceholder')}
168
+ className="pl-9"
169
+ />
170
+ <AlarmClock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
171
+ </div>
172
+ <div className="min-w-[180px]">
173
+ <Select value={status} onValueChange={(value) => setStatus(value as StatusFilter)}>
174
+ <SelectTrigger className="w-full">
175
+ <SelectValue placeholder={t('cronStatusLabel')} />
176
+ </SelectTrigger>
177
+ <SelectContent>
178
+ <SelectItem value="all">{t('cronStatusAll')}</SelectItem>
179
+ <SelectItem value="enabled">{t('cronStatusEnabled')}</SelectItem>
180
+ <SelectItem value="disabled">{t('cronStatusDisabled')}</SelectItem>
181
+ </SelectContent>
182
+ </Select>
183
+ </div>
184
+ <div className="text-xs text-gray-500 ml-auto">
185
+ {t('cronTotalLabel')}: {cronQuery.data?.total ?? 0} / {jobs.length}
186
+ </div>
187
+ </div>
188
+ </CardContent>
189
+ </Card>
190
+
191
+ <div className="flex-1 overflow-auto custom-scrollbar">
192
+ {cronQuery.isLoading ? (
193
+ <div className="text-sm text-gray-400 p-4 text-center">{t('cronLoading')}</div>
194
+ ) : jobs.length === 0 ? (
195
+ <div className="text-sm text-gray-400 p-4 text-center">{t('cronEmpty')}</div>
196
+ ) : (
197
+ <div className="space-y-4">
198
+ {jobs.map((job) => (
199
+ <Card key={job.id} className="border border-gray-200">
200
+ <CardContent className="pt-5 pb-5">
201
+ <div className="flex flex-wrap items-start justify-between gap-4">
202
+ <div className="min-w-[220px] flex-1">
203
+ <div className="flex flex-wrap items-center gap-2">
204
+ <span className="text-sm font-semibold text-gray-900">
205
+ {job.name || job.id}
206
+ </span>
207
+ <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">
208
+ {job.id}
209
+ </span>
210
+ <span
211
+ className={cn(
212
+ 'text-[10px] font-semibold px-2 py-0.5 rounded-full',
213
+ job.enabled ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-500'
214
+ )}
215
+ >
216
+ {job.enabled ? t('enabled') : t('disabled')}
217
+ </span>
218
+ {job.deleteAfterRun && (
219
+ <span className="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-amber-50 text-amber-700">
220
+ {t('cronOneShot')}
221
+ </span>
222
+ )}
223
+ </div>
224
+ <div className="mt-2 text-xs text-gray-500">
225
+ {t('cronScheduleLabel')}: {describeSchedule(job)}
226
+ </div>
227
+ <div className="mt-2 text-sm text-gray-700 whitespace-pre-wrap break-words">
228
+ {job.payload.message}
229
+ </div>
230
+ <div className="mt-2 text-xs text-gray-500">
231
+ {t('cronDeliverTo')}: {describeDelivery(job)}
232
+ </div>
233
+ </div>
234
+ <div className="min-w-[220px] text-xs text-gray-500 space-y-2">
235
+ <div>
236
+ <span className="font-medium text-gray-700">{t('cronNextRun')}:</span>{' '}
237
+ {formatDate(job.state.nextRunAt)}
238
+ </div>
239
+ <div>
240
+ <span className="font-medium text-gray-700">{t('cronLastRun')}:</span>{' '}
241
+ {formatDate(job.state.lastRunAt)}
242
+ </div>
243
+ <div>
244
+ <span className="font-medium text-gray-700">{t('cronLastStatus')}:</span>{' '}
245
+ {job.state.lastStatus ?? '-'}
246
+ </div>
247
+ {job.state.lastError && (
248
+ <div className="text-[11px] text-red-500 break-words">
249
+ {job.state.lastError}
250
+ </div>
251
+ )}
252
+ </div>
253
+ <div className="flex items-start gap-2 flex-wrap justify-end">
254
+ <Button
255
+ variant="subtle"
256
+ size="sm"
257
+ onClick={() => handleRun(job)}
258
+ className="gap-1"
259
+ >
260
+ <Play className="h-3.5 w-3.5" />
261
+ {t('cronRunNow')}
262
+ </Button>
263
+ <Button
264
+ variant={job.enabled ? 'outline' : 'primary'}
265
+ size="sm"
266
+ onClick={() => handleToggle(job)}
267
+ className="gap-1"
268
+ >
269
+ <Power className="h-3.5 w-3.5" />
270
+ {job.enabled ? t('cronDisable') : t('cronEnable')}
271
+ </Button>
272
+ <Button
273
+ variant="destructive"
274
+ size="sm"
275
+ onClick={() => handleDelete(job)}
276
+ className="gap-1"
277
+ >
278
+ <Trash2 className="h-3.5 w-3.5" />
279
+ {t('delete')}
280
+ </Button>
281
+ </div>
282
+ </div>
283
+ </CardContent>
284
+ </Card>
285
+ ))}
286
+ </div>
287
+ )}
288
+ </div>
289
+ <ConfirmDialog />
290
+ </div>
291
+ );
292
+ }
@@ -1,6 +1,6 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { t } from '@/lib/i18n';
3
- import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store } from 'lucide-react';
3
+ import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store, AlarmClock } from 'lucide-react';
4
4
  import { NavLink } from 'react-router-dom';
5
5
  import { useDocBrowser } from '@/components/doc-browser';
6
6
 
@@ -30,6 +30,11 @@ const navItems = [
30
30
  label: t('sessions'),
31
31
  icon: History,
32
32
  },
33
+ {
34
+ target: '/cron',
35
+ label: t('cron'),
36
+ icon: AlarmClock,
37
+ },
33
38
  {
34
39
  target: '/marketplace',
35
40
  label: 'Marketplace',
@@ -11,7 +11,11 @@ import {
11
11
  fetchSessions,
12
12
  fetchSessionHistory,
13
13
  updateSession,
14
- deleteSession
14
+ deleteSession,
15
+ fetchCronJobs,
16
+ deleteCronJob,
17
+ setCronJobEnabled,
18
+ runCronJob
15
19
  } from '@/api/config';
16
20
  import { toast } from 'sonner';
17
21
  import { t } from '@/lib/i18n';
@@ -164,3 +168,56 @@ export function useDeleteSession() {
164
168
  }
165
169
  });
166
170
  }
171
+
172
+ export function useCronJobs(params: { all?: boolean } = { all: true }) {
173
+ return useQuery({
174
+ queryKey: ['cron', params],
175
+ queryFn: () => fetchCronJobs(params),
176
+ staleTime: 10_000
177
+ });
178
+ }
179
+
180
+ export function useDeleteCronJob() {
181
+ const queryClient = useQueryClient();
182
+
183
+ return useMutation({
184
+ mutationFn: ({ id }: { id: string }) => deleteCronJob(id),
185
+ onSuccess: () => {
186
+ queryClient.invalidateQueries({ queryKey: ['cron'] });
187
+ toast.success(t('configSavedApplied'));
188
+ },
189
+ onError: (error: Error) => {
190
+ toast.error(t('configSaveFailed') + ': ' + error.message);
191
+ }
192
+ });
193
+ }
194
+
195
+ export function useToggleCronJob() {
196
+ const queryClient = useQueryClient();
197
+
198
+ return useMutation({
199
+ mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) => setCronJobEnabled(id, { enabled }),
200
+ onSuccess: () => {
201
+ queryClient.invalidateQueries({ queryKey: ['cron'] });
202
+ toast.success(t('configSavedApplied'));
203
+ },
204
+ onError: (error: Error) => {
205
+ toast.error(t('configSaveFailed') + ': ' + error.message);
206
+ }
207
+ });
208
+ }
209
+
210
+ export function useRunCronJob() {
211
+ const queryClient = useQueryClient();
212
+
213
+ return useMutation({
214
+ mutationFn: ({ id, force }: { id: string; force?: boolean }) => runCronJob(id, { force }),
215
+ onSuccess: () => {
216
+ queryClient.invalidateQueries({ queryKey: ['cron'] });
217
+ toast.success(t('configSavedApplied'));
218
+ },
219
+ onError: (error: Error) => {
220
+ toast.error(t('configSaveFailed') + ': ' + error.message);
221
+ }
222
+ });
223
+ }
package/src/lib/i18n.ts CHANGED
@@ -4,6 +4,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
4
4
  model: { zh: '模型', en: 'Model' },
5
5
  providers: { zh: '提供商', en: 'Providers' },
6
6
  channels: { zh: '渠道', en: 'Channels' },
7
+ cron: { zh: '定时任务', en: 'Cron Jobs' },
7
8
 
8
9
  // Common
9
10
  enabled: { zh: '启用', en: 'Enabled' },
@@ -137,6 +138,32 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
137
138
  sessionsApplyingChanges: { zh: '正在应用会话变更...', en: 'Applying session changes...' },
138
139
  sessionsUnknownChannel: { zh: '未知渠道', en: 'Unknown channel' },
139
140
 
141
+ // Cron
142
+ cronPageTitle: { zh: '定时任务', en: 'Cron Jobs' },
143
+ cronPageDescription: { zh: '查看与删除定时任务,关注执行时间与状态。', en: 'View and delete cron jobs, track schedule and status.' },
144
+ cronSearchPlaceholder: { zh: '搜索名称 / 消息 / ID', en: 'Search name / message / ID' },
145
+ cronStatusLabel: { zh: '状态', en: 'Status' },
146
+ cronStatusAll: { zh: '全部', en: 'All' },
147
+ cronStatusEnabled: { zh: '仅启用', en: 'Enabled' },
148
+ cronStatusDisabled: { zh: '仅禁用', en: 'Disabled' },
149
+ cronTotalLabel: { zh: '总数', en: 'Total' },
150
+ cronLoading: { zh: '加载定时任务中...', en: 'Loading cron jobs...' },
151
+ cronEmpty: { zh: '暂无定时任务。', en: 'No cron jobs yet.' },
152
+ cronScheduleLabel: { zh: '计划', en: 'Schedule' },
153
+ cronDeliverTo: { zh: '投递到', en: 'Deliver to' },
154
+ cronNextRun: { zh: '下次执行', en: 'Next run' },
155
+ cronLastRun: { zh: '上次执行', en: 'Last run' },
156
+ cronLastStatus: { zh: '上次状态', en: 'Last status' },
157
+ cronDeleteConfirm: { zh: '确认删除定时任务', en: 'Delete cron job' },
158
+ cronOneShot: { zh: '一次性', en: 'One-shot' },
159
+ cronEnable: { zh: '启用', en: 'Enable' },
160
+ cronDisable: { zh: '禁用', en: 'Disable' },
161
+ cronRunNow: { zh: '立即执行', en: 'Run now' },
162
+ cronEnableConfirm: { zh: '确认启用定时任务', en: 'Enable cron job' },
163
+ cronDisableConfirm: { zh: '确认禁用定时任务', en: 'Disable cron job' },
164
+ cronRunConfirm: { zh: '确认立即执行定时任务', en: 'Run cron job now' },
165
+ cronRunForceConfirm: { zh: '任务已禁用,仍要立即执行', en: 'Cron job disabled. Force run now' },
166
+
140
167
  // UI
141
168
  saveVerifyConnect: { zh: '保存并验证 / 连接', en: 'Save & Verify / Connect' },
142
169