@lobehub/lobehub 2.0.13 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.1.0](https://github.com/lobehub/lobe-chat/compare/v2.0.13...v2.1.0)
6
+
7
+ <sup>Released on **2026-01-30**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Refactor cron job UI and use runtime enableBusinessFeatures flag.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Refactor cron job UI and use runtime enableBusinessFeatures flag, closes [#11975](https://github.com/lobehub/lobe-chat/issues/11975) ([104a19a](https://github.com/lobehub/lobe-chat/commit/104a19a))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 2.0.13](https://github.com/lobehub/lobe-chat/compare/v2.0.12...v2.0.13)
6
31
 
7
32
  <sup>Released on **2026-01-29**</sup>
package/changelog/v2.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Refactor cron job UI and use runtime enableBusinessFeatures flag."
6
+ ]
7
+ },
8
+ "date": "2026-01-30",
9
+ "version": "2.1.0"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
@@ -34,11 +34,20 @@
34
34
  "agentCronJobs.empty.description": "Create your first scheduled task to automate your agent",
35
35
  "agentCronJobs.empty.title": "No scheduled tasks yet",
36
36
  "agentCronJobs.enable": "Enable",
37
+ "agentCronJobs.form.at": "at",
37
38
  "agentCronJobs.form.content.placeholder": "Enter the prompt or instruction for the agent",
39
+ "agentCronJobs.form.every": "Every",
40
+ "agentCronJobs.form.frequency": "Frequency",
41
+ "agentCronJobs.form.hours": "hour(s)",
42
+ "agentCronJobs.form.maxExecutions": "Stop after",
38
43
  "agentCronJobs.form.maxExecutions.placeholder": "Leave empty for unlimited",
39
44
  "agentCronJobs.form.name.placeholder": "Enter task name",
45
+ "agentCronJobs.form.time": "Time",
40
46
  "agentCronJobs.form.timeRange.end": "End Time",
41
47
  "agentCronJobs.form.timeRange.start": "Start Time",
48
+ "agentCronJobs.form.times": "times",
49
+ "agentCronJobs.form.timezone": "Timezone",
50
+ "agentCronJobs.form.unlimited": "Run continuously",
42
51
  "agentCronJobs.form.validation.contentRequired": "Task content is required",
43
52
  "agentCronJobs.form.validation.invalidTimeRange": "Start time must be before end time",
44
53
  "agentCronJobs.form.validation.nameRequired": "Task name is required",
@@ -83,6 +92,13 @@
83
92
  "agentCronJobs.weekday.tuesday": "Tuesday",
84
93
  "agentCronJobs.weekday.wednesday": "Wednesday",
85
94
  "agentCronJobs.weekdays": "Weekdays",
95
+ "agentCronJobs.weekdays.fri": "Fri",
96
+ "agentCronJobs.weekdays.mon": "Mon",
97
+ "agentCronJobs.weekdays.sat": "Sat",
98
+ "agentCronJobs.weekdays.sun": "Sun",
99
+ "agentCronJobs.weekdays.thu": "Thu",
100
+ "agentCronJobs.weekdays.tue": "Tue",
101
+ "agentCronJobs.weekdays.wed": "Wed",
86
102
  "agentInfoDescription.basic.avatar": "Avatar",
87
103
  "agentInfoDescription.basic.description": "Description",
88
104
  "agentInfoDescription.basic.name": "Name",
@@ -34,11 +34,20 @@
34
34
  "agentCronJobs.empty.description": "创建您的第一个定时任务以实现智能体自动化",
35
35
  "agentCronJobs.empty.title": "暂无定时任务",
36
36
  "agentCronJobs.enable": "启用",
37
+ "agentCronJobs.form.at": "于",
37
38
  "agentCronJobs.form.content.placeholder": "输入智能体的提示词或指令",
39
+ "agentCronJobs.form.every": "每",
40
+ "agentCronJobs.form.frequency": "执行频率",
41
+ "agentCronJobs.form.hours": "小时",
42
+ "agentCronJobs.form.maxExecutions": "执行",
38
43
  "agentCronJobs.form.maxExecutions.placeholder": "留空表示无限次",
39
44
  "agentCronJobs.form.name.placeholder": "输入任务名称",
45
+ "agentCronJobs.form.time": "时间",
40
46
  "agentCronJobs.form.timeRange.end": "结束时间",
41
47
  "agentCronJobs.form.timeRange.start": "开始时间",
48
+ "agentCronJobs.form.times": "次后停止",
49
+ "agentCronJobs.form.timezone": "时区",
50
+ "agentCronJobs.form.unlimited": "持续执行",
42
51
  "agentCronJobs.form.validation.contentRequired": "任务内容不能为空",
43
52
  "agentCronJobs.form.validation.invalidTimeRange": "开始时间必须早于结束时间",
44
53
  "agentCronJobs.form.validation.nameRequired": "任务名称不能为空",
@@ -83,6 +92,13 @@
83
92
  "agentCronJobs.weekday.tuesday": "星期二",
84
93
  "agentCronJobs.weekday.wednesday": "星期三",
85
94
  "agentCronJobs.weekdays": "工作日",
95
+ "agentCronJobs.weekdays.fri": "周五",
96
+ "agentCronJobs.weekdays.mon": "周一",
97
+ "agentCronJobs.weekdays.sat": "周六",
98
+ "agentCronJobs.weekdays.sun": "周日",
99
+ "agentCronJobs.weekdays.thu": "周四",
100
+ "agentCronJobs.weekdays.tue": "周二",
101
+ "agentCronJobs.weekdays.wed": "周三",
86
102
  "agentInfoDescription.basic.avatar": "头像",
87
103
  "agentInfoDescription.basic.description": "描述",
88
104
  "agentInfoDescription.basic.name": "名称",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.13",
3
+ "version": "2.1.0",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -26,9 +26,11 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
26
26
  s.activeAgentId,
27
27
  s.useFetchCronTopicsWithJobInfo,
28
28
  ]);
29
- const { data: cronTopicsGroupsWithJobInfo = [], isLoading } =
30
- useFetchCronTopicsWithJobInfo(agentId);
31
29
  const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
30
+ const { data: cronTopicsGroupsWithJobInfo = [], isLoading } = useFetchCronTopicsWithJobInfo(
31
+ agentId,
32
+ enableBusinessFeatures,
33
+ );
32
34
 
33
35
  const handleCreateCronJob = useCallback(() => {
34
36
  if (!agentId) return;
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import {
2
4
  ReactCodePlugin,
3
5
  ReactCodemirrorPlugin,
@@ -8,12 +10,19 @@ import {
8
10
  ReactTablePlugin,
9
11
  } from '@lobehub/editor';
10
12
  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';
13
+ import { FormGroup } from '@lobehub/ui';
14
+ import { createStaticStyles } from 'antd-style';
14
15
  import { memo, useCallback, useEffect, useRef } from 'react';
15
16
  import { useTranslation } from 'react-i18next';
16
17
 
18
+ const styles = createStaticStyles(({ css }) => ({
19
+ editorWrapper: css`
20
+ min-height: 200px;
21
+ padding-block: 8px;
22
+ padding-inline: 0;
23
+ `,
24
+ }));
25
+
17
26
  interface CronJobContentEditorProps {
18
27
  enableRichRender: boolean;
19
28
  initialValue: string;
@@ -26,12 +35,10 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
26
35
  const editor = useEditor();
27
36
  const currentValueRef = useRef(initialValue);
28
37
 
29
- // Update currentValueRef when initialValue changes
30
38
  useEffect(() => {
31
39
  currentValueRef.current = initialValue;
32
40
  }, [initialValue]);
33
41
 
34
- // Initialize editor content when editor is ready
35
42
  useEffect(() => {
36
43
  if (!editor) return;
37
44
  try {
@@ -48,7 +55,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
48
55
  }
49
56
  }, [editor, enableRichRender, initialValue]);
50
57
 
51
- // Handle content changes
52
58
  const handleContentChange = useCallback(
53
59
  (e: any) => {
54
60
  const nextContent = enableRichRender
@@ -57,7 +63,6 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
57
63
 
58
64
  const finalContent = nextContent || '';
59
65
 
60
- // Only call onChange if content actually changed
61
66
  if (finalContent !== currentValueRef.current) {
62
67
  currentValueRef.current = finalContent;
63
68
  onChange(finalContent);
@@ -67,43 +72,33 @@ const CronJobContentEditor = memo<CronJobContentEditorProps>(
67
72
  );
68
73
 
69
74
  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>
75
+ <FormGroup title={t('agentCronJobs.content')} variant="filled">
76
+ <div className={styles.editorWrapper}>
77
+ <Editor
78
+ content={''}
79
+ editor={editor}
80
+ lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
81
+ onTextChange={handleContentChange}
82
+ placeholder={t('agentCronJobs.form.content.placeholder')}
83
+ plugins={
84
+ enableRichRender
85
+ ? [
86
+ ReactListPlugin,
87
+ ReactCodePlugin,
88
+ ReactCodemirrorPlugin,
89
+ ReactHRPlugin,
90
+ ReactLinkPlugin,
91
+ ReactTablePlugin,
92
+ ReactMathPlugin,
93
+ ]
94
+ : undefined
95
+ }
96
+ style={{ paddingBottom: 48 }}
97
+ type={'text'}
98
+ variant={'chat'}
99
+ />
100
+ </div>
101
+ </FormGroup>
107
102
  );
108
103
  },
109
104
  );
@@ -1,8 +1,19 @@
1
- import { Flexbox, Input } from '@lobehub/ui';
2
- import { Switch } from 'antd';
1
+ 'use client';
2
+
3
+ import { Flexbox, Input, LobeSwitch as Switch } from '@lobehub/ui';
4
+ import { createStaticStyles } from 'antd-style';
3
5
  import { memo } from 'react';
4
6
  import { useTranslation } from 'react-i18next';
5
7
 
8
+ const styles = createStaticStyles(({ css }) => ({
9
+ titleInput: css`
10
+ flex: 1;
11
+ font-size: 28px;
12
+ font-weight: 500;
13
+ line-height: 1.4;
14
+ `,
15
+ }));
16
+
6
17
  interface CronJobHeaderProps {
7
18
  enabled?: boolean;
8
19
  isNewJob?: boolean;
@@ -17,30 +28,26 @@ const CronJobHeader = memo<CronJobHeaderProps>(
17
28
  const { t } = useTranslation(['setting', 'common']);
18
29
 
19
30
  return (
20
- <Flexbox gap={16}>
21
- {/* Title Input */}
31
+ <Flexbox
32
+ align="center"
33
+ gap={16}
34
+ horizontal
35
+ justify="space-between"
36
+ style={{ marginBottom: 8 }}
37
+ >
22
38
  <Input
39
+ className={styles.titleInput}
23
40
  onChange={(e) => onNameChange(e.target.value)}
24
41
  placeholder={t('agentCronJobs.form.name.placeholder')}
25
- style={{
26
- fontSize: 28,
27
- fontWeight: 600,
28
- padding: 0,
29
- }}
30
42
  value={name}
31
- variant={'borderless'}
43
+ variant="borderless"
32
44
  />
33
-
34
- {/* Controls Row */}
35
45
  {!isNewJob && (
36
- <Flexbox align="center" gap={12} horizontal>
37
- {/* Enable/Disable Switch */}
38
- <Switch
39
- checked={enabled ?? false}
40
- loading={isTogglingEnabled}
41
- onChange={onToggleEnabled}
42
- />
43
- </Flexbox>
46
+ <Switch
47
+ checked={enabled ?? false}
48
+ loading={isTogglingEnabled}
49
+ onChange={onToggleEnabled}
50
+ />
44
51
  )}
45
52
  </Flexbox>
46
53
  );
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import { Button, Flexbox } from '@lobehub/ui';
2
4
  import { Save } from 'lucide-react';
3
5
  import { memo } from 'react';
@@ -13,13 +15,13 @@ const CronJobSaveButton = memo<CronJobSaveButtonProps>(({ disabled, loading, onS
13
15
  const { t } = useTranslation('setting');
14
16
 
15
17
  return (
16
- <Flexbox paddingBlock={16}>
18
+ <Flexbox paddingBlock={8}>
17
19
  <Button
18
20
  disabled={disabled}
19
21
  icon={Save}
20
22
  loading={loading}
21
23
  onClick={onSave}
22
- style={{ width: 200 }}
24
+ style={{ maxWidth: 200, width: '100%' }}
23
25
  type="primary"
24
26
  >
25
27
  {t('agentCronJobs.saveAsNew')}
@@ -1,17 +1,62 @@
1
- import { Flexbox, Tag, Text } from '@lobehub/ui';
2
- import { Card, InputNumber, Select, TimePicker } from 'antd';
1
+ 'use client';
2
+
3
+ import { Checkbox, Flexbox, FormGroup, LobeSelect as Select, Text } from '@lobehub/ui';
4
+ import { Divider, InputNumber, TimePicker } from 'antd';
5
+ import { createStaticStyles, cx } from 'antd-style';
3
6
  import type { Dayjs } from 'dayjs';
4
7
  import dayjs from 'dayjs';
5
- import { memo, useMemo } from 'react';
8
+ import { memo } from 'react';
6
9
  import { useTranslation } from 'react-i18next';
7
10
 
8
- import {
9
- SCHEDULE_TYPE_OPTIONS,
10
- type ScheduleType,
11
- TIMEZONE_OPTIONS,
12
- WEEKDAY_LABELS,
13
- WEEKDAY_OPTIONS,
14
- } from '../CronConfig';
11
+ import { SCHEDULE_TYPE_OPTIONS, type ScheduleType, TIMEZONE_OPTIONS } from '../CronConfig';
12
+
13
+ const styles = createStaticStyles(({ css, cssVar }) => ({
14
+ label: css`
15
+ flex-shrink: 0;
16
+ width: 120px;
17
+ `,
18
+ row: css`
19
+ min-height: 48px;
20
+ padding-block: 12px;
21
+ padding-inline: 0;
22
+ `,
23
+ weekdayButton: css`
24
+ cursor: pointer;
25
+
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+
30
+ width: 40px;
31
+ height: 32px;
32
+ border: 1px solid ${cssVar.colorBorder};
33
+ border-radius: 6px;
34
+
35
+ font-size: 12px;
36
+ font-weight: 500;
37
+ color: ${cssVar.colorTextSecondary};
38
+
39
+ background: transparent;
40
+
41
+ transition: all 0.15s ease;
42
+
43
+ &:hover {
44
+ border-color: ${cssVar.colorPrimary};
45
+ color: ${cssVar.colorPrimary};
46
+ }
47
+ `,
48
+ weekdayButtonActive: css`
49
+ border-color: ${cssVar.colorPrimary};
50
+ color: ${cssVar.colorTextLightSolid};
51
+ background: ${cssVar.colorPrimary};
52
+
53
+ &:hover {
54
+ border-color: ${cssVar.colorPrimaryHover};
55
+ color: ${cssVar.colorTextLightSolid};
56
+ background: ${cssVar.colorPrimaryHover};
57
+ }
58
+ `,
59
+ }));
15
60
 
16
61
  interface CronJobScheduleConfigProps {
17
62
  hourlyInterval?: number;
@@ -30,6 +75,16 @@ interface CronJobScheduleConfigProps {
30
75
  weekdays: number[];
31
76
  }
32
77
 
78
+ const WEEKDAYS = [
79
+ { key: 1, label: 'agentCronJobs.weekdays.mon' },
80
+ { key: 2, label: 'agentCronJobs.weekdays.tue' },
81
+ { key: 3, label: 'agentCronJobs.weekdays.wed' },
82
+ { key: 4, label: 'agentCronJobs.weekdays.thu' },
83
+ { key: 5, label: 'agentCronJobs.weekdays.fri' },
84
+ { key: 6, label: 'agentCronJobs.weekdays.sat' },
85
+ { key: 0, label: 'agentCronJobs.weekdays.sun' },
86
+ ];
87
+
33
88
  const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
34
89
  ({
35
90
  hourlyInterval,
@@ -42,172 +97,155 @@ const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
42
97
  }) => {
43
98
  const { t } = useTranslation('setting');
44
99
 
45
- // Compute summary tags
46
- const summaryTags = useMemo(() => {
47
- const result: Array<{ key: string; label: string }> = [];
48
-
49
- // Schedule type
50
- const scheduleTypeLabel = SCHEDULE_TYPE_OPTIONS.find(
51
- (opt) => opt.value === scheduleType,
52
- )?.label;
53
- if (scheduleTypeLabel) {
54
- result.push({
55
- key: 'scheduleType',
56
- label: t(scheduleTypeLabel as any),
57
- });
58
- }
59
-
60
- // Trigger time
61
- if (scheduleType === 'hourly') {
62
- const minute = triggerTime.minute();
63
- result.push({
64
- key: 'interval',
65
- label: `Every ${hourlyInterval || 1} hour(s) at :${minute.toString().padStart(2, '0')}`,
66
- });
67
- } else {
68
- result.push({
69
- key: 'triggerTime',
70
- label: triggerTime.format('HH:mm'),
71
- });
72
- }
73
-
74
- // Timezone
75
- result.push({
76
- key: 'timezone',
77
- label: timezone,
78
- });
79
-
80
- // Weekdays for weekly schedule
81
- if (scheduleType === 'weekly' && weekdays.length > 0) {
82
- result.push({
83
- key: 'weekdays',
84
- label: weekdays.map((day) => t(WEEKDAY_LABELS[day] as any)).join(', '),
85
- });
86
- }
87
-
88
- return result;
89
- }, [scheduleType, triggerTime, timezone, weekdays, hourlyInterval, t]);
100
+ const toggleWeekday = (day: number) => {
101
+ const newWeekdays = weekdays.includes(day)
102
+ ? weekdays.filter((d) => d !== day)
103
+ : [...weekdays, day];
104
+ onScheduleChange({ weekdays: newWeekdays });
105
+ };
106
+
107
+ const isUnlimited = maxExecutions === null || maxExecutions === undefined;
90
108
 
91
109
  return (
92
- <Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
93
- <Flexbox gap={12}>
94
- {/* Summary Tags */}
95
- {summaryTags.length > 0 && (
96
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
97
- {summaryTags.map((tag) => (
98
- <Tag key={tag.key} variant={'filled'}>
99
- {tag.label}
100
- </Tag>
101
- ))}
102
- </Flexbox>
103
- )}
104
- {/* Schedule Configuration - All in one row */}
105
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
106
- <Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
107
- <Select
108
- onChange={(value: ScheduleType) =>
109
- onScheduleChange({
110
- scheduleType: value,
111
- weekdays: value === 'weekly' ? [0, 1, 2, 3, 4, 5, 6] : [],
112
- })
113
- }
114
- options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
115
- label: t(opt.label as any),
116
- value: opt.value,
117
- }))}
118
- size="small"
119
- style={{ minWidth: 120 }}
120
- value={scheduleType}
121
- />
110
+ <FormGroup title={t('agentCronJobs.schedule')} variant="filled">
111
+ {/* Frequency Row */}
112
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
113
+ <Text className={styles.label}>{t('agentCronJobs.form.frequency')}</Text>
114
+ <Select
115
+ onChange={(value: ScheduleType) =>
116
+ onScheduleChange({
117
+ scheduleType: value,
118
+ weekdays: value === 'weekly' ? [1, 2, 3, 4, 5] : [],
119
+ })
120
+ }
121
+ options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
122
+ label: t(opt.label as any),
123
+ value: opt.value,
124
+ }))}
125
+ style={{ width: 140 }}
126
+ value={scheduleType}
127
+ variant="outlined"
128
+ />
129
+ </Flexbox>
122
130
 
123
- {/* Weekdays - show only for weekly */}
124
- {scheduleType === 'weekly' && (
125
- <Select
126
- maxTagCount="responsive"
127
- mode="multiple"
128
- onChange={(values: number[]) => onScheduleChange({ weekdays: values })}
129
- options={WEEKDAY_OPTIONS.map((opt) => ({
130
- label: t(opt.label as any),
131
- value: opt.value,
132
- }))}
133
- placeholder="Select days"
134
- size="small"
135
- style={{ minWidth: 150 }}
136
- value={weekdays}
137
- />
138
- )}
131
+ <Divider style={{ margin: 0 }} />
139
132
 
140
- {/* Trigger Time - show for daily and weekly */}
141
- {scheduleType !== 'hourly' && (
133
+ {/* Time Row (for daily/weekly) */}
134
+ {scheduleType !== 'hourly' && (
135
+ <>
136
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
137
+ <Text className={styles.label}>{t('agentCronJobs.form.time')}</Text>
142
138
  <TimePicker
143
139
  format="HH:mm"
144
- minuteStep={30}
140
+ minuteStep={15}
145
141
  onChange={(value) => {
146
142
  if (value) onScheduleChange({ triggerTime: value });
147
143
  }}
148
- size="small"
149
- style={{ minWidth: 120 }}
150
- value={triggerTime ?? dayjs().hour(0).minute(0)}
144
+ style={{ width: 120 }}
145
+ value={triggerTime ?? dayjs().hour(9).minute(0)}
151
146
  />
152
- )}
153
-
154
- {/* Hourly Interval - show only for hourly */}
155
- {scheduleType === 'hourly' && (
156
- <>
157
- <Tag variant={'borderless'}>Every</Tag>
147
+ </Flexbox>
148
+ <Divider style={{ margin: 0 }} />
149
+ </>
150
+ )}
151
+
152
+ {/* Hourly Interval Row */}
153
+ {scheduleType === 'hourly' && (
154
+ <>
155
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
156
+ <Text className={styles.label}>{t('agentCronJobs.form.every')}</Text>
157
+ <Flexbox align="center" gap={8} horizontal>
158
158
  <InputNumber
159
159
  max={24}
160
160
  min={1}
161
- onChange={(value: number | null) =>
162
- onScheduleChange({ hourlyInterval: value ?? 1 })
163
- }
164
- size="small"
165
- style={{ width: 80 }}
161
+ onChange={(value) => onScheduleChange({ hourlyInterval: value ?? 1 })}
162
+ style={{ width: 70 }}
166
163
  value={hourlyInterval ?? 1}
167
164
  />
168
- <Text type="secondary">hour(s) at</Text>
165
+ <Text type="secondary">{t('agentCronJobs.form.hours')}</Text>
166
+ <Text type="secondary">{t('agentCronJobs.form.at')}</Text>
169
167
  <Select
170
168
  onChange={(value: number) =>
171
169
  onScheduleChange({ triggerTime: dayjs().hour(0).minute(value) })
172
170
  }
173
171
  options={[
174
172
  { label: ':00', value: 0 },
173
+ { label: ':15', value: 15 },
175
174
  { label: ':30', value: 30 },
175
+ { label: ':45', value: 45 },
176
176
  ]}
177
- size="small"
178
- style={{ width: 80 }}
177
+ style={{ width: '80px' }}
179
178
  value={triggerTime?.minute() ?? 0}
179
+ variant="outlined"
180
180
  />
181
- </>
182
- )}
183
-
184
- {/* Timezone */}
185
- <Select
186
- onChange={(value: string) => onScheduleChange({ timezone: value })}
187
- options={TIMEZONE_OPTIONS}
188
- showSearch
189
- size="small"
190
- style={{ maxWidth: 300, minWidth: 200 }}
191
- value={timezone}
192
- />
193
- </Flexbox>
181
+ </Flexbox>
182
+ </Flexbox>
183
+ <Divider style={{ margin: 0 }} />
184
+ </>
185
+ )}
186
+
187
+ {/* Weekday Selector (only for weekly) */}
188
+ {scheduleType === 'weekly' && (
189
+ <>
190
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
191
+ <Text className={styles.label}>{t('agentCronJobs.weekdays')}</Text>
192
+ <Flexbox gap={6} horizontal>
193
+ {WEEKDAYS.map(({ key, label }) => (
194
+ <div
195
+ className={cx(
196
+ styles.weekdayButton,
197
+ weekdays.includes(key) && styles.weekdayButtonActive,
198
+ )}
199
+ key={key}
200
+ onClick={() => toggleWeekday(key)}
201
+ >
202
+ {t(label as any)}
203
+ </div>
204
+ ))}
205
+ </Flexbox>
206
+ </Flexbox>
207
+ <Divider style={{ margin: 0 }} />
208
+ </>
209
+ )}
210
+
211
+ {/* Timezone Row */}
212
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
213
+ <Text className={styles.label}>{t('agentCronJobs.form.timezone')}</Text>
214
+ <Select
215
+ onChange={(value: string) => onScheduleChange({ timezone: value })}
216
+ options={TIMEZONE_OPTIONS}
217
+ popupMatchSelectWidth={false}
218
+ showSearch
219
+ style={{ minWidth: '200px', width: 'fit-content' }}
220
+ value={timezone}
221
+ variant="outlined"
222
+ />
223
+ </Flexbox>
224
+
225
+ <Divider style={{ margin: 0 }} />
194
226
 
195
- {/* Max Executions */}
196
- <Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
197
- <Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
227
+ {/* Execution Limit Row */}
228
+ <Flexbox align="center" className={styles.row} gap={24} horizontal>
229
+ <Text className={styles.label}>{t('agentCronJobs.maxExecutions')}</Text>
230
+ <Flexbox align="center" gap={12} horizontal>
198
231
  <InputNumber
232
+ disabled={isUnlimited}
199
233
  min={1}
200
- onChange={(value: number | null) =>
201
- onScheduleChange({ maxExecutions: value ?? null })
202
- }
203
- placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
204
- size="small"
205
- style={{ width: 160 }}
206
- value={maxExecutions ?? null}
234
+ onChange={(value) => onScheduleChange({ maxExecutions: value })}
235
+ placeholder="100"
236
+ style={{ width: 100 }}
237
+ value={maxExecutions ?? undefined}
207
238
  />
239
+ <Text type="secondary">{t('agentCronJobs.form.times')}</Text>
240
+ <Checkbox
241
+ checked={isUnlimited}
242
+ onChange={(checked) => onScheduleChange({ maxExecutions: checked ? null : 100 })}
243
+ >
244
+ {t('agentCronJobs.form.unlimited')}
245
+ </Checkbox>
208
246
  </Flexbox>
209
247
  </Flexbox>
210
- </Card>
248
+ </FormGroup>
211
249
  );
212
250
  },
213
251
  );
@@ -1,4 +1,3 @@
1
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
2
1
  import { message } from 'antd';
3
2
  import { useCallback } from 'react';
4
3
  import { useTranslation } from 'react-i18next';
@@ -10,7 +9,7 @@ import type {
10
9
  } from '@/database/schemas/agentCronJob';
11
10
  import { agentCronJobService } from '@/services/agentCronJob';
12
11
 
13
- export const useAgentCronJobs = (agentId?: string) => {
12
+ export const useAgentCronJobs = (agentId?: string, enabled: boolean = true) => {
14
13
  const { t } = useTranslation('setting');
15
14
 
16
15
  // Fetch cron jobs for the agent
@@ -20,8 +19,8 @@ export const useAgentCronJobs = (agentId?: string) => {
20
19
  isLoading: loading,
21
20
  mutate,
22
21
  } = useSWR(
23
- ENABLE_BUSINESS_FEATURES && agentId ? `/api/agent-cron-jobs/${agentId}` : null,
24
- ENABLE_BUSINESS_FEATURES && agentId ? () => agentCronJobService.getByAgentId(agentId) : null,
22
+ enabled && agentId ? `/api/agent-cron-jobs/${agentId}` : null,
23
+ enabled && agentId ? () => agentCronJobService.getByAgentId(agentId) : null,
25
24
  {
26
25
  onError: (error) => {
27
26
  console.error('Failed to fetch cron jobs:', error);
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
3
  import { Flexbox } from '@lobehub/ui';
5
4
  import { Typography } from 'antd';
6
5
  import { Clock } from 'lucide-react';
@@ -10,6 +9,7 @@ import urlJoin from 'url-join';
10
9
 
11
10
  import { useQueryRoute } from '@/hooks/useQueryRoute';
12
11
  import { useAgentStore } from '@/store/agent';
12
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
13
13
 
14
14
  import CronJobCards from './CronJobCards';
15
15
  import { useAgentCronJobs } from './hooks/useAgentCronJobs';
@@ -20,8 +20,9 @@ const AgentCronJobs = memo(() => {
20
20
  const { t } = useTranslation('setting');
21
21
  const agentId = useAgentStore((s) => s.activeAgentId);
22
22
  const router = useQueryRoute();
23
+ const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
23
24
 
24
- const { cronJobs, loading, deleteCronJob } = useAgentCronJobs(agentId);
25
+ const { cronJobs, loading, deleteCronJob } = useAgentCronJobs(agentId, enableBusinessFeatures);
25
26
 
26
27
  // Edit: Navigate to cron job detail page
27
28
  const handleEdit = useCallback(
@@ -40,7 +41,7 @@ const AgentCronJobs = memo(() => {
40
41
  [deleteCronJob],
41
42
  );
42
43
 
43
- if (!ENABLE_BUSINESS_FEATURES) return null;
44
+ if (!enableBusinessFeatures) return null;
44
45
 
45
46
  if (!agentId) {
46
47
  return null;
@@ -1,6 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
3
  import { Button, Flexbox } from '@lobehub/ui';
5
4
  import { Divider } from 'antd';
6
5
  import { useTheme } from 'antd-style';
@@ -15,6 +14,7 @@ import { useQueryRoute } from '@/hooks/useQueryRoute';
15
14
  import { useAgentStore } from '@/store/agent';
16
15
  import { agentSelectors } from '@/store/agent/selectors';
17
16
  import { useChatStore } from '@/store/chat';
17
+ import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
18
18
 
19
19
  import AgentCronJobs from '../AgentCronJobs';
20
20
  import AgentSettings from '../AgentSettings';
@@ -31,6 +31,7 @@ const ProfileEditor = memo(() => {
31
31
  const agentId = useAgentStore((s) => s.activeAgentId);
32
32
  const switchTopic = useChatStore((s) => s.switchTopic);
33
33
  const router = useQueryRoute();
34
+ const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
34
35
 
35
36
  const handleCreateCronJob = useCallback(() => {
36
37
  if (!agentId) return;
@@ -95,7 +96,7 @@ const ProfileEditor = memo(() => {
95
96
  {t('startConversation')}
96
97
  </Button>
97
98
  <AgentPublishButton />
98
- {ENABLE_BUSINESS_FEATURES && (
99
+ {enableBusinessFeatures && (
99
100
  <Button icon={Clock} onClick={handleCreateCronJob}>
100
101
  {t('agentCronJobs.addJob')}
101
102
  </Button>
@@ -106,7 +107,7 @@ const ProfileEditor = memo(() => {
106
107
  {/* Main Content: Prompt Editor */}
107
108
  <EditorCanvas />
108
109
  {/* Agent Cron Jobs Display (only show if jobs exist) */}
109
- {ENABLE_BUSINESS_FEATURES && <AgentCronJobs />}
110
+ {enableBusinessFeatures && <AgentCronJobs />}
110
111
  {/* Advanced Settings Modal */}
111
112
  <AgentSettings />
112
113
  </>
@@ -37,11 +37,20 @@ export default {
37
37
  'agentCronJobs.empty.description': 'Create your first scheduled task to automate your agent',
38
38
  'agentCronJobs.empty.title': 'No scheduled tasks yet',
39
39
  'agentCronJobs.enable': 'Enable',
40
+ 'agentCronJobs.form.at': 'at',
40
41
  'agentCronJobs.form.content.placeholder': 'Enter the prompt or instruction for the agent',
42
+ 'agentCronJobs.form.every': 'Every',
43
+ 'agentCronJobs.form.frequency': 'Frequency',
44
+ 'agentCronJobs.form.hours': 'hour(s)',
45
+ 'agentCronJobs.form.maxExecutions': 'Stop after',
41
46
  'agentCronJobs.form.maxExecutions.placeholder': 'Leave empty for unlimited',
42
47
  'agentCronJobs.form.name.placeholder': 'Enter task name',
48
+ 'agentCronJobs.form.time': 'Time',
43
49
  'agentCronJobs.form.timeRange.end': 'End Time',
44
50
  'agentCronJobs.form.timeRange.start': 'Start Time',
51
+ 'agentCronJobs.form.times': 'times',
52
+ 'agentCronJobs.form.timezone': 'Timezone',
53
+ 'agentCronJobs.form.unlimited': 'Run continuously',
45
54
  'agentCronJobs.form.validation.contentRequired': 'Task content is required',
46
55
  'agentCronJobs.form.validation.invalidTimeRange': 'Start time must be before end time',
47
56
  'agentCronJobs.form.validation.nameRequired': 'Task name is required',
@@ -86,6 +95,13 @@ export default {
86
95
  'agentCronJobs.weekday.tuesday': 'Tuesday',
87
96
  'agentCronJobs.weekday.wednesday': 'Wednesday',
88
97
  'agentCronJobs.weekdays': 'Weekdays',
98
+ 'agentCronJobs.weekdays.fri': 'Fri',
99
+ 'agentCronJobs.weekdays.mon': 'Mon',
100
+ 'agentCronJobs.weekdays.sat': 'Sat',
101
+ 'agentCronJobs.weekdays.sun': 'Sun',
102
+ 'agentCronJobs.weekdays.thu': 'Thu',
103
+ 'agentCronJobs.weekdays.tue': 'Tue',
104
+ 'agentCronJobs.weekdays.wed': 'Wed',
89
105
  'agentInfoDescription.basic.avatar': 'Avatar',
90
106
  'agentInfoDescription.basic.description': 'Description',
91
107
  'agentInfoDescription.basic.name': 'Name',
@@ -1,4 +1,3 @@
1
- import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
2
1
  import type { SWRResponse } from 'swr';
3
2
  import { type StateCreator } from 'zustand/vanilla';
4
3
 
@@ -33,7 +32,10 @@ export interface CronTopicGroupWithJobInfo {
33
32
  export interface CronSliceAction {
34
33
  createAgentCronJob: () => Promise<string | null>;
35
34
  internal_refreshCronTopics: () => Promise<void>;
36
- useFetchCronTopicsWithJobInfo: (agentId?: string) => SWRResponse<CronTopicGroupWithJobInfo[]>;
35
+ useFetchCronTopicsWithJobInfo: (
36
+ agentId?: string,
37
+ enabled?: boolean,
38
+ ) => SWRResponse<CronTopicGroupWithJobInfo[]>;
37
39
  }
38
40
 
39
41
  export const createCronSlice: StateCreator<
@@ -69,9 +71,9 @@ export const createCronSlice: StateCreator<
69
71
  await mutate([FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, get().activeAgentId]);
70
72
  },
71
73
 
72
- useFetchCronTopicsWithJobInfo: (agentId) =>
74
+ useFetchCronTopicsWithJobInfo: (agentId, enabled = true) =>
73
75
  useClientDataSWR<CronTopicGroupWithJobInfo[]>(
74
- ENABLE_BUSINESS_FEATURES && agentId ? [FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, agentId] : null,
76
+ enabled && agentId ? [FETCH_CRON_TOPICS_WITH_JOB_INFO_KEY, agentId] : null,
75
77
  async ([, id]: [string, string]) => {
76
78
  const [cronJobsResult, cronTopicsGroups] = await Promise.all([
77
79
  lambdaClient.agentCronJob.findByAgent.query({ agentId: id }),