@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.
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +5 -0
- package/locales/ar/common.json +3 -1
- package/locales/ar/discover.json +2 -0
- package/locales/ar/onboarding.json +4 -4
- package/locales/bg-BG/discover.json +2 -0
- package/locales/bg-BG/onboarding.json +4 -4
- package/locales/de-DE/discover.json +2 -0
- package/locales/de-DE/onboarding.json +0 -3
- package/locales/en-US/common.json +2 -0
- package/locales/en-US/discover.json +2 -0
- package/locales/en-US/onboarding.json +3 -3
- package/locales/es-ES/discover.json +2 -0
- package/locales/es-ES/onboarding.json +0 -3
- package/locales/fa-IR/discover.json +2 -0
- package/locales/fa-IR/onboarding.json +0 -3
- package/locales/fr-FR/discover.json +2 -0
- package/locales/fr-FR/onboarding.json +0 -3
- package/locales/it-IT/discover.json +2 -0
- package/locales/it-IT/onboarding.json +0 -3
- package/locales/ja-JP/discover.json +2 -0
- package/locales/ja-JP/onboarding.json +0 -3
- package/locales/ko-KR/discover.json +2 -0
- package/locales/ko-KR/onboarding.json +0 -3
- package/locales/nl-NL/discover.json +2 -0
- package/locales/nl-NL/onboarding.json +0 -3
- package/locales/pl-PL/discover.json +2 -0
- package/locales/pl-PL/onboarding.json +0 -3
- package/locales/pt-BR/discover.json +2 -0
- package/locales/pt-BR/onboarding.json +0 -3
- package/locales/ru-RU/discover.json +2 -0
- package/locales/ru-RU/onboarding.json +0 -3
- package/locales/tr-TR/discover.json +2 -0
- package/locales/tr-TR/onboarding.json +0 -3
- package/locales/vi-VN/discover.json +2 -0
- package/locales/vi-VN/onboarding.json +0 -3
- package/locales/zh-CN/common.json +2 -0
- package/locales/zh-CN/discover.json +2 -0
- package/locales/zh-CN/onboarding.json +0 -3
- package/locales/zh-TW/discover.json +2 -0
- package/locales/zh-TW/onboarding.json +0 -3
- package/package.json +1 -1
- package/packages/database/src/repositories/search/index.ts +108 -8
- package/packages/database/src/schemas/agentCronJob.ts +6 -104
- package/packages/types/src/agentCronJob/index.ts +115 -0
- package/packages/types/src/discover/assistants.ts +4 -2
- package/packages/types/src/index.ts +1 -0
- package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -1
- package/src/app/[variants]/(main)/community/(list)/assistant/features/Category/useCategory.tsx +6 -0
- package/src/app/[variants]/(main)/community/(list)/assistant/index.tsx +2 -2
- package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
- package/src/features/CommandMenu/SearchResults.tsx +27 -0
- package/src/features/CommandMenu/utils/queryParser.ts +1 -0
- package/src/locales/default/common.ts +2 -0
- package/src/locales/default/discover.ts +2 -0
- package/src/locales/default/onboarding.ts +3 -3
- package/src/server/routers/lambda/agentCronJob.ts +10 -10
- 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.
|
|
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
|
-
|
|
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) >
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
export
|
|
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 {
|
|
@@ -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({
|
package/src/app/[variants]/(main)/community/(list)/assistant/features/Category/useCategory.tsx
CHANGED
|
@@ -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))}
|