@lobehub/lobehub 2.0.0-next.276 → 2.0.0-next.278

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 (98) hide show
  1. package/.cursor/rules/db-migrations.mdc +1 -1
  2. package/.cursor/rules/debug-usage.mdc +7 -5
  3. package/.cursor/rules/desktop-controller-tests.mdc +2 -1
  4. package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
  5. package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
  6. package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
  7. package/.cursor/rules/desktop-window-management.mdc +17 -2
  8. package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
  9. package/.cursor/rules/hotkey.mdc +1 -0
  10. package/.cursor/rules/i18n.mdc +1 -0
  11. package/.cursor/rules/project-structure.mdc +16 -3
  12. package/.cursor/rules/react.mdc +17 -5
  13. package/.cursor/rules/recent-data-usage.mdc +2 -1
  14. package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
  15. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
  16. package/.cursor/rules/zustand-action-patterns.mdc +1 -1
  17. package/.cursor/rules/zustand-slice-organization.mdc +4 -4
  18. package/CHANGELOG.md +51 -0
  19. package/CLAUDE.md +1 -1
  20. package/GEMINI.md +1 -1
  21. package/changelog/v1.json +14 -0
  22. package/docs/development/database-schema.dbml +16 -0
  23. package/locales/en-US/chat.json +24 -0
  24. package/locales/en-US/setting.json +11 -0
  25. package/locales/zh-CN/chat.json +24 -0
  26. package/locales/zh-CN/setting.json +11 -0
  27. package/package.json +1 -1
  28. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
  29. package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
  30. package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
  31. package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
  32. package/packages/business/const/src/index.ts +3 -0
  33. package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
  34. package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
  35. package/packages/database/migrations/meta/_journal.json +7 -0
  36. package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
  37. package/packages/database/src/models/topicShare.ts +177 -0
  38. package/packages/database/src/schemas/topic.ts +44 -2
  39. package/packages/types/src/agentCronJob/index.ts +19 -23
  40. package/packages/types/src/conversation.ts +5 -0
  41. package/packages/types/src/serverConfig.ts +1 -0
  42. package/packages/types/src/topic/topic.ts +46 -0
  43. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
  44. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
  45. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
  46. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
  47. package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
  48. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
  49. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
  50. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
  51. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
  52. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
  53. package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
  54. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
  55. package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
  56. package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
  57. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
  58. package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
  59. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
  60. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
  61. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
  62. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
  63. package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
  64. package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
  65. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
  66. package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
  67. package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
  68. package/src/app/robots.tsx +1 -1
  69. package/src/business/client/BusinessMobileRoutes.tsx +1 -1
  70. package/src/features/Conversation/ChatList/index.tsx +12 -5
  71. package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
  72. package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
  73. package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
  74. package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
  75. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
  76. package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
  77. package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
  78. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
  79. package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
  80. package/src/features/Conversation/Messages/index.tsx +1 -1
  81. package/src/features/Conversation/store/slices/data/action.ts +2 -1
  82. package/src/features/SharePopover/index.tsx +215 -0
  83. package/src/features/SharePopover/style.ts +10 -0
  84. package/src/libs/next/proxy/define-config.ts +4 -1
  85. package/src/locales/default/chat.ts +26 -0
  86. package/src/proxy.ts +1 -0
  87. package/src/server/globalConfig/index.ts +1 -0
  88. package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
  89. package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
  90. package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
  91. package/src/server/routers/lambda/index.ts +2 -0
  92. package/src/server/routers/lambda/message.ts +37 -4
  93. package/src/server/routers/lambda/share.ts +55 -0
  94. package/src/server/routers/lambda/topic.ts +45 -0
  95. package/src/services/chatGroup/index.ts +1 -4
  96. package/src/services/message/index.ts +1 -0
  97. package/src/services/topic/index.ts +16 -0
  98. package/src/store/serverConfig/selectors.ts +1 -0
@@ -1,6 +1,8 @@
1
1
  import type { BaseDataModel } from '../meta';
2
2
 
3
3
  // Type definitions
4
+ export type ShareVisibility = 'private' | 'link';
5
+
4
6
  export type TimeGroupId =
5
7
  | 'today'
6
8
  | 'yesterday'
@@ -126,3 +128,47 @@ export interface QueryTopicParams {
126
128
  isInbox?: boolean;
127
129
  pageSize?: number;
128
130
  }
131
+
132
+ /**
133
+ * Shared message data for public sharing
134
+ */
135
+ export interface SharedMessage {
136
+ content: string;
137
+ createdAt: Date;
138
+ id: string;
139
+ role: string;
140
+ }
141
+
142
+ /**
143
+ * Shared topic data returned by public API
144
+ */
145
+ export interface SharedTopicData {
146
+ agentId: string | null;
147
+ agentMeta?: {
148
+ avatar?: string | null;
149
+ backgroundColor?: string | null;
150
+ marketIdentifier?: string | null;
151
+ slug?: string | null;
152
+ title?: string | null;
153
+ };
154
+ groupId: string | null;
155
+ groupMeta?: {
156
+ avatar?: string | null;
157
+ backgroundColor?: string | null;
158
+ members?: { avatar: string | null; backgroundColor: string | null }[];
159
+ title?: string | null;
160
+ };
161
+ shareId: string;
162
+ title: string | null;
163
+ topicId: string;
164
+ visibility: ShareVisibility;
165
+ }
166
+
167
+ /**
168
+ * Topic share info returned to the owner
169
+ */
170
+ export interface TopicShareInfo {
171
+ id: string;
172
+ topicId: string;
173
+ visibility: ShareVisibility;
174
+ }
@@ -0,0 +1,31 @@
1
+ import { ActionIcon, Dropdown } from '@lobehub/ui';
2
+ import { MoreHorizontal } from 'lucide-react';
3
+ import { memo } from 'react';
4
+
5
+ import { useCronJobDropdownMenu } from './useDropdownMenu';
6
+
7
+ interface ActionsProps {
8
+ cronJobId: string;
9
+ topics: Array<{ id: string }>;
10
+ }
11
+
12
+ const Actions = memo<ActionsProps>(({ cronJobId, topics }) => {
13
+ const dropdownMenu = useCronJobDropdownMenu(cronJobId, topics);
14
+
15
+ return (
16
+ <Dropdown
17
+ arrow={false}
18
+ menu={{
19
+ items: dropdownMenu,
20
+ onClick: ({ domEvent }) => {
21
+ domEvent.stopPropagation();
22
+ },
23
+ }}
24
+ trigger={['click']}
25
+ >
26
+ <ActionIcon icon={MoreHorizontal} size={'small'} />
27
+ </Dropdown>
28
+ );
29
+ });
30
+
31
+ export default Actions;
@@ -9,6 +9,7 @@ import { useParams } from 'react-router-dom';
9
9
  import { useRouter } from '@/app/[variants]/(main)/hooks/useRouter';
10
10
  import type { AgentCronJob } from '@/database/schemas/agentCronJob';
11
11
 
12
+ import Actions from './Actions';
12
13
  import CronTopicItem from './CronTopicItem';
13
14
 
14
15
  interface CronTopicGroupProps {
@@ -43,12 +44,15 @@ const CronTopicGroup = memo<CronTopicGroupProps>(({ cronJob, cronJobId, topics }
43
44
  return (
44
45
  <AccordionItem
45
46
  action={
46
- <ActionIcon
47
- icon={Settings2Icon}
48
- onClick={handleOpenCronJob}
49
- size="small"
50
- title={t('agentCronJobs.editJob')}
51
- />
47
+ <Flexbox align="center" gap={4} horizontal>
48
+ <ActionIcon
49
+ icon={Settings2Icon}
50
+ onClick={handleOpenCronJob}
51
+ size="small"
52
+ title={t('agentCronJobs.editJob')}
53
+ />
54
+ <Actions cronJobId={cronJobId} topics={topics} />
55
+ </Flexbox>
52
56
  }
53
57
  itemKey={cronJobId}
54
58
  paddingBlock={4}
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
3
  import { Accordion, AccordionItem, ActionIcon, Flexbox, Text } from '@lobehub/ui';
5
4
  import { Plus } from 'lucide-react';
6
5
  import { memo, useCallback } from 'react';
@@ -12,6 +11,7 @@ import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
12
11
  import SkeletonList from '@/features/NavPanel/components/SkeletonList';
13
12
  import { useQueryRoute } from '@/hooks/useQueryRoute';
14
13
  import { useAgentStore } from '@/store/agent';
14
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
15
15
 
16
16
  import CronTopicGroup from './CronTopicGroup';
17
17
 
@@ -22,24 +22,20 @@ interface CronTopicListProps {
22
22
  const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
23
23
  const { t } = useTranslation('setting');
24
24
  const router = useQueryRoute();
25
- const [agentId, createAgentCronJob, useFetchCronTopicsWithJobInfo] = useAgentStore((s) => [
25
+ const [agentId, useFetchCronTopicsWithJobInfo] = useAgentStore((s) => [
26
26
  s.activeAgentId,
27
- s.createAgentCronJob,
28
27
  s.useFetchCronTopicsWithJobInfo,
29
28
  ]);
30
29
  const { data: cronTopicsGroupsWithJobInfo = [], isLoading } =
31
30
  useFetchCronTopicsWithJobInfo(agentId);
31
+ const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
32
32
 
33
- const handleCreateCronJob = useCallback(async () => {
33
+ const handleCreateCronJob = useCallback(() => {
34
34
  if (!agentId) return;
35
+ router.push(urlJoin('/agent', agentId, 'cron', 'new'));
36
+ }, [agentId, router]);
35
37
 
36
- const cronJobId = await createAgentCronJob();
37
- if (cronJobId) {
38
- router.push(urlJoin('/agent', agentId, 'cron', cronJobId));
39
- }
40
- }, [agentId, createAgentCronJob, router]);
41
-
42
- if (!ENABLE_BUSINESS_FEATURES) return null;
38
+ if (!enableBusinessFeatures) return null;
43
39
 
44
40
  const addAction = (
45
41
  <ActionIcon
@@ -0,0 +1,102 @@
1
+ import { Icon, type MenuProps } from '@lobehub/ui';
2
+ import { App } from 'antd';
3
+ import { Trash } from 'lucide-react';
4
+ import { useCallback, useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { agentCronJobService } from '@/services/agentCronJob';
8
+ import { topicService } from '@/services/topic';
9
+ import { useAgentStore } from '@/store/agent';
10
+
11
+ export const useCronJobDropdownMenu = (
12
+ cronJobId: string,
13
+ topics: Array<{ id: string }>,
14
+ ): MenuProps['items'] => {
15
+ const { t } = useTranslation(['setting', 'common']);
16
+ const { modal } = App.useApp();
17
+
18
+ const refreshCronTopics = useAgentStore((s) => s.internal_refreshCronTopics);
19
+
20
+ const handleDeleteCronJob = useCallback(async () => {
21
+ try {
22
+ // Delete all topics associated with this cron job
23
+ if (topics.length > 0) {
24
+ const topicIds = topics.map((t) => t.id);
25
+ await topicService.batchRemoveTopics(topicIds);
26
+ }
27
+
28
+ // Delete the cron job
29
+ await agentCronJobService.delete(cronJobId);
30
+
31
+ // Refresh the cron topics list
32
+ await refreshCronTopics();
33
+ } catch (error) {
34
+ console.error('Failed to delete cron job:', error);
35
+ modal.error({
36
+ content: t('agentCronJobs.deleteFailed' as any),
37
+ title: t('error' as any, { ns: 'common' }),
38
+ });
39
+ }
40
+ }, [cronJobId, topics, refreshCronTopics, modal, t]);
41
+
42
+ const handleClearTopics = useCallback(async () => {
43
+ if (topics.length === 0) return;
44
+
45
+ try {
46
+ const topicIds = topics.map((t) => t.id);
47
+ await topicService.batchRemoveTopics(topicIds);
48
+
49
+ // Refresh the cron topics list
50
+ await refreshCronTopics();
51
+ } catch (error) {
52
+ console.error('Failed to clear topics:', error);
53
+ modal.error({
54
+ content: t('agentCronJobs.clearTopicsFailed' as any),
55
+ title: t('error' as any, { ns: 'common' }),
56
+ });
57
+ }
58
+ }, [topics, refreshCronTopics, modal, t]);
59
+
60
+ return useMemo(
61
+ () =>
62
+ [
63
+ {
64
+ icon: <Icon icon={Trash} />,
65
+ key: 'clearTopics',
66
+ label: t('agentCronJobs.clearTopics' as any),
67
+ onClick: () => {
68
+ modal.confirm({
69
+ cancelText: t('cancel', { ns: 'common' }),
70
+ centered: true,
71
+ content: t('agentCronJobs.confirmClearTopics' as any, { count: topics.length }),
72
+ okButtonProps: { danger: true },
73
+ okText: t('ok', { ns: 'common' }),
74
+ onOk: handleClearTopics,
75
+ title: t('agentCronJobs.clearTopics' as any),
76
+ });
77
+ },
78
+ },
79
+ {
80
+ type: 'divider' as const,
81
+ },
82
+ {
83
+ danger: true,
84
+ icon: <Icon icon={Trash} />,
85
+ key: 'deleteCronJob',
86
+ label: t('agentCronJobs.deleteCronJob' as any),
87
+ onClick: () => {
88
+ modal.confirm({
89
+ cancelText: t('cancel', { ns: 'common' }),
90
+ centered: true,
91
+ content: t('agentCronJobs.confirmDeleteCronJob' as any),
92
+ okButtonProps: { danger: true },
93
+ okText: t('ok', { ns: 'common' }),
94
+ onOk: handleDeleteCronJob,
95
+ title: t('agentCronJobs.deleteCronJob' as any),
96
+ });
97
+ },
98
+ },
99
+ ].filter(Boolean) as MenuProps['items'],
100
+ [topics.length, handleClearTopics, handleDeleteCronJob, t, modal],
101
+ );
102
+ };
@@ -0,0 +1,179 @@
1
+ import type { Dayjs } from 'dayjs';
2
+
3
+ export type ScheduleType = 'daily' | 'hourly' | 'weekly';
4
+
5
+ // Schedule type options
6
+ export const SCHEDULE_TYPE_OPTIONS = [
7
+ { label: 'agentCronJobs.scheduleType.daily', value: 'daily' },
8
+ { label: 'agentCronJobs.scheduleType.hourly', value: 'hourly' },
9
+ { label: 'agentCronJobs.scheduleType.weekly', value: 'weekly' },
10
+ ] as const;
11
+
12
+ // Timezone options - covering major cities worldwide
13
+ export const TIMEZONE_OPTIONS = [
14
+ { label: 'UTC', value: 'UTC' },
15
+
16
+ // Americas
17
+ { label: 'America/New_York (EST/EDT, UTC-5/-4)', value: 'America/New_York' },
18
+ { label: 'America/Chicago (CST/CDT, UTC-6/-5)', value: 'America/Chicago' },
19
+ { label: 'America/Denver (MST/MDT, UTC-7/-6)', value: 'America/Denver' },
20
+ { label: 'America/Los_Angeles (PST/PDT, UTC-8/-7)', value: 'America/Los_Angeles' },
21
+ { label: 'America/Toronto (EST/EDT, UTC-5/-4)', value: 'America/Toronto' },
22
+ { label: 'America/Vancouver (PST/PDT, UTC-8/-7)', value: 'America/Vancouver' },
23
+ { label: 'America/Mexico_City (CST, UTC-6)', value: 'America/Mexico_City' },
24
+ { label: 'America/Sao_Paulo (BRT, UTC-3)', value: 'America/Sao_Paulo' },
25
+ { label: 'America/Buenos_Aires (ART, UTC-3)', value: 'America/Buenos_Aires' },
26
+
27
+ // Europe
28
+ { label: 'Europe/London (GMT/BST, UTC+0/+1)', value: 'Europe/London' },
29
+ { label: 'Europe/Paris (CET/CEST, UTC+1/+2)', value: 'Europe/Paris' },
30
+ { label: 'Europe/Berlin (CET/CEST, UTC+1/+2)', value: 'Europe/Berlin' },
31
+ { label: 'Europe/Madrid (CET/CEST, UTC+1/+2)', value: 'Europe/Madrid' },
32
+ { label: 'Europe/Rome (CET/CEST, UTC+1/+2)', value: 'Europe/Rome' },
33
+ { label: 'Europe/Amsterdam (CET/CEST, UTC+1/+2)', value: 'Europe/Amsterdam' },
34
+ { label: 'Europe/Brussels (CET/CEST, UTC+1/+2)', value: 'Europe/Brussels' },
35
+ { label: 'Europe/Moscow (MSK, UTC+3)', value: 'Europe/Moscow' },
36
+ { label: 'Europe/Istanbul (TRT, UTC+3)', value: 'Europe/Istanbul' },
37
+
38
+ // Asia
39
+ { label: 'Asia/Dubai (GST, UTC+4)', value: 'Asia/Dubai' },
40
+ { label: 'Asia/Kolkata (IST, UTC+5:30)', value: 'Asia/Kolkata' },
41
+ { label: 'Asia/Shanghai (CST, UTC+8)', value: 'Asia/Shanghai' },
42
+ { label: 'Asia/Hong_Kong (HKT, UTC+8)', value: 'Asia/Hong_Kong' },
43
+ { label: 'Asia/Taipei (CST, UTC+8)', value: 'Asia/Taipei' },
44
+ { label: 'Asia/Singapore (SGT, UTC+8)', value: 'Asia/Singapore' },
45
+ { label: 'Asia/Tokyo (JST, UTC+9)', value: 'Asia/Tokyo' },
46
+ { label: 'Asia/Seoul (KST, UTC+9)', value: 'Asia/Seoul' },
47
+ { label: 'Asia/Bangkok (ICT, UTC+7)', value: 'Asia/Bangkok' },
48
+ { label: 'Asia/Jakarta (WIB, UTC+7)', value: 'Asia/Jakarta' },
49
+
50
+ // Oceania
51
+ { label: 'Australia/Sydney (AEDT/AEST, UTC+11/+10)', value: 'Australia/Sydney' },
52
+ { label: 'Australia/Melbourne (AEDT/AEST, UTC+11/+10)', value: 'Australia/Melbourne' },
53
+ { label: 'Australia/Brisbane (AEST, UTC+10)', value: 'Australia/Brisbane' },
54
+ { label: 'Australia/Perth (AWST, UTC+8)', value: 'Australia/Perth' },
55
+ { label: 'Pacific/Auckland (NZDT/NZST, UTC+13/+12)', value: 'Pacific/Auckland' },
56
+
57
+ // Africa & Middle East
58
+ { label: 'Africa/Cairo (EET, UTC+2)', value: 'Africa/Cairo' },
59
+ { label: 'Africa/Johannesburg (SAST, UTC+2)', value: 'Africa/Johannesburg' },
60
+ ];
61
+
62
+ // Weekday options for checkbox group
63
+ export const WEEKDAY_OPTIONS = [
64
+ { label: 'Mon', value: 1 },
65
+ { label: 'Tue', value: 2 },
66
+ { label: 'Wed', value: 3 },
67
+ { label: 'Thu', value: 4 },
68
+ { label: 'Fri', value: 5 },
69
+ { label: 'Sat', value: 6 },
70
+ { label: 'Sun', value: 0 },
71
+ ];
72
+
73
+ // Weekday labels for display
74
+ export const WEEKDAY_LABELS: Record<number, string> = {
75
+ 0: 'Sunday',
76
+ 1: 'Monday',
77
+ 2: 'Tuesday',
78
+ 3: 'Wednesday',
79
+ 4: 'Thursday',
80
+ 5: 'Friday',
81
+ 6: 'Saturday',
82
+ };
83
+
84
+ /**
85
+ * Parse cron pattern to extract schedule info
86
+ * Format: minute hour day month weekday
87
+ */
88
+ export const parseCronPattern = (
89
+ cronPattern: string,
90
+ ): {
91
+ hourlyInterval?: number;
92
+ scheduleType: ScheduleType;
93
+ triggerHour: number;
94
+ triggerMinute: number;
95
+ weekdays?: number[];
96
+ } => {
97
+ const parts = cronPattern.split(' ');
98
+ if (parts.length !== 5) {
99
+ return { scheduleType: 'daily', triggerHour: 0, triggerMinute: 0 };
100
+ }
101
+
102
+ // eslint-disable-next-line unicorn/no-unreadable-array-destructuring
103
+ const [minute, hour, , , weekday] = parts;
104
+ const rawMinute = minute === '*' ? 0 : Number.parseInt(minute);
105
+ // Normalize to nearest 30-minute interval (0 or 30)
106
+ const triggerMinute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
107
+
108
+ // Hourly: 0 * * * * or 0 */N * * *
109
+ if (hour.startsWith('*/')) {
110
+ const interval = Number.parseInt(hour.slice(2));
111
+ return {
112
+ hourlyInterval: interval,
113
+ scheduleType: 'hourly',
114
+ triggerHour: 0,
115
+ triggerMinute,
116
+ };
117
+ }
118
+ if (hour === '*') {
119
+ return {
120
+ hourlyInterval: 1,
121
+ scheduleType: 'hourly',
122
+ triggerHour: 0,
123
+ triggerMinute,
124
+ };
125
+ }
126
+
127
+ const triggerHour = Number.parseInt(hour);
128
+
129
+ // Weekly: has specific weekday(s)
130
+ if (weekday !== '*') {
131
+ const weekdays = weekday.split(',').map((d) => Number.parseInt(d));
132
+ return {
133
+ scheduleType: 'weekly',
134
+ triggerHour,
135
+ triggerMinute,
136
+ weekdays,
137
+ };
138
+ }
139
+
140
+ // Daily: specific hour, any weekday
141
+ return {
142
+ scheduleType: 'daily',
143
+ triggerHour,
144
+ triggerMinute,
145
+ };
146
+ };
147
+
148
+ /**
149
+ * Build cron pattern from schedule info
150
+ * Format: minute hour day month weekday
151
+ */
152
+ export const buildCronPattern = (
153
+ scheduleType: ScheduleType,
154
+ triggerTime: Dayjs,
155
+ hourlyInterval?: number,
156
+ weekdays?: number[],
157
+ ): string => {
158
+ const rawMinute = triggerTime.minute();
159
+ // Normalize to 0 or 30
160
+ const minute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
161
+ const hour = triggerTime.hour();
162
+
163
+ switch (scheduleType) {
164
+ case 'hourly': {
165
+ const interval = hourlyInterval || 1;
166
+ if (interval === 1) {
167
+ return `${minute} * * * *`;
168
+ }
169
+ return `${minute} */${interval} * * *`;
170
+ }
171
+ case 'daily': {
172
+ return `${minute} ${hour} * * *`;
173
+ }
174
+ case 'weekly': {
175
+ const days = weekdays && weekdays.length > 0 ? weekdays.sort().join(',') : '0,1,2,3,4,5,6';
176
+ return `${minute} ${hour} * * ${days}`;
177
+ }
178
+ }
179
+ };
@@ -0,0 +1,111 @@
1
+ import {
2
+ ReactCodePlugin,
3
+ ReactCodemirrorPlugin,
4
+ ReactHRPlugin,
5
+ ReactLinkPlugin,
6
+ ReactListPlugin,
7
+ ReactMathPlugin,
8
+ ReactTablePlugin,
9
+ } from '@lobehub/editor';
10
+ import { Editor, useEditor } from '@lobehub/editor/react';
11
+ import { Flexbox, Icon, Text } from '@lobehub/ui';
12
+ import { Card } from 'antd';
13
+ import { Clock } from 'lucide-react';
14
+ import { memo, useCallback, useEffect, useRef } from 'react';
15
+ import { useTranslation } from 'react-i18next';
16
+
17
+ interface CronJobContentEditorProps {
18
+ enableRichRender: boolean;
19
+ initialValue: string;
20
+ onChange: (value: string) => void;
21
+ }
22
+
23
+ const CronJobContentEditor = memo<CronJobContentEditorProps>(
24
+ ({ enableRichRender, initialValue, onChange }) => {
25
+ const { t } = useTranslation('setting');
26
+ const editor = useEditor();
27
+ const currentValueRef = useRef(initialValue);
28
+
29
+ // Update currentValueRef when initialValue changes
30
+ useEffect(() => {
31
+ currentValueRef.current = initialValue;
32
+ }, [initialValue]);
33
+
34
+ // Initialize editor content when editor is ready
35
+ useEffect(() => {
36
+ if (!editor) return;
37
+ try {
38
+ setTimeout(() => {
39
+ if (initialValue) {
40
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
41
+ }
42
+ }, 100);
43
+ } catch (error) {
44
+ console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
45
+ setTimeout(() => {
46
+ editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
47
+ }, 100);
48
+ }
49
+ }, [editor, enableRichRender, initialValue]);
50
+
51
+ // Handle content changes
52
+ const handleContentChange = useCallback(
53
+ (e: any) => {
54
+ const nextContent = enableRichRender
55
+ ? (e.getDocument('markdown') as unknown as string)
56
+ : (e.getDocument('text') as unknown as string);
57
+
58
+ const finalContent = nextContent || '';
59
+
60
+ // Only call onChange if content actually changed
61
+ if (finalContent !== currentValueRef.current) {
62
+ currentValueRef.current = finalContent;
63
+ onChange(finalContent);
64
+ }
65
+ },
66
+ [enableRichRender, onChange],
67
+ );
68
+
69
+ return (
70
+ <Flexbox gap={12}>
71
+ <Flexbox align="center" gap={6} horizontal>
72
+ <Icon icon={Clock} size={16} />
73
+ <Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
74
+ </Flexbox>
75
+ <Card
76
+ size="small"
77
+ style={{ borderRadius: 12, overflow: 'hidden' }}
78
+ styles={{ body: { padding: 0 } }}
79
+ >
80
+ <Flexbox padding={16} style={{ minHeight: 220 }}>
81
+ <Editor
82
+ content={''}
83
+ editor={editor}
84
+ lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
85
+ onTextChange={handleContentChange}
86
+ placeholder={t('agentCronJobs.form.content.placeholder')}
87
+ plugins={
88
+ enableRichRender
89
+ ? [
90
+ ReactListPlugin,
91
+ ReactCodePlugin,
92
+ ReactCodemirrorPlugin,
93
+ ReactHRPlugin,
94
+ ReactLinkPlugin,
95
+ ReactTablePlugin,
96
+ ReactMathPlugin,
97
+ ]
98
+ : undefined
99
+ }
100
+ style={{ paddingBottom: 48 }}
101
+ type={'text'}
102
+ variant={'chat'}
103
+ />
104
+ </Flexbox>
105
+ </Card>
106
+ </Flexbox>
107
+ );
108
+ },
109
+ );
110
+
111
+ export default CronJobContentEditor;
@@ -0,0 +1,45 @@
1
+ import { Flexbox, Input } from '@lobehub/ui';
2
+ import { Switch } from 'antd';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface CronJobHeaderProps {
7
+ enabled?: boolean;
8
+ isNewJob?: boolean;
9
+ name: string;
10
+ onNameChange: (name: string) => void;
11
+ onToggleEnabled?: (enabled: boolean) => void;
12
+ }
13
+
14
+ const CronJobHeader = memo<CronJobHeaderProps>(
15
+ ({ enabled, isNewJob, name, onNameChange, onToggleEnabled }) => {
16
+ const { t } = useTranslation(['setting', 'common']);
17
+
18
+ return (
19
+ <Flexbox gap={16}>
20
+ {/* Title Input */}
21
+ <Input
22
+ onChange={(e) => onNameChange(e.target.value)}
23
+ placeholder={t('agentCronJobs.form.name.placeholder')}
24
+ style={{
25
+ fontSize: 28,
26
+ fontWeight: 600,
27
+ padding: 0,
28
+ }}
29
+ value={name}
30
+ variant={'borderless'}
31
+ />
32
+
33
+ {/* Controls Row */}
34
+ {!isNewJob && (
35
+ <Flexbox align="center" gap={12} horizontal>
36
+ {/* Enable/Disable Switch */}
37
+ <Switch checked={enabled ?? false} onChange={onToggleEnabled} />
38
+ </Flexbox>
39
+ )}
40
+ </Flexbox>
41
+ );
42
+ },
43
+ );
44
+
45
+ export default CronJobHeader;
@@ -0,0 +1,31 @@
1
+ import { Button, Flexbox } from '@lobehub/ui';
2
+ import { Save } from 'lucide-react';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ interface CronJobSaveButtonProps {
7
+ disabled?: boolean;
8
+ loading?: boolean;
9
+ onSave: () => void;
10
+ }
11
+
12
+ const CronJobSaveButton = memo<CronJobSaveButtonProps>(({ disabled, loading, onSave }) => {
13
+ const { t } = useTranslation('setting');
14
+
15
+ return (
16
+ <Flexbox paddingBlock={16}>
17
+ <Button
18
+ disabled={disabled}
19
+ icon={Save}
20
+ loading={loading}
21
+ onClick={onSave}
22
+ style={{ width: 200 }}
23
+ type="primary"
24
+ >
25
+ {t('agentCronJobs.saveAsNew', { defaultValue: 'Save as New Scheduled Task' })}
26
+ </Button>
27
+ </Flexbox>
28
+ );
29
+ });
30
+
31
+ export default CronJobSaveButton;