@lobehub/lobehub 2.0.0-next.250 → 2.0.0-next.252

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 (60) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +14 -0
  3. package/package.json +1 -1
  4. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +0 -2
  5. package/packages/database/migrations/meta/_journal.json +1 -1
  6. package/packages/database/src/models/__tests__/topics/topic.create.test.ts +37 -8
  7. package/packages/database/src/models/topic.ts +71 -4
  8. package/packages/database/src/schemas/agentCronJob.ts +1 -2
  9. package/packages/memory-user-memory/src/extractors/context.ts +1 -4
  10. package/packages/memory-user-memory/src/extractors/experience.ts +2 -8
  11. package/packages/memory-user-memory/src/extractors/preference.ts +2 -8
  12. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +123 -123
  13. package/packages/memory-user-memory/src/prompts/layers/context.ts +152 -152
  14. package/packages/memory-user-memory/src/prompts/layers/experience.ts +159 -159
  15. package/packages/memory-user-memory/src/prompts/layers/identity.ts +213 -213
  16. package/packages/memory-user-memory/src/prompts/layers/preference.ts +160 -160
  17. package/packages/memory-user-memory/src/services/extractExecutor.ts +33 -30
  18. package/packages/memory-user-memory/src/types.ts +10 -8
  19. package/packages/types/src/discover/mcp.ts +1 -1
  20. package/packages/types/src/topic/topic.ts +9 -0
  21. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Body.tsx +4 -1
  22. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +74 -0
  23. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicItem.tsx +40 -0
  24. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/index.tsx +140 -0
  25. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/index.tsx +1 -1
  26. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/index.tsx +1 -1
  27. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/index.tsx +1 -1
  28. package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +664 -0
  29. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobCards.tsx +160 -0
  30. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +202 -0
  31. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobList.tsx +137 -0
  32. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/hooks/useAgentCronJobs.ts +138 -0
  33. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/index.tsx +130 -0
  34. package/src/app/[variants]/(main)/chat/profile/features/ProfileEditor/index.tsx +33 -3
  35. package/src/app/[variants]/(main)/community/(detail)/assistant/features/Sidebar/ActionButton/AddAgent.tsx +6 -0
  36. package/src/app/[variants]/(main)/community/(list)/assistant/features/List/Item.tsx +12 -3
  37. package/src/app/[variants]/(main)/community/(list)/mcp/features/List/Item.tsx +14 -4
  38. package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
  39. package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +1 -1
  40. package/src/hooks/useFetchCronTopics.ts +29 -0
  41. package/src/hooks/useFetchCronTopicsWithJobInfo.ts +56 -0
  42. package/src/hooks/useFetchTopics.ts +4 -1
  43. package/src/locales/default/setting.ts +44 -1
  44. package/src/server/routers/lambda/agentCronJob.ts +367 -0
  45. package/src/server/routers/lambda/image/index.test.ts +2 -2
  46. package/src/server/routers/lambda/index.ts +2 -0
  47. package/src/server/routers/lambda/market/index.ts +45 -4
  48. package/src/server/routers/lambda/topic.ts +15 -3
  49. package/src/server/services/aiAgent/index.ts +18 -1
  50. package/src/server/services/discover/index.ts +29 -3
  51. package/src/server/services/memory/userMemory/extract.ts +14 -6
  52. package/src/services/agentCronJob.ts +95 -0
  53. package/src/services/discover.ts +38 -1
  54. package/src/services/topic/index.ts +1 -0
  55. package/src/store/chat/slices/topic/action.ts +53 -2
  56. package/src/store/chat/slices/topic/initialState.ts +1 -0
  57. package/src/store/chat/slices/topic/selectors.ts +14 -6
  58. package/src/store/tool/slices/mcpStore/action.test.ts +38 -0
  59. package/src/store/tool/slices/mcpStore/action.ts +18 -0
  60. package/src/tools/placeholders.ts +1 -4
@@ -0,0 +1,664 @@
1
+ 'use client';
2
+
3
+ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
+ 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 } from '@lobehub/editor/react';
15
+ import { ActionIcon, Flexbox, Icon, Input, Tag, Text } from '@lobehub/ui';
16
+ import { useDebounceFn } from 'ahooks';
17
+ import { App, Card, Checkbox, Empty, InputNumber, Select, Switch, TimePicker, message } from 'antd';
18
+ 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';
29
+ import { useTranslation } from 'react-i18next';
30
+ import { useParams } from 'react-router-dom';
31
+ import useSWR from 'swr';
32
+
33
+ import AutoSaveHint from '@/components/Editor/AutoSaveHint';
34
+ import Loading from '@/components/Loading/BrandTextLoading';
35
+ import type { ExecutionConditions, UpdateAgentCronJobData } from '@/database/schemas/agentCronJob';
36
+ import TypoBar from '@/features/EditorModal/Typobar';
37
+ import NavHeader from '@/features/NavHeader';
38
+ import WideScreenContainer from '@/features/WideScreenContainer';
39
+ import { useQueryRoute } from '@/hooks/useQueryRoute';
40
+ import { mutate } from '@/libs/swr';
41
+ import { lambdaClient } from '@/libs/trpc/client/lambda';
42
+ import { agentCronJobService } from '@/services/agentCronJob';
43
+ import { topicService } from '@/services/topic';
44
+ import { useAgentStore } from '@/store/agent';
45
+ import { useChatStore } from '@/store/chat';
46
+ import { useUserStore } from '@/store/user';
47
+ import { labPreferSelectors } from '@/store/user/selectors';
48
+
49
+ interface CronJobDraft {
50
+ content: string;
51
+ cronPattern: string;
52
+ description: string;
53
+ maxExecutions?: number | null;
54
+ maxExecutionsPerDay?: number | null;
55
+ name: string;
56
+ timeRange?: [Dayjs, Dayjs];
57
+ weekdays: number[];
58
+ }
59
+
60
+ type AutoSaveStatus = 'idle' | 'saving' | 'saved';
61
+ interface AutoSaveState {
62
+ lastUpdatedTime: Date | null | any;
63
+ status: AutoSaveStatus;
64
+ }
65
+
66
+ const autoSaveStore = {
67
+ listeners: new Set<() => void>(),
68
+ state: { lastUpdatedTime: null, status: 'idle' as AutoSaveStatus },
69
+ };
70
+
71
+ const getAutoSaveState = () => autoSaveStore.state;
72
+ const subscribeAutoSave = (listener: () => void) => {
73
+ autoSaveStore.listeners.add(listener);
74
+ return () => autoSaveStore.listeners.delete(listener);
75
+ };
76
+ const setAutoSaveState = (patch: Partial<AutoSaveState>) => {
77
+ autoSaveStore.state = { ...autoSaveStore.state, ...patch };
78
+ autoSaveStore.listeners.forEach((listener) => listener());
79
+ };
80
+ const useAutoSaveState = () =>
81
+ useSyncExternalStore(subscribeAutoSave, getAutoSaveState, getAutoSaveState);
82
+
83
+ const AutoSaveHintSlot = memo(() => {
84
+ const { lastUpdatedTime, status } = useAutoSaveState();
85
+ return <AutoSaveHint lastUpdatedTime={lastUpdatedTime} saveStatus={status} />;
86
+ });
87
+
88
+ const CRON_PATTERNS = [
89
+ { label: 'agentCronJobs.interval.30min', value: '0 */30 * * *' },
90
+ { label: 'agentCronJobs.interval.1hour', value: '0 0 * * *' },
91
+ { label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' },
92
+ { label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' },
93
+ { label: 'agentCronJobs.interval.daily', value: '0 0 0 * *' },
94
+ { label: 'agentCronJobs.interval.weekly', value: '0 0 0 * 0' },
95
+ ];
96
+
97
+ const WEEKDAY_OPTIONS = [
98
+ { label: 'Mon', value: 1 },
99
+ { label: 'Tue', value: 2 },
100
+ { label: 'Wed', value: 3 },
101
+ { label: 'Thu', value: 4 },
102
+ { label: 'Fri', value: 5 },
103
+ { label: 'Sat', value: 6 },
104
+ { label: 'Sun', value: 0 },
105
+ ];
106
+
107
+ const WEEKDAY_LABELS: Record<number, string> = {
108
+ 0: 'Sunday',
109
+ 1: 'Monday',
110
+ 2: 'Tuesday',
111
+ 3: 'Wednesday',
112
+ 4: 'Thursday',
113
+ 5: 'Friday',
114
+ 6: 'Saturday',
115
+ };
116
+
117
+ const getIntervalText = (cronPattern: string) => {
118
+ const intervalMap: Record<string, string> = {
119
+ '0 */12 * * *': 'agentCronJobs.interval.12hours',
120
+ '0 */30 * * *': 'agentCronJobs.interval.30min',
121
+ '0 */6 * * *': 'agentCronJobs.interval.6hours',
122
+ '0 0 * * *': 'agentCronJobs.interval.1hour',
123
+ '0 0 0 * *': 'agentCronJobs.interval.daily',
124
+ '0 0 0 * 0': 'agentCronJobs.interval.weekly',
125
+ };
126
+
127
+ return intervalMap[cronPattern] || cronPattern;
128
+ };
129
+
130
+ const resolveDate = (value?: Date | string | null) => {
131
+ if (!value) return null;
132
+ return typeof value === 'string' ? new Date(value) : value;
133
+ };
134
+
135
+ const CronJobDetailPage = memo(() => {
136
+ const { t } = useTranslation(['setting', 'common']);
137
+ const { aid, cronId } = useParams<{ aid?: string; cronId?: string }>();
138
+ const router = useQueryRoute();
139
+ const { modal } = App.useApp();
140
+ const editor = useEditor();
141
+ const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
142
+ const [editorReady, setEditorReady] = useState(false);
143
+
144
+ const [draft, setDraft] = useState<CronJobDraft | null>(null);
145
+ const draftRef = useRef<CronJobDraft | null>(null);
146
+ const contentRef = useRef('');
147
+ const pendingContentRef = useRef<string | null>(null);
148
+ const pendingSaveRef = useRef(false);
149
+ const initializedIdRef = useRef<string | null>(null);
150
+ const readyRef = useRef(false);
151
+ const lastSavedNameRef = useRef<string | null>(null);
152
+ const lastSavedPayloadRef = useRef<UpdateAgentCronJobData | null>(null);
153
+ const lastSavedAtRef = useRef<Date | null>(null);
154
+ const previousCronIdRef = useRef<string | null>(null);
155
+ const hydratedAtRef = useRef<string | null>(null);
156
+ const isDirtyRef = useRef(false);
157
+
158
+ const [activeTopicId, refreshTopic, switchTopic] = useChatStore((s) => [
159
+ s.activeTopicId,
160
+ s.refreshTopic,
161
+ s.switchTopic,
162
+ ]);
163
+
164
+ const activeAgentId = useAgentStore((s) => s.activeAgentId);
165
+ const cronListAgentId = activeAgentId || aid;
166
+
167
+ const { data: cronJob, isLoading } = useSWR(
168
+ ENABLE_BUSINESS_FEATURES && cronId ? ['cronJob', cronId] : null,
169
+ async () => {
170
+ if (!cronId) return null;
171
+ const result = await agentCronJobService.getById(cronId);
172
+ return result.success ? result.data : null;
173
+ },
174
+ {
175
+ dedupingInterval: 0,
176
+ revalidateIfStale: true,
177
+ revalidateOnFocus: false,
178
+ revalidateOnMount: true,
179
+ },
180
+ );
181
+
182
+ const resolvedCronPattern = draft ? draft.cronPattern : cronJob?.cronPattern;
183
+ const resolvedWeekdays = draft ? draft.weekdays : cronJob?.executionConditions?.weekdays || [];
184
+ const resolvedTimeRange = draft
185
+ ? draft.timeRange
186
+ : cronJob?.executionConditions?.timeRange
187
+ ? [
188
+ dayjs(cronJob.executionConditions.timeRange.start, 'HH:mm'),
189
+ dayjs(cronJob.executionConditions.timeRange.end, 'HH:mm'),
190
+ ]
191
+ : undefined;
192
+
193
+ const summaryTags = useMemo(() => {
194
+ const tags: Array<{ key: string; label: string }> = [];
195
+
196
+ if (resolvedCronPattern) {
197
+ tags.push({
198
+ key: 'interval',
199
+ label: t(getIntervalText(resolvedCronPattern) as any),
200
+ });
201
+ }
202
+
203
+ if (resolvedWeekdays.length > 0) {
204
+ tags.push({
205
+ key: 'weekdays',
206
+ label: resolvedWeekdays.map((day) => WEEKDAY_LABELS[day]).join(', '),
207
+ });
208
+ }
209
+
210
+ if (resolvedTimeRange && resolvedTimeRange.length === 2) {
211
+ tags.push({
212
+ key: 'timeRange',
213
+ label: `${resolvedTimeRange[0].format('HH:mm')} - ${resolvedTimeRange[1].format('HH:mm')}`,
214
+ });
215
+ }
216
+
217
+ return tags;
218
+ }, [resolvedCronPattern, resolvedTimeRange, resolvedWeekdays, t]);
219
+
220
+ const buildUpdateData = useCallback(
221
+ (snapshot: CronJobDraft | null, content: string): UpdateAgentCronJobData | null => {
222
+ if (!snapshot) return null;
223
+ if (!snapshot.content) return null;
224
+ if (!snapshot.name) return null;
225
+
226
+ const executionConditions: ExecutionConditions = {};
227
+ if (snapshot.timeRange && snapshot.timeRange.length === 2) {
228
+ executionConditions.timeRange = {
229
+ end: snapshot.timeRange[1].format('HH:mm'),
230
+ start: snapshot.timeRange[0].format('HH:mm'),
231
+ };
232
+ }
233
+
234
+ if (snapshot.weekdays && snapshot.weekdays.length > 0) {
235
+ executionConditions.weekdays = snapshot.weekdays;
236
+ }
237
+
238
+ if (snapshot.maxExecutionsPerDay) {
239
+ executionConditions.maxExecutionsPerDay = snapshot.maxExecutionsPerDay;
240
+ }
241
+
242
+ return {
243
+ content,
244
+ cronPattern: snapshot.cronPattern,
245
+ description: snapshot.description?.trim() || null,
246
+ executionConditions:
247
+ Object.keys(executionConditions).length > 0 ? executionConditions : null,
248
+ maxExecutions: snapshot.maxExecutions ?? null,
249
+ name: snapshot.name?.trim() || null,
250
+ };
251
+ },
252
+ [],
253
+ );
254
+
255
+ const refreshCronList = useCallback(() => {
256
+ if (!cronListAgentId) return;
257
+ void mutate(['cronTopicsWithJobInfo', cronListAgentId]);
258
+ }, [cronListAgentId]);
259
+
260
+ useEffect(() => {
261
+ const prevCronId = previousCronIdRef.current;
262
+ if (prevCronId && prevCronId !== cronId && lastSavedPayloadRef.current) {
263
+ const payload = lastSavedPayloadRef.current;
264
+ const updatedAt = lastSavedAtRef.current;
265
+ mutate(
266
+ ['cronJob', prevCronId],
267
+ (current) =>
268
+ current
269
+ ? {
270
+ ...current,
271
+ ...payload,
272
+ executionConditions: payload.executionConditions ?? null,
273
+ ...(updatedAt ? { updatedAt } : null),
274
+ }
275
+ : current,
276
+ false,
277
+ );
278
+ }
279
+
280
+ previousCronIdRef.current = cronId ?? null;
281
+ lastSavedPayloadRef.current = null;
282
+ lastSavedAtRef.current = null;
283
+ hydratedAtRef.current = null;
284
+ isDirtyRef.current = false;
285
+ }, [cronId]);
286
+
287
+ const { run: debouncedSave, cancel: cancelDebouncedSave } = useDebounceFn(
288
+ async () => {
289
+ if (!cronId || initializedIdRef.current !== cronId) return;
290
+ const payload = buildUpdateData(draftRef.current, contentRef.current);
291
+ if (!payload) return;
292
+ if (!payload.content || !payload.name) return;
293
+
294
+ try {
295
+ await agentCronJobService.update(cronId, payload);
296
+ const savedAt = new Date();
297
+ lastSavedPayloadRef.current = payload;
298
+ lastSavedAtRef.current = savedAt;
299
+ isDirtyRef.current = false;
300
+ setAutoSaveState({ lastUpdatedTime: savedAt, status: 'saved' });
301
+ const nextName = payload.name ?? null;
302
+ if (nextName !== lastSavedNameRef.current) {
303
+ lastSavedNameRef.current = nextName;
304
+ refreshCronList();
305
+ }
306
+ } catch (error) {
307
+ console.error('Failed to update cron job:', error);
308
+ setAutoSaveState({ status: 'idle' });
309
+ message.error('Failed to update scheduled task');
310
+ }
311
+ },
312
+ { wait: EDITOR_DEBOUNCE_TIME },
313
+ );
314
+
315
+ useEffect(() => {
316
+ cancelDebouncedSave();
317
+ pendingSaveRef.current = false;
318
+ }, [cancelDebouncedSave, cronId]);
319
+
320
+ const scheduleSave = useCallback(() => {
321
+ if (!readyRef.current || !draftRef.current) {
322
+ pendingSaveRef.current = true;
323
+ return;
324
+ }
325
+ isDirtyRef.current = true;
326
+ setAutoSaveState({ status: 'saving' });
327
+ debouncedSave();
328
+ }, [debouncedSave]);
329
+
330
+ const flushPendingSave = useCallback(() => {
331
+ if (!pendingSaveRef.current || !draftRef.current) return;
332
+ pendingSaveRef.current = false;
333
+ isDirtyRef.current = true;
334
+ setAutoSaveState({ status: 'saving' });
335
+ debouncedSave();
336
+ }, [debouncedSave]);
337
+
338
+ const updateDraft = useCallback(
339
+ (patch: Partial<CronJobDraft>) => {
340
+ setDraft((prev) => {
341
+ if (!prev) return prev;
342
+ const next = { ...prev, ...patch };
343
+ draftRef.current = next;
344
+ return next;
345
+ });
346
+ scheduleSave();
347
+ },
348
+ [scheduleSave],
349
+ );
350
+
351
+ const handleContentChange = useCallback(() => {
352
+ if (!readyRef.current || !editor || !editorReady) return;
353
+ const nextContent = enableRichRender
354
+ ? (editor.getDocument('markdown') as unknown as string)
355
+ : (editor.getDocument('text') as unknown as string);
356
+ contentRef.current = nextContent || '';
357
+ scheduleSave();
358
+ }, [editor, editorReady, enableRichRender, scheduleSave]);
359
+
360
+ const handleToggleEnabled = useCallback(
361
+ async (enabled: boolean) => {
362
+ if (!cronId) return;
363
+ setAutoSaveState({ status: 'saving' });
364
+ try {
365
+ await agentCronJobService.update(cronId, { enabled });
366
+ setAutoSaveState({ lastUpdatedTime: new Date(), status: 'saved' });
367
+ } catch (error) {
368
+ console.error('Failed to update cron job status:', error);
369
+ setAutoSaveState({ status: 'idle' });
370
+ message.error('Failed to update scheduled task');
371
+ }
372
+ },
373
+ [cronId, mutate, refreshCronList],
374
+ );
375
+
376
+ const handleDeleteCronJob = useCallback(() => {
377
+ if (!cronId) return;
378
+
379
+ modal.confirm({
380
+ centered: true,
381
+ okButtonProps: { danger: true },
382
+ onOk: async () => {
383
+ try {
384
+ let topicIds: string[] = [];
385
+ if (aid) {
386
+ const groups = await lambdaClient.topic.getCronTopicsGroupedByCronJob.query({
387
+ agentId: aid,
388
+ });
389
+ const group = groups.find((item) => item.cronJobId === cronId);
390
+ topicIds = group?.topics.map((topic) => topic.id) || [];
391
+ }
392
+
393
+ await agentCronJobService.delete(cronId);
394
+
395
+ if (topicIds.length > 0) {
396
+ await topicService.batchRemoveTopics(topicIds);
397
+ await refreshTopic();
398
+ if (activeTopicId && topicIds.includes(activeTopicId)) {
399
+ switchTopic();
400
+ }
401
+ }
402
+
403
+ if (cronListAgentId) {
404
+ await mutate(['cronTopicsWithJobInfo', cronListAgentId]);
405
+ router.push(`/agent/${cronListAgentId}`);
406
+ } else {
407
+ router.push('/');
408
+ }
409
+ } catch (error) {
410
+ console.error('Failed to delete cron job:', error);
411
+ message.error('Failed to delete scheduled task');
412
+ }
413
+ },
414
+ title: t('agentCronJobs.confirmDelete'),
415
+ });
416
+ }, [activeTopicId, cronId, cronListAgentId, modal, refreshTopic, router, switchTopic, t]);
417
+
418
+ useEffect(() => {
419
+ if (!cronJob) return;
420
+ const cronUpdatedAt = cronJob.updatedAt ? new Date(cronJob.updatedAt).toISOString() : null;
421
+ const shouldHydrate =
422
+ initializedIdRef.current !== cronJob.id ||
423
+ (cronUpdatedAt !== hydratedAtRef.current && !isDirtyRef.current);
424
+
425
+ if (!shouldHydrate) return;
426
+ initializedIdRef.current = cronJob.id;
427
+ hydratedAtRef.current = cronUpdatedAt;
428
+ isDirtyRef.current = false;
429
+ readyRef.current = false;
430
+ lastSavedNameRef.current = cronJob.name ?? null;
431
+
432
+ const nextDraft: CronJobDraft = {
433
+ content: cronJob.content || '',
434
+ cronPattern: cronJob.cronPattern,
435
+ description: cronJob.description || '',
436
+ maxExecutions: cronJob.maxExecutions ?? null,
437
+ maxExecutionsPerDay: cronJob.executionConditions?.maxExecutionsPerDay ?? null,
438
+ name: cronJob.name || '',
439
+ timeRange: cronJob.executionConditions?.timeRange
440
+ ? [
441
+ dayjs(cronJob.executionConditions.timeRange.start, 'HH:mm'),
442
+ dayjs(cronJob.executionConditions.timeRange.end, 'HH:mm'),
443
+ ]
444
+ : undefined,
445
+ weekdays: cronJob.executionConditions?.weekdays || [],
446
+ };
447
+
448
+ setDraft(nextDraft);
449
+ draftRef.current = nextDraft;
450
+
451
+ contentRef.current = nextDraft.content;
452
+ pendingContentRef.current = nextDraft.content;
453
+
454
+ setAutoSaveState({
455
+ lastUpdatedTime: resolveDate(cronJob.updatedAt),
456
+ status: 'saved',
457
+ });
458
+
459
+ if (editorReady && editor) {
460
+ try {
461
+ setTimeout(() => {
462
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', nextDraft.content);
463
+ }, 100);
464
+ pendingContentRef.current = null;
465
+ readyRef.current = true;
466
+ flushPendingSave();
467
+ } catch (error) {
468
+ console.error('[CronJobDetailPage] Failed to init editor content:', error);
469
+ setTimeout(() => {
470
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', nextDraft.content);
471
+ }, 100);
472
+ }
473
+ }
474
+ }, [cronJob, editor, editorReady, enableRichRender]);
475
+
476
+ useEffect(() => {
477
+ if (!editorReady || !editor || pendingContentRef.current === null) return;
478
+ try {
479
+ setTimeout(() => {
480
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', pendingContentRef.current);
481
+ }, 100);
482
+ pendingContentRef.current = null;
483
+ readyRef.current = true;
484
+ flushPendingSave();
485
+ } catch (error) {
486
+ console.error('[CronJobDetailPage] Failed to init editor content:', error);
487
+ setTimeout(() => {
488
+ console.log('setDocument timeout', pendingContentRef.current);
489
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', pendingContentRef.current);
490
+ }, 100);
491
+ }
492
+ }, [editor, editorReady, enableRichRender]);
493
+
494
+ if (!ENABLE_BUSINESS_FEATURES) {
495
+ return null;
496
+ }
497
+
498
+ return (
499
+ <Flexbox flex={1} height={'100%'}>
500
+ <NavHeader left={<AutoSaveHintSlot />} />
501
+ <Flexbox flex={1} style={{ overflowY: 'auto' }}>
502
+ <WideScreenContainer paddingBlock={16}>
503
+ {isLoading && <Loading debugId="CronJobDetailPage" />}
504
+ {!isLoading && !cronJob && (
505
+ <Empty
506
+ description={t('agentCronJobs.empty.description')}
507
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
508
+ />
509
+ )}
510
+ {!isLoading && cronJob && (
511
+ <Flexbox gap={24}>
512
+ <Flexbox align="center" gap={16} horizontal justify="space-between">
513
+ <Flexbox gap={6} style={{ flex: 1, minWidth: 0 }}>
514
+ <Input
515
+ onChange={(e) => updateDraft({ name: e.target.value })}
516
+ placeholder={t('agentCronJobs.form.name.placeholder')}
517
+ style={{
518
+ fontSize: 28,
519
+ fontWeight: 600,
520
+ padding: 0,
521
+ }}
522
+ value={draft?.name ?? cronJob.name ?? ''}
523
+ variant={'borderless'}
524
+ />
525
+ </Flexbox>
526
+ <Flexbox align="center" gap={8} horizontal>
527
+ <ActionIcon
528
+ icon={Trash2}
529
+ onClick={handleDeleteCronJob}
530
+ size={'small'}
531
+ title={t('delete', { ns: 'common' })}
532
+ />
533
+ <Text type="secondary">
534
+ {t(
535
+ cronJob?.enabled
536
+ ? 'agentCronJobs.status.enabled'
537
+ : 'agentCronJobs.status.disabled',
538
+ )}
539
+ </Text>
540
+ <Switch
541
+ defaultChecked={cronJob?.enabled ?? false}
542
+ disabled={!cronJob}
543
+ key={cronJob?.id ?? 'cron-switch'}
544
+ onChange={handleToggleEnabled}
545
+ size="small"
546
+ />
547
+ </Flexbox>
548
+ </Flexbox>
549
+
550
+ <Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
551
+ <Flexbox gap={12}>
552
+ {summaryTags.length > 0 && (
553
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
554
+ {summaryTags.map((tag) => (
555
+ <Tag key={tag.key} variant={'filled'}>
556
+ {tag.label}
557
+ </Tag>
558
+ ))}
559
+ </Flexbox>
560
+ )}
561
+
562
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
563
+ <Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
564
+ <Select
565
+ onChange={(value) => updateDraft({ cronPattern: value })}
566
+ options={CRON_PATTERNS.map((pattern) => ({
567
+ label: t(pattern.label as any),
568
+ value: pattern.value,
569
+ }))}
570
+ size="small"
571
+ style={{ minWidth: 160 }}
572
+ value={draft?.cronPattern ?? cronJob.cronPattern}
573
+ />
574
+ <TimePicker.RangePicker
575
+ format="HH:mm"
576
+ onChange={(value) =>
577
+ updateDraft({
578
+ timeRange:
579
+ value && value.length === 2
580
+ ? [value[0] as Dayjs, value[1] as Dayjs]
581
+ : undefined,
582
+ })
583
+ }
584
+ placeholder={[
585
+ t('agentCronJobs.form.timeRange.start'),
586
+ t('agentCronJobs.form.timeRange.end'),
587
+ ]}
588
+ size="small"
589
+ value={
590
+ draft?.timeRange ??
591
+ (resolvedTimeRange as [Dayjs, Dayjs] | undefined) ??
592
+ null
593
+ }
594
+ />
595
+ <Checkbox.Group
596
+ onChange={(values) => updateDraft({ weekdays: values as number[] })}
597
+ options={WEEKDAY_OPTIONS}
598
+ style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}
599
+ value={draft?.weekdays ?? resolvedWeekdays}
600
+ />
601
+ </Flexbox>
602
+
603
+ <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
604
+ <Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
605
+ <InputNumber
606
+ min={1}
607
+ onChange={(value) => updateDraft({ maxExecutions: value ?? null })}
608
+ placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
609
+ size="small"
610
+ style={{ width: 160 }}
611
+ value={draft?.maxExecutions ?? cronJob.maxExecutions ?? null}
612
+ />
613
+ </Flexbox>
614
+ </Flexbox>
615
+ </Card>
616
+
617
+ <Flexbox gap={12}>
618
+ <Flexbox align="center" gap={6} horizontal>
619
+ <Icon icon={Clock} size={16} />
620
+ <Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
621
+ </Flexbox>
622
+ <Card
623
+ size="small"
624
+ style={{ borderRadius: 12, overflow: 'hidden' }}
625
+ styles={{ body: { padding: 0 } }}
626
+ >
627
+ {enableRichRender && <TypoBar editor={editor} />}
628
+ <Flexbox padding={16} style={{ minHeight: 220 }}>
629
+ <Editor
630
+ content={''}
631
+ editor={editor}
632
+ lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
633
+ onInit={() => setEditorReady(true)}
634
+ onTextChange={handleContentChange}
635
+ placeholder={t('agentCronJobs.form.content.placeholder')}
636
+ plugins={
637
+ enableRichRender
638
+ ? [
639
+ ReactListPlugin,
640
+ ReactCodePlugin,
641
+ ReactCodemirrorPlugin,
642
+ ReactHRPlugin,
643
+ ReactLinkPlugin,
644
+ ReactTablePlugin,
645
+ ReactMathPlugin,
646
+ ]
647
+ : undefined
648
+ }
649
+ style={{ paddingBottom: 48 }}
650
+ type={'text'}
651
+ variant={'chat'}
652
+ />
653
+ </Flexbox>
654
+ </Card>
655
+ </Flexbox>
656
+ </Flexbox>
657
+ )}
658
+ </WideScreenContainer>
659
+ </Flexbox>
660
+ </Flexbox>
661
+ );
662
+ });
663
+
664
+ export default CronJobDetailPage;