@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/CHANGELOG.md +7 -0
- package/dist/assets/index-B3foa-xK.css +1 -0
- package/dist/assets/index-CsMwBztg.js +347 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/config.ts +46 -1
- package/src/api/types.ts +50 -0
- package/src/components/config/CronConfig.tsx +292 -0
- package/src/components/layout/Sidebar.tsx +6 -1
- package/src/hooks/useConfig.ts +58 -1
- package/src/lib/i18n.ts +27 -0
- package/dist/assets/index-BKd1WzmD.js +0 -332
- package/dist/assets/index-D3I3sUSu.css +0 -1
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
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',
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -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
|
|