@lobehub/lobehub 2.0.0-next.269 → 2.0.0-next.270

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 (58) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/changelog/v1.json +5 -0
  3. package/locales/ar/common.json +3 -1
  4. package/locales/ar/discover.json +2 -0
  5. package/locales/ar/onboarding.json +4 -4
  6. package/locales/bg-BG/discover.json +2 -0
  7. package/locales/bg-BG/onboarding.json +4 -4
  8. package/locales/de-DE/discover.json +2 -0
  9. package/locales/de-DE/onboarding.json +0 -3
  10. package/locales/en-US/common.json +2 -0
  11. package/locales/en-US/discover.json +2 -0
  12. package/locales/en-US/onboarding.json +3 -3
  13. package/locales/es-ES/discover.json +2 -0
  14. package/locales/es-ES/onboarding.json +0 -3
  15. package/locales/fa-IR/discover.json +2 -0
  16. package/locales/fa-IR/onboarding.json +0 -3
  17. package/locales/fr-FR/discover.json +2 -0
  18. package/locales/fr-FR/onboarding.json +0 -3
  19. package/locales/it-IT/discover.json +2 -0
  20. package/locales/it-IT/onboarding.json +0 -3
  21. package/locales/ja-JP/discover.json +2 -0
  22. package/locales/ja-JP/onboarding.json +0 -3
  23. package/locales/ko-KR/discover.json +2 -0
  24. package/locales/ko-KR/onboarding.json +0 -3
  25. package/locales/nl-NL/discover.json +2 -0
  26. package/locales/nl-NL/onboarding.json +0 -3
  27. package/locales/pl-PL/discover.json +2 -0
  28. package/locales/pl-PL/onboarding.json +0 -3
  29. package/locales/pt-BR/discover.json +2 -0
  30. package/locales/pt-BR/onboarding.json +0 -3
  31. package/locales/ru-RU/discover.json +2 -0
  32. package/locales/ru-RU/onboarding.json +0 -3
  33. package/locales/tr-TR/discover.json +2 -0
  34. package/locales/tr-TR/onboarding.json +0 -3
  35. package/locales/vi-VN/discover.json +2 -0
  36. package/locales/vi-VN/onboarding.json +0 -3
  37. package/locales/zh-CN/common.json +2 -0
  38. package/locales/zh-CN/discover.json +2 -0
  39. package/locales/zh-CN/onboarding.json +0 -3
  40. package/locales/zh-TW/discover.json +2 -0
  41. package/locales/zh-TW/onboarding.json +0 -3
  42. package/package.json +1 -1
  43. package/packages/database/src/repositories/search/index.ts +108 -8
  44. package/packages/database/src/schemas/agentCronJob.ts +6 -104
  45. package/packages/types/src/agentCronJob/index.ts +115 -0
  46. package/packages/types/src/discover/assistants.ts +4 -2
  47. package/packages/types/src/index.ts +1 -0
  48. package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -1
  49. package/src/app/[variants]/(main)/community/(list)/assistant/features/Category/useCategory.tsx +6 -0
  50. package/src/app/[variants]/(main)/community/(list)/assistant/index.tsx +2 -2
  51. package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
  52. package/src/features/CommandMenu/SearchResults.tsx +27 -0
  53. package/src/features/CommandMenu/utils/queryParser.ts +1 -0
  54. package/src/locales/default/common.ts +2 -0
  55. package/src/locales/default/discover.ts +2 -0
  56. package/src/locales/default/onboarding.ts +3 -3
  57. package/src/server/routers/lambda/agentCronJob.ts +10 -10
  58. package/src/server/routers/lambda/search.ts +13 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.269",
3
+ "version": "2.0.0-next.270",
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",
@@ -9,6 +9,7 @@ export type SearchResultType =
9
9
  | 'agent'
10
10
  | 'topic'
11
11
  | 'file'
12
+ | 'folder'
12
13
  | 'memory'
13
14
  | 'message'
14
15
  | 'mcp'
@@ -60,6 +61,12 @@ export interface FileSearchResult extends BaseSearchResult {
60
61
  url: string | null;
61
62
  }
62
63
 
64
+ export interface FolderSearchResult extends BaseSearchResult {
65
+ knowledgeBaseId: string | null;
66
+ slug: string | null;
67
+ type: 'folder';
68
+ }
69
+
63
70
  export interface MessageSearchResult extends BaseSearchResult {
64
71
  agentId: string | null;
65
72
  content: string;
@@ -106,6 +113,7 @@ export type SearchResult =
106
113
  | AgentSearchResult
107
114
  | TopicSearchResult
108
115
  | FileSearchResult
116
+ | FolderSearchResult
109
117
  | MessageSearchResult
110
118
  | MCPSearchResult
111
119
  | PluginSearchResult
@@ -167,6 +175,9 @@ export class SearchRepo {
167
175
  if ((!type || type === 'file') && limits.file > 0) {
168
176
  queries.push(this.buildFileQuery(searchTerm, exactQuery, prefixQuery, limits.file));
169
177
  }
178
+ if ((!type || type === 'folder') && limits.folder > 0) {
179
+ queries.push(this.buildFolderQuery(searchTerm, exactQuery, prefixQuery, limits.folder));
180
+ }
170
181
  if ((!type || type === 'page') && limits.page > 0) {
171
182
  queries.push(this.buildPageQuery(searchTerm, exactQuery, prefixQuery, limits.page));
172
183
  }
@@ -192,7 +203,7 @@ export class SearchRepo {
192
203
  * Calculate result limits based on context
193
204
  * - Agent context: expand topics (6) and messages (6), limit others (3 each)
194
205
  * - Page context: expand pages (6), limit others (3 each)
195
- * - Resource context: expand files (6), limit others (3 each)
206
+ * - Resource context: expand files (6) and folders (6), limit others (3 each)
196
207
  * - General context: limit all types to 3 each
197
208
  */
198
209
  private calculateLimits(
@@ -203,6 +214,7 @@ export class SearchRepo {
203
214
  ): {
204
215
  agent: number;
205
216
  file: number;
217
+ folder: number;
206
218
  message: number;
207
219
  page: number;
208
220
  pageContent: number;
@@ -213,6 +225,7 @@ export class SearchRepo {
213
225
  return {
214
226
  agent: type === 'agent' ? baseLimit : 0,
215
227
  file: type === 'file' ? baseLimit : 0,
228
+ folder: type === 'folder' ? baseLimit : 0,
216
229
  message: type === 'message' ? baseLimit : 0,
217
230
  page: type === 'page' ? baseLimit : 0,
218
231
  pageContent: type === 'pageContent' ? baseLimit : 0,
@@ -225,6 +238,7 @@ export class SearchRepo {
225
238
  return {
226
239
  agent: 3,
227
240
  file: 3,
241
+ folder: 3,
228
242
  message: 3,
229
243
  page: 6,
230
244
  pageContent: 0, // Not available yet
@@ -232,11 +246,12 @@ export class SearchRepo {
232
246
  };
233
247
  }
234
248
 
235
- // Resource context: expand files to 6, limit others to 3
249
+ // Resource context: expand files and folders to 6, limit others to 3
236
250
  if (contextType === 'resource') {
237
251
  return {
238
252
  agent: 3,
239
253
  file: 6,
254
+ folder: 6,
240
255
  message: 3,
241
256
  page: 3,
242
257
  pageContent: 0, // Not available yet
@@ -249,6 +264,7 @@ export class SearchRepo {
249
264
  return {
250
265
  agent: 3,
251
266
  file: 3,
267
+ folder: 3,
252
268
  message: 6,
253
269
  page: 3,
254
270
  pageContent: 0, // Not available yet
@@ -260,6 +276,7 @@ export class SearchRepo {
260
276
  return {
261
277
  agent: 3,
262
278
  file: 3,
279
+ folder: 3,
263
280
  message: 3,
264
281
  page: 3,
265
282
  pageContent: 0, // Not available yet
@@ -267,6 +284,20 @@ export class SearchRepo {
267
284
  };
268
285
  }
269
286
 
287
+ /**
288
+ * Truncate content at database level with ellipsis indicator
289
+ * Uses SQL LEFT() function for efficient truncation
290
+ * Note: This helper is defined for documentation but not currently used.
291
+ * Truncation is implemented inline in query methods for better SQL readability.
292
+ */
293
+ private truncateContent(columnName: string, maxLength: number): string {
294
+ return `CASE
295
+ WHEN LENGTH(${columnName}) > ${maxLength}
296
+ THEN LEFT(${columnName}, ${maxLength}) || '...'
297
+ ELSE ${columnName}
298
+ END`;
299
+ }
300
+
270
301
  /**
271
302
  * Build agent search query
272
303
  * Searches: title, description, slug, tags (JSONB array)
@@ -362,7 +393,10 @@ export class SearchRepo {
362
393
  t.id,
363
394
  'topic' as type,
364
395
  t.title,
365
- t.content as description,
396
+ CASE
397
+ WHEN length(COALESCE(t.content, '')) > 200 THEN substring(COALESCE(t.content, ''), 1, 200) || '...'
398
+ ELSE t.content
399
+ END as description,
366
400
  NULL::varchar(100) as slug,
367
401
  NULL::text as avatar,
368
402
  NULL::text as background_color,
@@ -447,7 +481,7 @@ export class SearchRepo {
447
481
  m.id,
448
482
  'message' as type,
449
483
  CASE
450
- WHEN length(m.content) > 100 THEN substring(m.content, 1, 100) || '...'
484
+ WHEN length(m.content) > 200 THEN substring(m.content, 1, 200) || '...'
451
485
  ELSE m.content
452
486
  END as title,
453
487
  COALESCE(a.title, 'General Chat') as description,
@@ -492,7 +526,10 @@ export class SearchRepo {
492
526
  f.id,
493
527
  'file' as type,
494
528
  f.name as title,
495
- d.content as description,
529
+ CASE
530
+ WHEN length(COALESCE(d.content, '')) > 200 THEN substring(COALESCE(d.content, ''), 1, 200) || '...'
531
+ ELSE d.content
532
+ END as description,
496
533
  NULL::varchar(100) as slug,
497
534
  NULL::text as avatar,
498
535
  NULL::text as background_color,
@@ -520,13 +557,16 @@ export class SearchRepo {
520
557
  AND f.name ILIKE ${searchTerm}
521
558
  `;
522
559
 
523
- // Query for standalone documents (not pages and not linked to files)
560
+ // Query for standalone documents (not pages, not folders, and not linked to files)
524
561
  const documentQuery = sql`
525
562
  SELECT
526
563
  d.id,
527
564
  'file' as type,
528
565
  COALESCE(d.title, d.filename, 'Untitled') as title,
529
- d.content as description,
566
+ CASE
567
+ WHEN length(COALESCE(d.content, '')) > 200 THEN substring(COALESCE(d.content, ''), 1, 200) || '...'
568
+ ELSE d.content
569
+ END as description,
530
570
  NULL::varchar(100) as slug,
531
571
  NULL::text as avatar,
532
572
  NULL::text as background_color,
@@ -552,6 +592,7 @@ export class SearchRepo {
552
592
  WHERE d.user_id = ${this.userId}
553
593
  AND d.source_type != 'file'
554
594
  AND d.file_type != 'custom/document'
595
+ AND d.file_type != 'custom/folder'
555
596
  AND (
556
597
  COALESCE(d.title, '') ILIKE ${searchTerm}
557
598
  OR COALESCE(d.filename, '') ILIKE ${searchTerm}
@@ -571,6 +612,54 @@ export class SearchRepo {
571
612
  `;
572
613
  }
573
614
 
615
+ /**
616
+ * Build folder search query
617
+ * Searches folders in the documents table (file_type='custom/folder')
618
+ */
619
+ private buildFolderQuery(
620
+ searchTerm: string,
621
+ exactQuery: string,
622
+ prefixQuery: string,
623
+ limit: number,
624
+ ): ReturnType<typeof sql> {
625
+ return sql`
626
+ SELECT
627
+ d.id,
628
+ 'folder' as type,
629
+ COALESCE(d.title, d.filename, 'Untitled') as title,
630
+ d.description,
631
+ d.slug,
632
+ NULL::text as avatar,
633
+ NULL::text as background_color,
634
+ NULL::jsonb as tags,
635
+ d.created_at,
636
+ d.updated_at,
637
+ CASE
638
+ WHEN COALESCE(d.title, d.filename) ILIKE ${exactQuery} THEN 1
639
+ WHEN COALESCE(d.title, d.filename) ILIKE ${prefixQuery} THEN 2
640
+ ELSE 3
641
+ END as relevance,
642
+ NULL::boolean as favorite,
643
+ NULL::text as session_id,
644
+ NULL::text as agent_id,
645
+ COALESCE(d.title, d.filename, 'Untitled') as name,
646
+ d.file_type,
647
+ NULL::integer as size,
648
+ NULL::text as url,
649
+ d.knowledge_base_id
650
+ FROM ${documents} d
651
+ WHERE d.user_id = ${this.userId}
652
+ AND d.file_type = 'custom/folder'
653
+ AND (
654
+ COALESCE(d.title, '') ILIKE ${searchTerm}
655
+ OR COALESCE(d.filename, '') ILIKE ${searchTerm}
656
+ OR COALESCE(d.description, '') ILIKE ${searchTerm}
657
+ )
658
+ ORDER BY relevance ASC, updated_at DESC
659
+ LIMIT ${limit}
660
+ `;
661
+ }
662
+
574
663
  /**
575
664
  * Build page search query
576
665
  * Fast search on page titles only (no content search for better performance)
@@ -635,7 +724,10 @@ export class SearchRepo {
635
724
  d.id,
636
725
  'pageContent' as type,
637
726
  COALESCE(d.title, d.filename, 'Untitled') as title,
638
- d.content as description,
727
+ CASE
728
+ WHEN length(COALESCE(d.content, '')) > 200 THEN substring(COALESCE(d.content, ''), 1, 200) || '...'
729
+ ELSE d.content
730
+ END as description,
639
731
  NULL::varchar(100) as slug,
640
732
  NULL::text as avatar,
641
733
  NULL::text as background_color,
@@ -737,6 +829,14 @@ export class SearchRepo {
737
829
  url: row.url,
738
830
  };
739
831
  }
832
+ case 'folder': {
833
+ return {
834
+ ...base,
835
+ knowledgeBaseId: row.knowledge_base_id,
836
+ slug: row.slug,
837
+ type: 'folder' as const,
838
+ };
839
+ }
740
840
  case 'message': {
741
841
  return {
742
842
  ...base,
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { type ExecutionConditions } from '@lobechat/types';
2
3
  import { boolean, index, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
3
- import { createInsertSchema } from 'drizzle-zod';
4
- import { z } from 'zod';
5
4
 
6
5
  import { idGenerator } from '../utils/idGenerator';
7
6
  import { timestamps } from './_helpers';
@@ -9,16 +8,6 @@ import { agents } from './agent';
9
8
  import { chatGroups } from './chatGroup';
10
9
  import { users } from './user';
11
10
 
12
- // Execution conditions type for JSONB field
13
- export interface ExecutionConditions {
14
- maxExecutionsPerDay?: number;
15
- timeRange?: {
16
- end: string; // "18:00"
17
- start: string; // "09:00"
18
- };
19
- weekdays?: number[]; // [1,2,3,4,5] (Monday=1, Sunday=0)
20
- }
21
-
22
11
  // Agent cron jobs table - supports multiple cron jobs per agent
23
12
  export const agentCronJobs = pgTable(
24
13
  'agent_cron_jobs',
@@ -74,98 +63,11 @@ export const agentCronJobs = pgTable(
74
63
  ],
75
64
  );
76
65
 
77
- // Validation schemas
78
- export const cronPatternSchema = z
79
- .string()
80
- .regex(
81
- /^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
82
- 'Invalid cron pattern',
83
- );
84
-
85
- // Minimum 30 minutes validation (using standard cron format)
86
- export const minimumIntervalSchema = z.string().refine((pattern) => {
87
- // Standard cron format: minute hour day month weekday
88
- const allowedPatterns = [
89
- '*/30 * * * *', // Every 30 minutes
90
- '0 * * * *', // Every hour
91
- '0 */2 * * *', // Every 2 hours
92
- '0 */3 * * *', // Every 3 hours
93
- '0 */4 * * *', // Every 4 hours
94
- '0 */6 * * *', // Every 6 hours
95
- '0 */8 * * *', // Every 8 hours
96
- '0 */12 * * *', // Every 12 hours
97
- '0 0 * * *', // Daily at midnight
98
- '0 0 * * 0', // Weekly on Sunday
99
- '0 0 1 * *', // Monthly on 1st
100
- ];
101
-
102
- // Check if it matches allowed patterns
103
- if (allowedPatterns.includes(pattern)) {
104
- return true;
105
- }
106
-
107
- // Parse pattern to validate minimum 30-minute interval
108
- const parts = pattern.split(' ');
109
- if (parts.length !== 5) {
110
- return false;
111
- }
112
-
113
- const [minute, hour] = parts;
114
-
115
- // Allow minute intervals >= 30 (e.g., */30, */45, */60)
116
- if (minute.startsWith('*/')) {
117
- const interval = parseInt(minute.slice(2));
118
- if (!isNaN(interval) && interval >= 30) {
119
- return true;
120
- }
121
- }
122
-
123
- // Allow hourly patterns: 0 */N * * * where N >= 1
124
- if (minute === '0' && hour.startsWith('*/')) {
125
- const interval = parseInt(hour.slice(2));
126
- if (!isNaN(interval) && interval >= 1) {
127
- return true;
128
- }
129
- }
130
-
131
- // Allow specific hour patterns: 0 N * * * (runs once per day)
132
- if (minute === '0' && /^\d+$/.test(hour)) {
133
- const h = parseInt(hour);
134
- if (!isNaN(h) && h >= 0 && h <= 23) {
135
- return true;
136
- }
137
- }
138
-
139
- return false;
140
- }, 'Minimum execution interval is 30 minutes');
141
-
142
- export const executionConditionsSchema = z
143
- .object({
144
- maxExecutionsPerDay: z.number().min(1).max(100).optional(),
145
- timeRange: z
146
- .object({
147
- end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
148
- start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
149
- })
150
- .optional(),
151
- weekdays: z.array(z.number().min(0).max(6)).optional(),
152
- })
153
- .optional();
154
-
155
- export const insertAgentCronJobSchema = createInsertSchema(agentCronJobs, {
156
- cronPattern: minimumIntervalSchema,
157
- content: z.string().min(1).max(2000),
158
- editData: z.record(z.any()).optional(), // Allow any JSON structure for rich content
159
- name: z.string().max(100).optional(),
160
- description: z.string().max(500).optional(),
161
- maxExecutions: z.number().min(1).max(10_000).optional(),
162
- executionConditions: executionConditionsSchema,
163
- });
164
-
165
- export const updateAgentCronJobSchema = insertAgentCronJobSchema.partial();
166
-
167
66
  // Type exports
168
67
  export type NewAgentCronJob = typeof agentCronJobs.$inferInsert;
169
68
  export type AgentCronJob = typeof agentCronJobs.$inferSelect;
170
- export type CreateAgentCronJobData = z.infer<typeof insertAgentCronJobSchema>;
171
- export type UpdateAgentCronJobData = z.infer<typeof updateAgentCronJobSchema>;
69
+
70
+ // Re-export types from types package for consumers
71
+ export type { ExecutionConditions } from '@lobechat/types';
72
+ export type { InsertAgentCronJob as CreateAgentCronJobData } from '@lobechat/types';
73
+ export type { UpdateAgentCronJob as UpdateAgentCronJobData } from '@lobechat/types';
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+
3
+ // Execution conditions type
4
+ export interface ExecutionConditions {
5
+ maxExecutionsPerDay?: number;
6
+ timeRange?: {
7
+ end: string; // "18:00"
8
+ start: string; // "09:00"
9
+ };
10
+ weekdays?: number[]; // [1,2,3,4,5] (Monday=1, Sunday=0)
11
+ }
12
+
13
+ // Cron pattern validation schema
14
+ export const cronPatternSchema = z
15
+ .string()
16
+ .regex(
17
+ /^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
18
+ 'Invalid cron pattern',
19
+ );
20
+
21
+ // Minimum 30 minutes validation (using standard cron format)
22
+ export const minimumIntervalSchema = z.string().refine((pattern) => {
23
+ // Standard cron format: minute hour day month weekday
24
+ const allowedPatterns = [
25
+ '*/30 * * * *', // Every 30 minutes
26
+ '0 * * * *', // Every hour
27
+ '0 */2 * * *', // Every 2 hours
28
+ '0 */3 * * *', // Every 3 hours
29
+ '0 */4 * * *', // Every 4 hours
30
+ '0 */6 * * *', // Every 6 hours
31
+ '0 */8 * * *', // Every 8 hours
32
+ '0 */12 * * *', // Every 12 hours
33
+ '0 0 * * *', // Daily at midnight
34
+ '0 0 * * 0', // Weekly on Sunday
35
+ '0 0 1 * *', // Monthly on 1st
36
+ ];
37
+
38
+ // Check if it matches allowed patterns
39
+ if (allowedPatterns.includes(pattern)) {
40
+ return true;
41
+ }
42
+
43
+ // Parse pattern to validate minimum 30-minute interval
44
+ const parts = pattern.split(' ');
45
+ if (parts.length !== 5) {
46
+ return false;
47
+ }
48
+
49
+ const [minute, hour] = parts;
50
+
51
+ // Allow minute intervals >= 30 (e.g., */30, */45, */60)
52
+ if (minute.startsWith('*/')) {
53
+ const interval = parseInt(minute.slice(2));
54
+ if (!isNaN(interval) && interval >= 30) {
55
+ return true;
56
+ }
57
+ }
58
+
59
+ // Allow hourly patterns: 0 */N * * * where N >= 1
60
+ if (minute === '0' && hour.startsWith('*/')) {
61
+ const interval = parseInt(hour.slice(2));
62
+ if (!isNaN(interval) && interval >= 1) {
63
+ return true;
64
+ }
65
+ }
66
+
67
+ // Allow specific hour patterns: 0 N * * * (runs once per day)
68
+ if (minute === '0' && /^\d+$/.test(hour)) {
69
+ const h = parseInt(hour);
70
+ if (!isNaN(h) && h >= 0 && h <= 23) {
71
+ return true;
72
+ }
73
+ }
74
+
75
+ return false;
76
+ }, 'Minimum execution interval is 30 minutes');
77
+
78
+ // Execution conditions schema
79
+ export const ExecutionConditionsSchema = z
80
+ .object({
81
+ maxExecutionsPerDay: z.number().min(1).max(100).optional(),
82
+ timeRange: z
83
+ .object({
84
+ end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
85
+ start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
86
+ })
87
+ .optional(),
88
+ weekdays: z.array(z.number().min(0).max(6)).optional(),
89
+ })
90
+ .optional();
91
+
92
+ // Insert schema for creating agent cron jobs
93
+ export const InsertAgentCronJobSchema = z.object({
94
+ agentId: z.string(),
95
+ content: z.string(), // Allow empty content (when using editData for rich content)
96
+ cronPattern: minimumIntervalSchema,
97
+ description: z.string().optional().nullable(),
98
+ editData: z.record(z.string(), z.any()).optional().nullable(),
99
+ enabled: z.boolean().optional().nullable(),
100
+ executionConditions: ExecutionConditionsSchema.nullable(),
101
+ groupId: z.string().optional().nullable(),
102
+ id: z.string().optional(),
103
+ maxExecutions: z.number().min(1).max(10_000).optional().nullable(),
104
+ name: z.string().optional().nullable(),
105
+ remainingExecutions: z.number().optional().nullable(),
106
+ timezone: z.string().optional().nullable(),
107
+ userId: z.string().optional(),
108
+ });
109
+
110
+ // Update schema (all fields optional)
111
+ export const UpdateAgentCronJobSchema = InsertAgentCronJobSchema.partial();
112
+
113
+ // Type exports
114
+ export type InsertAgentCronJob = z.infer<typeof InsertAgentCronJobSchema>;
115
+ export type UpdateAgentCronJob = z.infer<typeof UpdateAgentCronJobSchema>;
@@ -8,6 +8,7 @@ export enum AssistantCategory {
8
8
  Career = 'career',
9
9
  CopyWriting = 'copywriting',
10
10
  Design = 'design',
11
+ Discover = 'discover',
11
12
  Education = 'education',
12
13
  Emotions = 'emotions',
13
14
  Entertainment = 'entertainment',
@@ -17,7 +18,7 @@ export enum AssistantCategory {
17
18
  Marketing = 'marketing',
18
19
  Office = 'office',
19
20
  Programming = 'programming',
20
- Translation = 'translation',
21
+ Translation = 'translation'
21
22
  }
22
23
 
23
24
  export enum AssistantSorts {
@@ -26,8 +27,9 @@ export enum AssistantSorts {
26
27
  KnowledgeCount = 'knowledgeCount',
27
28
  MyOwn = 'myown',
28
29
  PluginCount = 'pluginCount',
30
+ Recommended = 'recommended',
29
31
  Title = 'title',
30
- TokenUsage = 'tokenUsage',
32
+ TokenUsage = 'tokenUsage'
31
33
  }
32
34
 
33
35
  export enum AssistantNavKey {
@@ -1,4 +1,5 @@
1
1
  export * from './agent';
2
+ export * from './agentCronJob';
2
3
  export * from './agentGroup';
3
4
  export * from './aiChat';
4
5
  export * from './aiProvider';
@@ -4,7 +4,7 @@ import { memo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useDiscoverStore } from '@/store/discover';
7
- import { McpSorts } from '@/types/discover';
7
+ import { McpSorts, AssistantSorts } from '@/types/discover';
8
8
 
9
9
  import Title from '../../components/Title';
10
10
  import AssistantList from '../assistant/features/List';
@@ -19,6 +19,7 @@ const HomePage = memo(() => {
19
19
  const { data: assistantList, isLoading: assistantLoading } = useAssistantList({
20
20
  page: 1,
21
21
  pageSize: 12,
22
+ sort: AssistantSorts.Recommended,
22
23
  });
23
24
 
24
25
  const { data: mcpList, isLoading: pluginLoading } = useMcpList({
@@ -14,6 +14,7 @@ import {
14
14
  PencilIcon,
15
15
  PrinterIcon,
16
16
  TerminalSquareIcon,
17
+ CompassIcon,
17
18
  } from 'lucide-react';
18
19
  import { useMemo } from 'react';
19
20
  import { useTranslation } from 'react-i18next';
@@ -25,6 +26,11 @@ export const useCategory = () => {
25
26
 
26
27
  return useMemo(
27
28
  () => [
29
+ {
30
+ icon: CompassIcon,
31
+ key: AssistantCategory.Discover,
32
+ label: t('category.assistant.discover'),
33
+ },
28
34
  {
29
35
  icon: LayoutPanelTop,
30
36
  key: AssistantCategory.All,
@@ -5,7 +5,7 @@ import { memo } from 'react';
5
5
 
6
6
  import { useQuery } from '@/hooks/useQuery';
7
7
  import { useDiscoverStore } from '@/store/discover';
8
- import { type AssistantQueryParams, DiscoverTab } from '@/types/discover';
8
+ import { type AssistantQueryParams, AssistantSorts, DiscoverTab } from '@/types/discover';
9
9
 
10
10
  import Pagination from '../features/Pagination';
11
11
  import List from './features/List';
@@ -20,7 +20,7 @@ const AssistantPage = memo(() => {
20
20
  page,
21
21
  pageSize: 21,
22
22
  q,
23
- sort,
23
+ sort: sort ?? AssistantSorts.Recommended,
24
24
  source,
25
25
  });
26
26
 
@@ -27,6 +27,10 @@ const SortButton = memo(() => {
27
27
  switch (activeTab) {
28
28
  case DiscoverTab.Assistants: {
29
29
  const baseItems = [
30
+ {
31
+ key: AssistantSorts.Recommended,
32
+ label: t('assistants.sorts.recommended'),
33
+ },
30
34
  {
31
35
  key: AssistantSorts.CreatedAt,
32
36
  label: t('assistants.sorts.createdAt'),
@@ -4,6 +4,7 @@ import {
4
4
  Bot,
5
5
  ChevronRight,
6
6
  FileText,
7
+ Folder,
7
8
  MessageCircle,
8
9
  MessageSquare,
9
10
  Plug,
@@ -75,6 +76,18 @@ const SearchResults = memo<SearchResultsProps>(
75
76
  }
76
77
  break;
77
78
  }
79
+ case 'folder': {
80
+ // Navigate to folder by slug
81
+ if (result.knowledgeBaseId && result.slug) {
82
+ navigate(`/resource/library/${result.knowledgeBaseId}/${result.slug}`);
83
+ } else if (result.slug) {
84
+ navigate(`/resource/library/${result.slug}`);
85
+ } else {
86
+ // Fallback to library root if no slug
87
+ navigate(`/resource/library`);
88
+ }
89
+ break;
90
+ }
78
91
  case 'page': {
79
92
  navigate(`/page/${result.id.split('_')[1]}`);
80
93
  break;
@@ -109,6 +122,9 @@ const SearchResults = memo<SearchResultsProps>(
109
122
  case 'file': {
110
123
  return <FileText size={16} />;
111
124
  }
125
+ case 'folder': {
126
+ return <Folder size={16} />;
127
+ }
112
128
  case 'page': {
113
129
  return <FileText size={16} />;
114
130
  }
@@ -138,6 +154,9 @@ const SearchResults = memo<SearchResultsProps>(
138
154
  case 'file': {
139
155
  return t('cmdk.search.file');
140
156
  }
157
+ case 'folder': {
158
+ return t('cmdk.search.folder');
159
+ }
141
160
  case 'page': {
142
161
  return t('cmdk.search.page');
143
162
  }
@@ -198,6 +217,7 @@ const SearchResults = memo<SearchResultsProps>(
198
217
  const agentResults = results.filter((r) => r.type === 'agent');
199
218
  const topicResults = results.filter((r) => r.type === 'topic');
200
219
  const fileResults = results.filter((r) => r.type === 'file');
220
+ const folderResults = results.filter((r) => r.type === 'folder');
201
221
  const pageResults = results.filter((r) => r.type === 'page');
202
222
  const mcpResults = results.filter((r) => r.type === 'mcp');
203
223
  const pluginResults = results.filter((r) => r.type === 'plugin');
@@ -315,6 +335,13 @@ const SearchResults = memo<SearchResultsProps>(
315
335
  </Command.Group>
316
336
  )}
317
337
 
338
+ {folderResults.length > 0 && (
339
+ <Command.Group>
340
+ {folderResults.map((result) => renderResultItem(result))}
341
+ {renderSearchMore('folder', folderResults.length)}
342
+ </Command.Group>
343
+ )}
344
+
318
345
  {mcpResults.length > 0 && (
319
346
  <Command.Group>
320
347
  {mcpResults.map((result) => renderResultItem(result))}
@@ -16,6 +16,7 @@ const VALID_TYPES = [
16
16
  'topic',
17
17
  'message',
18
18
  'file',
19
+ 'folder',
19
20
  'page',
20
21
  'mcp',
21
22
  'plugin',