@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.
- package/.cursor/rules/db-migrations.mdc +1 -1
- package/.cursor/rules/debug-usage.mdc +7 -5
- package/.cursor/rules/desktop-controller-tests.mdc +2 -1
- package/.cursor/rules/desktop-feature-implementation.mdc +9 -5
- package/.cursor/rules/desktop-local-tools-implement.mdc +67 -66
- package/.cursor/rules/desktop-menu-configuration.mdc +21 -9
- package/.cursor/rules/desktop-window-management.mdc +17 -2
- package/.cursor/rules/drizzle-schema-style-guide.mdc +6 -6
- package/.cursor/rules/hotkey.mdc +1 -0
- package/.cursor/rules/i18n.mdc +1 -0
- package/.cursor/rules/project-structure.mdc +16 -3
- package/.cursor/rules/react.mdc +17 -5
- package/.cursor/rules/recent-data-usage.mdc +2 -1
- package/.cursor/rules/testing-guide/testing-guide.mdc +262 -238
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +1 -1
- package/.cursor/rules/zustand-action-patterns.mdc +1 -1
- package/.cursor/rules/zustand-slice-organization.mdc +4 -4
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +1 -1
- package/GEMINI.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/development/database-schema.dbml +16 -0
- package/locales/en-US/chat.json +24 -0
- package/locales/en-US/setting.json +11 -0
- package/locales/zh-CN/chat.json +24 -0
- package/locales/zh-CN/setting.json +11 -0
- package/package.json +1 -1
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
- package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
- package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
- package/packages/business/const/src/index.ts +3 -0
- package/packages/database/migrations/0069_add_topic_shares_table.sql +22 -0
- package/packages/database/migrations/meta/0069_snapshot.json +9704 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/__tests__/topicShare.test.ts +318 -0
- package/packages/database/src/models/topicShare.ts +177 -0
- package/packages/database/src/schemas/topic.ts +44 -2
- package/packages/types/src/agentCronJob/index.ts +19 -23
- package/packages/types/src/conversation.ts +5 -0
- package/packages/types/src/serverConfig.ts +1 -0
- package/packages/types/src/topic/topic.ts +46 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
- package/src/app/[variants]/(main)/agent/features/Conversation/Header/ShareButton/index.tsx +24 -9
- package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
- package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
- package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
- package/src/app/[variants]/(main)/group/features/Conversation/Header/ShareButton/index.tsx +26 -9
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/AspectRatioSelect/index.tsx +1 -2
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ImageNum.tsx +54 -173
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ResolutionSelect.tsx +22 -67
- package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +18 -0
- package/src/app/[variants]/router/desktopRouter.config.tsx +18 -0
- package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +54 -0
- package/src/app/[variants]/share/t/[id]/_layout/index.tsx +170 -0
- package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +66 -0
- package/src/app/[variants]/share/t/[id]/index.tsx +112 -0
- package/src/app/robots.tsx +1 -1
- package/src/business/client/BusinessMobileRoutes.tsx +1 -1
- package/src/features/Conversation/ChatList/index.tsx +12 -5
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/index.tsx +8 -4
- package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +15 -10
- package/src/features/Conversation/Messages/AssistantGroup/Tools.tsx +3 -1
- package/src/features/Conversation/Messages/AssistantGroup/components/ContentBlock.tsx +3 -2
- package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +2 -2
- package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +25 -26
- package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +4 -2
- package/src/features/Conversation/Messages/Tool/Tool/index.tsx +16 -12
- package/src/features/Conversation/Messages/Tool/index.tsx +20 -11
- package/src/features/Conversation/Messages/index.tsx +1 -1
- package/src/features/Conversation/store/slices/data/action.ts +2 -1
- package/src/features/SharePopover/index.tsx +215 -0
- package/src/features/SharePopover/style.ts +10 -0
- package/src/libs/next/proxy/define-config.ts +4 -1
- package/src/locales/default/chat.ts +26 -0
- package/src/proxy.ts +1 -0
- package/src/server/globalConfig/index.ts +1 -0
- package/src/server/routers/lambda/__tests__/message.test.ts +152 -0
- package/src/server/routers/lambda/__tests__/share.test.ts +227 -0
- package/src/server/routers/lambda/__tests__/topic.test.ts +174 -0
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/message.ts +37 -4
- package/src/server/routers/lambda/share.ts +55 -0
- package/src/server/routers/lambda/topic.ts +45 -0
- package/src/services/chatGroup/index.ts +1 -4
- package/src/services/message/index.ts +1 -0
- package/src/services/topic/index.ts +16 -0
- 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
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
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(
|
|
33
|
+
const handleCreateCronJob = useCallback(() => {
|
|
34
34
|
if (!agentId) return;
|
|
35
|
+
router.push(urlJoin('/agent', agentId, 'cron', 'new'));
|
|
36
|
+
}, [agentId, router]);
|
|
35
37
|
|
|
36
|
-
|
|
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;
|