@lobehub/lobehub 2.0.0-next.275 → 2.0.0-next.277

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/changelog/v1.json +14 -0
  3. package/locales/en-US/setting.json +11 -0
  4. package/locales/zh-CN/setting.json +11 -0
  5. package/package.json +1 -1
  6. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
  7. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
  8. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
  9. package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
  10. package/packages/types/src/agentCronJob/index.ts +19 -23
  11. package/packages/types/src/serverConfig.ts +1 -0
  12. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
  13. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
  14. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
  15. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
  16. package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
  17. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
  18. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
  19. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
  20. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
  21. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
  22. package/src/app/[variants]/(main)/agent/features/Conversation/index.tsx +8 -2
  23. package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +1 -0
  24. package/src/app/[variants]/(main)/agent/features/Portal/features/Portal.tsx +4 -2
  25. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
  26. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
  27. package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
  28. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
  29. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +1 -0
  30. package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +4 -2
  31. package/src/hooks/useYamlArguments.ts +11 -8
  32. package/src/server/globalConfig/index.ts +1 -0
  33. package/src/services/chatGroup/index.ts +1 -4
  34. package/src/store/serverConfig/selectors.ts +1 -0
@@ -1,39 +1,19 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
3
  import { EDITOR_DEBOUNCE_TIME } from '@lobechat/const';
5
- import {
6
- ReactCodePlugin,
7
- ReactCodemirrorPlugin,
8
- ReactHRPlugin,
9
- ReactLinkPlugin,
10
- ReactListPlugin,
11
- ReactMathPlugin,
12
- ReactTablePlugin,
13
- } from '@lobehub/editor';
14
- import { Editor, useEditor, useEditorState } from '@lobehub/editor/react';
15
- import { ActionIcon, Flexbox, Icon, Input, Tag, Text } from '@lobehub/ui';
4
+ import { ActionIcon, Flexbox } from '@lobehub/ui';
16
5
  import { useDebounceFn } from 'ahooks';
17
- import { App, Card, Checkbox, Empty, InputNumber, Select, Switch, TimePicker, message } from 'antd';
6
+ import { App, Empty, message } from 'antd';
18
7
  import dayjs, { type Dayjs } from 'dayjs';
19
- import { Clock, Trash2 } from 'lucide-react';
20
- import {
21
- memo,
22
- useCallback,
23
- useEffect,
24
- useMemo,
25
- useRef,
26
- useState,
27
- useSyncExternalStore,
28
- } from 'react';
8
+ import { Trash2 } from 'lucide-react';
9
+ import { memo, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
29
10
  import { useTranslation } from 'react-i18next';
30
11
  import { useParams } from 'react-router-dom';
31
12
  import useSWR from 'swr';
32
13
 
33
14
  import AutoSaveHint from '@/components/Editor/AutoSaveHint';
34
15
  import Loading from '@/components/Loading/BrandTextLoading';
35
- import type { ExecutionConditions, UpdateAgentCronJobData } from '@/database/schemas/agentCronJob';
36
- import { InlineToolbar } from '@/features/EditorCanvas';
16
+ import type { UpdateAgentCronJobData } from '@/database/schemas/agentCronJob';
37
17
  import NavHeader from '@/features/NavHeader';
38
18
  import WideScreenContainer from '@/features/WideScreenContainer';
39
19
  import { useQueryRoute } from '@/hooks/useQueryRoute';
@@ -43,18 +23,27 @@ import { agentCronJobService } from '@/services/agentCronJob';
43
23
  import { topicService } from '@/services/topic';
44
24
  import { useAgentStore } from '@/store/agent';
45
25
  import { useChatStore } from '@/store/chat';
26
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
46
27
  import { useUserStore } from '@/store/user';
47
28
  import { labPreferSelectors } from '@/store/user/selectors';
48
29
 
30
+ import { type ScheduleType, buildCronPattern, parseCronPattern } from './CronConfig';
31
+ import CronJobContentEditor from './features/CronJobContentEditor';
32
+ import CronJobHeader from './features/CronJobHeader';
33
+ import CronJobSaveButton from './features/CronJobSaveButton';
34
+ import CronJobScheduleConfig from './features/CronJobScheduleConfig';
35
+
49
36
  interface CronJobDraft {
50
37
  content: string;
51
38
  cronPattern: string;
52
39
  description: string;
40
+ hourlyInterval?: number; // For hourly: interval in hours (1, 2, 6, 12)
53
41
  maxExecutions?: number | null;
54
- maxExecutionsPerDay?: number | null;
55
42
  name: string;
56
- timeRange?: [Dayjs, Dayjs];
57
- weekdays: number[];
43
+ scheduleType: ScheduleType;
44
+ timezone: string;
45
+ triggerTime: Dayjs; // Trigger time (HH:mm)
46
+ weekdays: number[]; // For weekly: selected days
58
47
  }
59
48
 
60
49
  type AutoSaveStatus = 'idle' | 'saving' | 'saved';
@@ -85,52 +74,6 @@ const AutoSaveHintSlot = memo(() => {
85
74
  return <AutoSaveHint lastUpdatedTime={lastUpdatedTime} saveStatus={status} />;
86
75
  });
87
76
 
88
- // Standard cron format: minute hour day month weekday
89
- const CRON_PATTERNS = [
90
- { label: 'agentCronJobs.interval.30min', value: '*/30 * * * *' },
91
- { label: 'agentCronJobs.interval.1hour', value: '0 * * * *' },
92
- { label: 'agentCronJobs.interval.2hours', value: '0 */2 * * *' },
93
- { label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' },
94
- { label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' },
95
- { label: 'agentCronJobs.interval.daily', value: '0 0 * * *' },
96
- { label: 'agentCronJobs.interval.weekly', value: '0 0 * * 0' },
97
- ];
98
-
99
- const WEEKDAY_OPTIONS = [
100
- { label: 'Mon', value: 1 },
101
- { label: 'Tue', value: 2 },
102
- { label: 'Wed', value: 3 },
103
- { label: 'Thu', value: 4 },
104
- { label: 'Fri', value: 5 },
105
- { label: 'Sat', value: 6 },
106
- { label: 'Sun', value: 0 },
107
- ];
108
-
109
- const WEEKDAY_LABELS: Record<number, string> = {
110
- 0: 'Sunday',
111
- 1: 'Monday',
112
- 2: 'Tuesday',
113
- 3: 'Wednesday',
114
- 4: 'Thursday',
115
- 5: 'Friday',
116
- 6: 'Saturday',
117
- };
118
-
119
- const getIntervalText = (cronPattern: string) => {
120
- // Standard cron format mapping
121
- const intervalMap: Record<string, string> = {
122
- '*/30 * * * *': 'agentCronJobs.interval.30min',
123
- '0 * * * *': 'agentCronJobs.interval.1hour',
124
- '0 */12 * * *': 'agentCronJobs.interval.12hours',
125
- '0 */2 * * *': 'agentCronJobs.interval.2hours',
126
- '0 */6 * * *': 'agentCronJobs.interval.6hours',
127
- '0 0 * * *': 'agentCronJobs.interval.daily',
128
- '0 0 * * 0': 'agentCronJobs.interval.weekly',
129
- };
130
-
131
- return intervalMap[cronPattern] || cronPattern;
132
- };
133
-
134
77
  const resolveDate = (value?: Date | string | null) => {
135
78
  if (!value) return null;
136
79
  return typeof value === 'string' ? new Date(value) : value;
@@ -141,15 +84,14 @@ const CronJobDetailPage = memo(() => {
141
84
  const { aid, cronId } = useParams<{ aid?: string; cronId?: string }>();
142
85
  const router = useQueryRoute();
143
86
  const { modal } = App.useApp();
144
- const editor = useEditor();
145
- const editorState = useEditorState(editor);
146
87
  const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
147
- const [editorReady, setEditorReady] = useState(false);
88
+ const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
89
+
90
+ const isNewJob = cronId === 'new';
148
91
 
149
92
  const [draft, setDraft] = useState<CronJobDraft | null>(null);
150
93
  const draftRef = useRef<CronJobDraft | null>(null);
151
94
  const contentRef = useRef('');
152
- const pendingContentRef = useRef<string | null>(null);
153
95
  const pendingSaveRef = useRef(false);
154
96
  const initializedIdRef = useRef<string | null>(null);
155
97
  const readyRef = useRef(false);
@@ -170,9 +112,9 @@ const CronJobDetailPage = memo(() => {
170
112
  const cronListAgentId = activeAgentId || aid;
171
113
 
172
114
  const { data: cronJob, isLoading } = useSWR(
173
- ENABLE_BUSINESS_FEATURES && cronId ? ['cronJob', cronId] : null,
115
+ enableBusinessFeatures && cronId && !isNewJob ? ['cronJob', cronId] : null,
174
116
  async () => {
175
- if (!cronId) return null;
117
+ if (!cronId || isNewJob) return null;
176
118
  const result = await agentCronJobService.getById(cronId);
177
119
  return result.success ? result.data : null;
178
120
  },
@@ -184,74 +126,28 @@ const CronJobDetailPage = memo(() => {
184
126
  },
185
127
  );
186
128
 
187
- const resolvedCronPattern = draft ? draft.cronPattern : cronJob?.cronPattern;
188
- const resolvedWeekdays = draft ? draft.weekdays : cronJob?.executionConditions?.weekdays || [];
189
- const resolvedTimeRange = draft
190
- ? draft.timeRange
191
- : cronJob?.executionConditions?.timeRange
192
- ? [
193
- dayjs(cronJob.executionConditions.timeRange.start, 'HH:mm'),
194
- dayjs(cronJob.executionConditions.timeRange.end, 'HH:mm'),
195
- ]
196
- : undefined;
197
-
198
- const summaryTags = useMemo(() => {
199
- const tags: Array<{ key: string; label: string }> = [];
200
-
201
- if (resolvedCronPattern) {
202
- tags.push({
203
- key: 'interval',
204
- label: t(getIntervalText(resolvedCronPattern) as any),
205
- });
206
- }
207
-
208
- if (resolvedWeekdays.length > 0) {
209
- tags.push({
210
- key: 'weekdays',
211
- label: resolvedWeekdays.map((day) => WEEKDAY_LABELS[day]).join(', '),
212
- });
213
- }
214
-
215
- if (resolvedTimeRange && resolvedTimeRange.length === 2) {
216
- tags.push({
217
- key: 'timeRange',
218
- label: `${resolvedTimeRange[0].format('HH:mm')} - ${resolvedTimeRange[1].format('HH:mm')}`,
219
- });
220
- }
221
-
222
- return tags;
223
- }, [resolvedCronPattern, resolvedTimeRange, resolvedWeekdays, t]);
224
-
225
129
  const buildUpdateData = useCallback(
226
130
  (snapshot: CronJobDraft | null, content: string): UpdateAgentCronJobData | null => {
227
131
  if (!snapshot) return null;
228
132
  if (!snapshot.content) return null;
229
133
  if (!snapshot.name) return null;
230
134
 
231
- const executionConditions: ExecutionConditions = {};
232
- if (snapshot.timeRange && snapshot.timeRange.length === 2) {
233
- executionConditions.timeRange = {
234
- end: snapshot.timeRange[1].format('HH:mm'),
235
- start: snapshot.timeRange[0].format('HH:mm'),
236
- };
237
- }
238
-
239
- if (snapshot.weekdays && snapshot.weekdays.length > 0) {
240
- executionConditions.weekdays = snapshot.weekdays;
241
- }
242
-
243
- if (snapshot.maxExecutionsPerDay) {
244
- executionConditions.maxExecutionsPerDay = snapshot.maxExecutionsPerDay;
245
- }
135
+ // Build cron pattern from schedule configuration
136
+ const cronPattern = buildCronPattern(
137
+ snapshot.scheduleType,
138
+ snapshot.triggerTime,
139
+ snapshot.hourlyInterval,
140
+ snapshot.weekdays,
141
+ );
246
142
 
247
143
  return {
248
144
  content,
249
- cronPattern: snapshot.cronPattern,
145
+ cronPattern,
250
146
  description: snapshot.description?.trim() || null,
251
- executionConditions:
252
- Object.keys(executionConditions).length > 0 ? executionConditions : null,
147
+ executionConditions: null, // No longer using executionConditions for time/weekdays
253
148
  maxExecutions: snapshot.maxExecutions ?? null,
254
149
  name: snapshot.name?.trim() || null,
150
+ timezone: snapshot.timezone,
255
151
  };
256
152
  },
257
153
  [],
@@ -272,11 +168,11 @@ const CronJobDetailPage = memo(() => {
272
168
  (current) =>
273
169
  current
274
170
  ? {
275
- ...current,
276
- ...payload,
277
- executionConditions: payload.executionConditions ?? null,
278
- ...(updatedAt ? { updatedAt } : null),
279
- }
171
+ ...current,
172
+ ...payload,
173
+ executionConditions: payload.executionConditions ?? null,
174
+ ...(updatedAt ? { updatedAt } : null),
175
+ }
280
176
  : current,
281
177
  false,
282
178
  );
@@ -291,6 +187,7 @@ const CronJobDetailPage = memo(() => {
291
187
 
292
188
  const { run: debouncedSave, cancel: cancelDebouncedSave } = useDebounceFn(
293
189
  async () => {
190
+ if (isNewJob) return; // Don't auto-save new jobs
294
191
  if (!cronId || initializedIdRef.current !== cronId) return;
295
192
  const payload = buildUpdateData(draftRef.current, contentRef.current);
296
193
  if (!payload) return;
@@ -323,6 +220,7 @@ const CronJobDetailPage = memo(() => {
323
220
  }, [cancelDebouncedSave, cronId]);
324
221
 
325
222
  const scheduleSave = useCallback(() => {
223
+ if (isNewJob) return; // Don't auto-save new jobs
326
224
  if (!readyRef.current || !draftRef.current) {
327
225
  pendingSaveRef.current = true;
328
226
  return;
@@ -330,15 +228,16 @@ const CronJobDetailPage = memo(() => {
330
228
  isDirtyRef.current = true;
331
229
  setAutoSaveState({ status: 'saving' });
332
230
  debouncedSave();
333
- }, [debouncedSave]);
231
+ }, [debouncedSave, isNewJob]);
334
232
 
335
233
  const flushPendingSave = useCallback(() => {
234
+ if (isNewJob) return; // Don't auto-save new jobs
336
235
  if (!pendingSaveRef.current || !draftRef.current) return;
337
236
  pendingSaveRef.current = false;
338
237
  isDirtyRef.current = true;
339
238
  setAutoSaveState({ status: 'saving' });
340
239
  debouncedSave();
341
- }, [debouncedSave]);
240
+ }, [debouncedSave, isNewJob]);
342
241
 
343
242
  const updateDraft = useCallback(
344
243
  (patch: Partial<CronJobDraft>) => {
@@ -353,14 +252,13 @@ const CronJobDetailPage = memo(() => {
353
252
  [scheduleSave],
354
253
  );
355
254
 
356
- const handleContentChange = useCallback(() => {
357
- if (!readyRef.current || !editor || !editorReady) return;
358
- const nextContent = enableRichRender
359
- ? (editor.getDocument('markdown') as unknown as string)
360
- : (editor.getDocument('text') as unknown as string);
361
- contentRef.current = nextContent || '';
362
- scheduleSave();
363
- }, [editor, editorReady, enableRichRender, scheduleSave]);
255
+ const handleContentChange = useCallback(
256
+ (content: string) => {
257
+ contentRef.current = content;
258
+ updateDraft({ content });
259
+ },
260
+ [updateDraft],
261
+ );
364
262
 
365
263
  const handleToggleEnabled = useCallback(
366
264
  async (enabled: boolean) => {
@@ -378,12 +276,15 @@ const CronJobDetailPage = memo(() => {
378
276
  [cronId, mutate, refreshCronList],
379
277
  );
380
278
 
381
- const handleDeleteCronJob = useCallback(() => {
279
+ const handleDeleteCronJob = useCallback(async () => {
382
280
  if (!cronId) return;
383
281
 
384
282
  modal.confirm({
283
+ cancelText: t('cancel', { ns: 'common' }),
385
284
  centered: true,
285
+ content: t('agentCronJobs.confirmDeleteCronJob' as any),
386
286
  okButtonProps: { danger: true },
287
+ okText: t('ok', { ns: 'common' }),
387
288
  onOk: async () => {
388
289
  try {
389
290
  let topicIds: string[] = [];
@@ -416,10 +317,82 @@ const CronJobDetailPage = memo(() => {
416
317
  message.error('Failed to delete scheduled task');
417
318
  }
418
319
  },
419
- title: t('agentCronJobs.confirmDelete'),
320
+ title: t('agentCronJobs.deleteCronJob' as any),
420
321
  });
421
322
  }, [activeTopicId, cronId, cronListAgentId, modal, refreshTopic, router, switchTopic, t]);
422
323
 
324
+ const handleSaveNewJob = useCallback(async () => {
325
+ if (!aid) {
326
+ message.error('Agent ID is required');
327
+ return;
328
+ }
329
+
330
+ const payload = buildUpdateData(draftRef.current, contentRef.current);
331
+ if (!payload) {
332
+ message.error('Please fill in all required fields');
333
+ return;
334
+ }
335
+
336
+ if (!payload.content || !payload.name || !payload.cronPattern) {
337
+ message.error('Name and content are required');
338
+ return;
339
+ }
340
+
341
+ setAutoSaveState({ status: 'saving' });
342
+ try {
343
+ const result = await agentCronJobService.create({
344
+ agentId: aid,
345
+ content: payload.content,
346
+ cronPattern: payload.cronPattern,
347
+ description: payload.description,
348
+ enabled: true,
349
+ executionConditions: payload.executionConditions,
350
+ maxExecutions: payload.maxExecutions,
351
+ name: payload.name,
352
+ timezone: payload.timezone,
353
+ });
354
+
355
+ if (result.success && result.data) {
356
+ setAutoSaveState({ lastUpdatedTime: new Date(), status: 'saved' });
357
+ message.success('Scheduled task created successfully');
358
+ refreshCronList();
359
+ // Navigate to the newly created job
360
+ router.push(`/agent/${aid}/cron/${result.data.id}`);
361
+ } else {
362
+ throw new Error('Failed to create job');
363
+ }
364
+ } catch (error) {
365
+ console.error('Failed to create cron job:', error);
366
+ setAutoSaveState({ status: 'idle' });
367
+ message.error('Failed to create scheduled task');
368
+ }
369
+ }, [aid, buildUpdateData, refreshCronList, router]);
370
+
371
+ // Initialize draft for new jobs
372
+ useEffect(() => {
373
+ if (!isNewJob || draft) return;
374
+
375
+ // Get browser timezone
376
+ const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
377
+
378
+ const defaultDraft: CronJobDraft = {
379
+ content: '',
380
+ cronPattern: '0 0 * * *', // Default: daily at midnight
381
+ description: '',
382
+ maxExecutions: null,
383
+ name: '',
384
+ scheduleType: 'daily',
385
+ timezone: browserTimezone,
386
+ triggerTime: dayjs().hour(0).minute(0),
387
+ weekdays: [0, 1, 2, 3, 4, 5, 6],
388
+ };
389
+
390
+ setDraft(defaultDraft);
391
+ draftRef.current = defaultDraft;
392
+ contentRef.current = '';
393
+ readyRef.current = true;
394
+ }, [isNewJob, draft]);
395
+
423
396
  useEffect(() => {
424
397
  if (!cronJob) return;
425
398
  const cronUpdatedAt = cronJob.updatedAt ? new Date(cronJob.updatedAt).toISOString() : null;
@@ -434,230 +407,99 @@ const CronJobDetailPage = memo(() => {
434
407
  readyRef.current = false;
435
408
  lastSavedNameRef.current = cronJob.name ?? null;
436
409
 
410
+ // Parse cron pattern to extract schedule configuration
411
+ const parsed = parseCronPattern(cronJob.cronPattern);
412
+
437
413
  const nextDraft: CronJobDraft = {
438
414
  content: cronJob.content || '',
439
415
  cronPattern: cronJob.cronPattern,
440
416
  description: cronJob.description || '',
417
+ hourlyInterval: parsed.hourlyInterval,
441
418
  maxExecutions: cronJob.maxExecutions ?? null,
442
- maxExecutionsPerDay: cronJob.executionConditions?.maxExecutionsPerDay ?? null,
443
419
  name: cronJob.name || '',
444
- timeRange: cronJob.executionConditions?.timeRange
445
- ? [
446
- dayjs(cronJob.executionConditions.timeRange.start, 'HH:mm'),
447
- dayjs(cronJob.executionConditions.timeRange.end, 'HH:mm'),
448
- ]
449
- : undefined,
450
- weekdays: cronJob.executionConditions?.weekdays || [],
420
+ scheduleType: parsed.scheduleType,
421
+ timezone: cronJob.timezone || 'UTC',
422
+ triggerTime: dayjs().hour(parsed.triggerHour).minute(parsed.triggerMinute),
423
+ weekdays:
424
+ parsed.scheduleType === 'weekly' && parsed.weekdays
425
+ ? parsed.weekdays
426
+ : [0, 1, 2, 3, 4, 5, 6], // Default: all days for weekly
451
427
  };
452
428
 
453
429
  setDraft(nextDraft);
454
430
  draftRef.current = nextDraft;
455
431
 
456
432
  contentRef.current = nextDraft.content;
457
- pendingContentRef.current = nextDraft.content;
458
433
 
459
434
  setAutoSaveState({
460
435
  lastUpdatedTime: resolveDate(cronJob.updatedAt),
461
436
  status: 'saved',
462
437
  });
463
438
 
464
- if (editorReady && editor) {
465
- try {
466
- setTimeout(() => {
467
- editor.setDocument(enableRichRender ? 'markdown' : 'text', nextDraft.content);
468
- }, 100);
469
- pendingContentRef.current = null;
470
- readyRef.current = true;
471
- flushPendingSave();
472
- } catch (error) {
473
- console.error('[CronJobDetailPage] Failed to init editor content:', error);
474
- setTimeout(() => {
475
- editor.setDocument(enableRichRender ? 'markdown' : 'text', nextDraft.content);
476
- }, 100);
477
- }
478
- }
479
- }, [cronJob, editor, editorReady, enableRichRender]);
480
-
481
- useEffect(() => {
482
- if (!editorReady || !editor || pendingContentRef.current === null) return;
483
- try {
484
- setTimeout(() => {
485
- editor.setDocument(enableRichRender ? 'markdown' : 'text', pendingContentRef.current);
486
- }, 100);
487
- pendingContentRef.current = null;
488
- readyRef.current = true;
489
- flushPendingSave();
490
- } catch (error) {
491
- console.error('[CronJobDetailPage] Failed to init editor content:', error);
492
- setTimeout(() => {
493
- console.log('setDocument timeout', pendingContentRef.current);
494
- editor.setDocument(enableRichRender ? 'markdown' : 'text', pendingContentRef.current);
495
- }, 100);
496
- }
497
- }, [editor, editorReady, enableRichRender]);
439
+ readyRef.current = true;
440
+ flushPendingSave();
441
+ }, [cronJob, flushPendingSave]);
498
442
 
499
- if (!ENABLE_BUSINESS_FEATURES) {
443
+ if (!enableBusinessFeatures) {
500
444
  return null;
501
445
  }
502
446
 
503
447
  return (
504
448
  <Flexbox flex={1} height={'100%'}>
505
- <NavHeader left={<AutoSaveHintSlot />} />
449
+ <NavHeader
450
+ left={!isNewJob ? <AutoSaveHintSlot /> : undefined}
451
+ right={
452
+ !isNewJob ? (
453
+ <ActionIcon
454
+ icon={Trash2}
455
+ onClick={handleDeleteCronJob}
456
+ title={t('delete', { ns: 'common' })}
457
+ />
458
+ ) : undefined
459
+ }
460
+ />
506
461
  <Flexbox flex={1} style={{ overflowY: 'auto' }}>
507
462
  <WideScreenContainer paddingBlock={16}>
508
463
  {isLoading && <Loading debugId="CronJobDetailPage" />}
509
- {!isLoading && !cronJob && (
464
+ {!isLoading && !cronJob && !isNewJob && (
510
465
  <Empty
511
466
  description={t('agentCronJobs.empty.description')}
512
467
  image={Empty.PRESENTED_IMAGE_SIMPLE}
513
468
  />
514
469
  )}
515
- {!isLoading && cronJob && (
470
+ {!isLoading && (cronJob || isNewJob) && draft && (
516
471
  <Flexbox gap={24}>
517
- <Flexbox align="center" gap={16} horizontal justify="space-between">
518
- <Flexbox gap={6} style={{ flex: 1, minWidth: 0 }}>
519
- <Input
520
- onChange={(e) => updateDraft({ name: e.target.value })}
521
- placeholder={t('agentCronJobs.form.name.placeholder')}
522
- style={{
523
- fontSize: 28,
524
- fontWeight: 600,
525
- padding: 0,
526
- }}
527
- value={draft?.name ?? cronJob.name ?? ''}
528
- variant={'borderless'}
529
- />
530
- </Flexbox>
531
- <Flexbox align="center" gap={8} horizontal>
532
- <ActionIcon
533
- icon={Trash2}
534
- onClick={handleDeleteCronJob}
535
- size={'small'}
536
- title={t('delete', { ns: 'common' })}
537
- />
538
- <Text type="secondary">
539
- {t(
540
- cronJob?.enabled
541
- ? 'agentCronJobs.status.enabled'
542
- : 'agentCronJobs.status.disabled',
543
- )}
544
- </Text>
545
- <Switch
546
- defaultChecked={cronJob?.enabled ?? false}
547
- disabled={!cronJob}
548
- key={cronJob?.id ?? 'cron-switch'}
549
- onChange={handleToggleEnabled}
550
- size="small"
551
- />
552
- </Flexbox>
553
- </Flexbox>
554
-
555
- <Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
556
- <Flexbox gap={12}>
557
- {summaryTags.length > 0 && (
558
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
559
- {summaryTags.map((tag) => (
560
- <Tag key={tag.key} variant={'filled'}>
561
- {tag.label}
562
- </Tag>
563
- ))}
564
- </Flexbox>
565
- )}
566
-
567
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
568
- <Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
569
- <Select
570
- onChange={(value) => updateDraft({ cronPattern: value })}
571
- options={CRON_PATTERNS.map((pattern) => ({
572
- label: t(pattern.label as any),
573
- value: pattern.value,
574
- }))}
575
- size="small"
576
- style={{ minWidth: 160 }}
577
- value={draft?.cronPattern ?? cronJob.cronPattern}
578
- />
579
- <TimePicker.RangePicker
580
- format="HH:mm"
581
- onChange={(value) =>
582
- updateDraft({
583
- timeRange:
584
- value && value.length === 2
585
- ? [value[0] as Dayjs, value[1] as Dayjs]
586
- : undefined,
587
- })
588
- }
589
- placeholder={[
590
- t('agentCronJobs.form.timeRange.start'),
591
- t('agentCronJobs.form.timeRange.end'),
592
- ]}
593
- size="small"
594
- value={
595
- draft?.timeRange ??
596
- (resolvedTimeRange as [Dayjs, Dayjs] | undefined) ??
597
- null
598
- }
599
- />
600
- <Checkbox.Group
601
- onChange={(values) => updateDraft({ weekdays: values as number[] })}
602
- options={WEEKDAY_OPTIONS}
603
- style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}
604
- value={draft?.weekdays ?? resolvedWeekdays}
605
- />
606
- </Flexbox>
607
-
608
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
609
- <Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
610
- <InputNumber
611
- min={1}
612
- onChange={(value) => updateDraft({ maxExecutions: value ?? null })}
613
- placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
614
- size="small"
615
- style={{ width: 160 }}
616
- value={draft?.maxExecutions ?? cronJob.maxExecutions ?? null}
617
- />
618
- </Flexbox>
619
- </Flexbox>
620
- </Card>
621
-
622
- <Flexbox gap={12}>
623
- <Flexbox align="center" gap={6} horizontal>
624
- <Icon icon={Clock} size={16} />
625
- <Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
626
- </Flexbox>
627
- <Card
628
- size="small"
629
- style={{ borderRadius: 12, overflow: 'hidden' }}
630
- styles={{ body: { padding: 0 } }}
631
- >
632
- {enableRichRender && <InlineToolbar editor={editor} editorState={editorState} />}
633
- <Flexbox padding={16} style={{ minHeight: 220 }}>
634
- <Editor
635
- content={''}
636
- editor={editor}
637
- lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
638
- onInit={() => setEditorReady(true)}
639
- onTextChange={handleContentChange}
640
- placeholder={t('agentCronJobs.form.content.placeholder')}
641
- plugins={
642
- enableRichRender
643
- ? [
644
- ReactListPlugin,
645
- ReactCodePlugin,
646
- ReactCodemirrorPlugin,
647
- ReactHRPlugin,
648
- ReactLinkPlugin,
649
- ReactTablePlugin,
650
- ReactMathPlugin,
651
- ]
652
- : undefined
653
- }
654
- style={{ paddingBottom: 48 }}
655
- type={'text'}
656
- variant={'chat'}
657
- />
658
- </Flexbox>
659
- </Card>
660
- </Flexbox>
472
+ <CronJobHeader
473
+ enabled={cronJob?.enabled ?? false}
474
+ isNewJob={isNewJob}
475
+ name={draft.name}
476
+ onNameChange={(name) => updateDraft({ name })}
477
+ onToggleEnabled={handleToggleEnabled}
478
+ />
479
+
480
+ <CronJobScheduleConfig
481
+ hourlyInterval={draft.hourlyInterval}
482
+ maxExecutions={draft.maxExecutions}
483
+ onScheduleChange={(updates) => updateDraft(updates)}
484
+ scheduleType={draft.scheduleType}
485
+ timezone={draft.timezone}
486
+ triggerTime={draft.triggerTime}
487
+ weekdays={draft.weekdays}
488
+ />
489
+
490
+ <CronJobContentEditor
491
+ enableRichRender={enableRichRender}
492
+ initialValue={cronJob?.content || ''}
493
+ onChange={handleContentChange}
494
+ />
495
+
496
+ {isNewJob && (
497
+ <CronJobSaveButton
498
+ disabled={!draft.name || !draft.content}
499
+ loading={false}
500
+ onSave={handleSaveNewJob}
501
+ />
502
+ )}
661
503
  </Flexbox>
662
504
  )}
663
505
  </WideScreenContainer>