@lobehub/lobehub 2.0.0-next.244 → 2.0.0-next.246
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/.github/workflows/bundle-analyzer.yml +2 -2
- package/CHANGELOG.md +58 -0
- package/apps/desktop/build/Icon-beta.Assets.car +0 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +34 -1
- package/package.json +2 -2
- package/packages/database/migrations/0067_add_agent_cron_tables.sql +54 -0
- package/packages/database/migrations/meta/0067_snapshot.json +10238 -0
- package/packages/database/migrations/meta/_journal.json +8 -1
- package/packages/database/src/models/__tests__/topics/topic.create.test.ts +4 -0
- package/packages/database/src/models/agentCronJob.ts +286 -0
- package/packages/database/src/schemas/agentCronJob.ts +138 -0
- package/packages/database/src/schemas/index.ts +1 -0
- package/packages/database/src/schemas/topic.ts +2 -0
- package/packages/database/src/utils/idGenerator.ts +1 -0
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Header/Agent/SwitchPanel.tsx +1 -0
- package/src/app/[variants]/(main)/chat/features/Conversation/Header/HeaderActions/index.tsx +1 -1
- package/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx +1 -1
- package/src/features/ChatInput/ActionBar/Tools/PopoverContent.tsx +92 -0
- package/src/features/ChatInput/ActionBar/Tools/ToolItem.tsx +2 -1
- package/src/features/ChatInput/ActionBar/Tools/ToolsList.tsx +107 -0
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +17 -74
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +2 -20
- package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +1 -1
- package/src/features/ChatInput/ActionBar/components/ActionPopover.tsx +1 -0
- package/src/features/ChatInput/ActionBar/components/CheckboxWithLoading.tsx +60 -0
- package/src/features/ModelSwitchPanel/index.tsx +1 -0
- package/src/features/ChatInput/ActionBar/components/CheckbokWithLoading.tsx +0 -53
|
@@ -469,7 +469,14 @@
|
|
|
469
469
|
"when": 1766474494249,
|
|
470
470
|
"tag": "0066_add_document_fields",
|
|
471
471
|
"breakpoints": true
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"idx": 67,
|
|
475
|
+
"version": "7",
|
|
476
|
+
"when": 1767929492232,
|
|
477
|
+
"tag": "0067_add_agent_cron_tables",
|
|
478
|
+
"breakpoints": true
|
|
472
479
|
}
|
|
473
480
|
],
|
|
474
481
|
"version": "6"
|
|
475
|
-
}
|
|
482
|
+
}
|
|
@@ -57,6 +57,8 @@ describe('TopicModel - Create', () => {
|
|
|
57
57
|
agentId: null,
|
|
58
58
|
content: null,
|
|
59
59
|
editorData: null,
|
|
60
|
+
trigger: null,
|
|
61
|
+
mode: null,
|
|
60
62
|
createdAt: expect.any(Date),
|
|
61
63
|
updatedAt: expect.any(Date),
|
|
62
64
|
accessedAt: expect.any(Date),
|
|
@@ -96,6 +98,8 @@ describe('TopicModel - Create', () => {
|
|
|
96
98
|
groupId: null,
|
|
97
99
|
historySummary: null,
|
|
98
100
|
metadata: null,
|
|
101
|
+
trigger: null,
|
|
102
|
+
mode: null,
|
|
99
103
|
sessionId,
|
|
100
104
|
userId,
|
|
101
105
|
createdAt: expect.any(Date),
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { and, desc, eq, gt, isNull, or, sql } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AgentCronJob,
|
|
5
|
+
type CreateAgentCronJobData,
|
|
6
|
+
type NewAgentCronJob,
|
|
7
|
+
type UpdateAgentCronJobData,
|
|
8
|
+
agentCronJobs,
|
|
9
|
+
} from '../schemas/agentCronJob';
|
|
10
|
+
import type { LobeChatDatabase } from '../type';
|
|
11
|
+
|
|
12
|
+
export class AgentCronJobModel {
|
|
13
|
+
private readonly userId: string;
|
|
14
|
+
private readonly db: LobeChatDatabase;
|
|
15
|
+
|
|
16
|
+
constructor(db: LobeChatDatabase, userId?: string) {
|
|
17
|
+
this.db = db;
|
|
18
|
+
this.userId = userId!;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create a new cron job
|
|
22
|
+
async create(data: CreateAgentCronJobData): Promise<AgentCronJob> {
|
|
23
|
+
const cronJob = await this.db
|
|
24
|
+
.insert(agentCronJobs)
|
|
25
|
+
.values({
|
|
26
|
+
...data,
|
|
27
|
+
// Initialize remaining executions to match max executions
|
|
28
|
+
remainingExecutions: data.maxExecutions,
|
|
29
|
+
|
|
30
|
+
userId: this.userId,
|
|
31
|
+
} as NewAgentCronJob)
|
|
32
|
+
.returning();
|
|
33
|
+
|
|
34
|
+
return cronJob[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find cron job by ID (with user ownership check)
|
|
38
|
+
async findById(id: string): Promise<AgentCronJob | null> {
|
|
39
|
+
const result = await this.db
|
|
40
|
+
.select()
|
|
41
|
+
.from(agentCronJobs)
|
|
42
|
+
.where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
|
|
43
|
+
.limit(1);
|
|
44
|
+
|
|
45
|
+
return result[0] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find all cron jobs for a specific agent
|
|
49
|
+
async findByAgentId(agentId: string): Promise<AgentCronJob[]> {
|
|
50
|
+
return this.db
|
|
51
|
+
.select()
|
|
52
|
+
.from(agentCronJobs)
|
|
53
|
+
.where(and(eq(agentCronJobs.agentId, agentId), eq(agentCronJobs.userId, this.userId)))
|
|
54
|
+
.orderBy(desc(agentCronJobs.createdAt));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Find all cron jobs for the user (across all agents)
|
|
58
|
+
async findByUserId(): Promise<AgentCronJob[]> {
|
|
59
|
+
return this.db
|
|
60
|
+
.select()
|
|
61
|
+
.from(agentCronJobs)
|
|
62
|
+
.where(eq(agentCronJobs.userId, this.userId))
|
|
63
|
+
.orderBy(desc(agentCronJobs.lastExecutedAt));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get all enabled cron jobs (system-wide for execution)
|
|
67
|
+
static async getEnabledJobs(db: LobeChatDatabase): Promise<AgentCronJob[]> {
|
|
68
|
+
return db
|
|
69
|
+
.select()
|
|
70
|
+
.from(agentCronJobs)
|
|
71
|
+
.where(
|
|
72
|
+
and(
|
|
73
|
+
eq(agentCronJobs.enabled, true),
|
|
74
|
+
or(gt(agentCronJobs.remainingExecutions, 0), isNull(agentCronJobs.remainingExecutions)),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
.orderBy(agentCronJobs.lastExecutedAt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Update cron job
|
|
81
|
+
async update(id: string, data: UpdateAgentCronJobData): Promise<AgentCronJob | null> {
|
|
82
|
+
const result = await this.db
|
|
83
|
+
.update(agentCronJobs)
|
|
84
|
+
.set({
|
|
85
|
+
...data,
|
|
86
|
+
updatedAt: new Date(),
|
|
87
|
+
})
|
|
88
|
+
.where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
|
|
89
|
+
.returning();
|
|
90
|
+
|
|
91
|
+
return result[0] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Delete cron job
|
|
95
|
+
async delete(id: string): Promise<boolean> {
|
|
96
|
+
const result = await this.db
|
|
97
|
+
.delete(agentCronJobs)
|
|
98
|
+
.where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
|
|
99
|
+
.returning();
|
|
100
|
+
|
|
101
|
+
return result.length > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Update execution statistics after job execution
|
|
105
|
+
static async updateExecutionStats(
|
|
106
|
+
db: LobeChatDatabase,
|
|
107
|
+
jobId: string,
|
|
108
|
+
): Promise<AgentCronJob | null> {
|
|
109
|
+
// Update execution statistics and decrement remaining executions
|
|
110
|
+
const result = await db
|
|
111
|
+
.update(agentCronJobs)
|
|
112
|
+
.set({
|
|
113
|
+
lastExecutedAt: new Date(),
|
|
114
|
+
remainingExecutions: sql`
|
|
115
|
+
CASE
|
|
116
|
+
WHEN ${agentCronJobs.remainingExecutions} IS NULL THEN NULL
|
|
117
|
+
ELSE ${agentCronJobs.remainingExecutions} - 1
|
|
118
|
+
END
|
|
119
|
+
`,
|
|
120
|
+
totalExecutions: sql`${agentCronJobs.totalExecutions} + 1`,
|
|
121
|
+
updatedAt: new Date(),
|
|
122
|
+
})
|
|
123
|
+
.where(eq(agentCronJobs.id, jobId))
|
|
124
|
+
.returning();
|
|
125
|
+
|
|
126
|
+
const updatedJob = result[0];
|
|
127
|
+
|
|
128
|
+
// Auto-disable job if remaining executions reached 0
|
|
129
|
+
if (updatedJob && updatedJob.remainingExecutions === 0) {
|
|
130
|
+
await db
|
|
131
|
+
.update(agentCronJobs)
|
|
132
|
+
.set({
|
|
133
|
+
enabled: false,
|
|
134
|
+
updatedAt: new Date(),
|
|
135
|
+
})
|
|
136
|
+
.where(eq(agentCronJobs.id, jobId));
|
|
137
|
+
|
|
138
|
+
// Return updated job with enabled = false
|
|
139
|
+
return { ...updatedJob, enabled: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return updatedJob || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Reset execution counts and re-enable job
|
|
146
|
+
async resetExecutions(id: string, newMaxExecutions?: number): Promise<AgentCronJob | null> {
|
|
147
|
+
const result = await this.db
|
|
148
|
+
.update(agentCronJobs)
|
|
149
|
+
.set({
|
|
150
|
+
enabled: true,
|
|
151
|
+
// Re-enable job when resetting
|
|
152
|
+
lastExecutedAt: null,
|
|
153
|
+
|
|
154
|
+
maxExecutions: newMaxExecutions,
|
|
155
|
+
|
|
156
|
+
remainingExecutions: newMaxExecutions,
|
|
157
|
+
totalExecutions: 0,
|
|
158
|
+
updatedAt: new Date(),
|
|
159
|
+
})
|
|
160
|
+
.where(and(eq(agentCronJobs.id, id), eq(agentCronJobs.userId, this.userId)))
|
|
161
|
+
.returning();
|
|
162
|
+
|
|
163
|
+
return result[0] || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get jobs that are near depletion (for warnings)
|
|
167
|
+
async getTasksNearDepletion(threshold: number = 5): Promise<AgentCronJob[]> {
|
|
168
|
+
return this.db
|
|
169
|
+
.select()
|
|
170
|
+
.from(agentCronJobs)
|
|
171
|
+
.where(
|
|
172
|
+
and(
|
|
173
|
+
eq(agentCronJobs.userId, this.userId),
|
|
174
|
+
eq(agentCronJobs.enabled, true),
|
|
175
|
+
gt(agentCronJobs.remainingExecutions, 0),
|
|
176
|
+
sql`${agentCronJobs.remainingExecutions} <= ${threshold}`,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
.orderBy(agentCronJobs.remainingExecutions);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Get jobs by execution status
|
|
183
|
+
async findByStatus(enabled: boolean): Promise<AgentCronJob[]> {
|
|
184
|
+
return this.db
|
|
185
|
+
.select()
|
|
186
|
+
.from(agentCronJobs)
|
|
187
|
+
.where(and(eq(agentCronJobs.userId, this.userId), eq(agentCronJobs.enabled, enabled)))
|
|
188
|
+
.orderBy(desc(agentCronJobs.updatedAt));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Get execution statistics for dashboard
|
|
192
|
+
async getExecutionStats(): Promise<{
|
|
193
|
+
activeJobs: number;
|
|
194
|
+
completedExecutions: number;
|
|
195
|
+
pendingExecutions: number;
|
|
196
|
+
totalJobs: number;
|
|
197
|
+
}> {
|
|
198
|
+
const result = await this.db
|
|
199
|
+
.select({
|
|
200
|
+
activeJobs: sql<number>`sum(case when ${agentCronJobs.enabled} then 1 else 0 end)`,
|
|
201
|
+
completedExecutions: sql<number>`sum(${agentCronJobs.totalExecutions})`,
|
|
202
|
+
pendingExecutions: sql<number>`
|
|
203
|
+
sum(
|
|
204
|
+
case when ${agentCronJobs.remainingExecutions} is null then 999999
|
|
205
|
+
else coalesce(${agentCronJobs.remainingExecutions}, 0) end
|
|
206
|
+
)
|
|
207
|
+
`,
|
|
208
|
+
totalJobs: sql<number>`count(*)`,
|
|
209
|
+
})
|
|
210
|
+
.from(agentCronJobs)
|
|
211
|
+
.where(eq(agentCronJobs.userId, this.userId));
|
|
212
|
+
|
|
213
|
+
const stats = result[0];
|
|
214
|
+
return {
|
|
215
|
+
activeJobs: Number(stats.activeJobs),
|
|
216
|
+
completedExecutions: Number(stats.completedExecutions),
|
|
217
|
+
pendingExecutions: Number(stats.pendingExecutions === 999_999 ? 0 : stats.pendingExecutions),
|
|
218
|
+
totalJobs: Number(stats.totalJobs),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Batch enable/disable jobs
|
|
223
|
+
async batchUpdateStatus(ids: string[], enabled: boolean): Promise<number> {
|
|
224
|
+
const result = await this.db
|
|
225
|
+
.update(agentCronJobs)
|
|
226
|
+
.set({
|
|
227
|
+
enabled,
|
|
228
|
+
updatedAt: new Date(),
|
|
229
|
+
})
|
|
230
|
+
.where(and(sql`${agentCronJobs.id} = ANY(${ids})`, eq(agentCronJobs.userId, this.userId)))
|
|
231
|
+
.returning();
|
|
232
|
+
|
|
233
|
+
return result.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Count total jobs for pagination
|
|
237
|
+
async countByAgentId(agentId: string): Promise<number> {
|
|
238
|
+
const result = await this.db
|
|
239
|
+
.select({ count: sql<number>`count(*)` })
|
|
240
|
+
.from(agentCronJobs)
|
|
241
|
+
.where(and(eq(agentCronJobs.agentId, agentId), eq(agentCronJobs.userId, this.userId)));
|
|
242
|
+
|
|
243
|
+
return Number(result[0].count);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Find jobs with pagination
|
|
247
|
+
async findWithPagination(options: {
|
|
248
|
+
agentId?: string;
|
|
249
|
+
enabled?: boolean;
|
|
250
|
+
limit?: number;
|
|
251
|
+
offset?: number;
|
|
252
|
+
}): Promise<{ jobs: AgentCronJob[]; total: number }> {
|
|
253
|
+
const { agentId, enabled, limit = 20, offset = 0 } = options;
|
|
254
|
+
|
|
255
|
+
const whereConditions = [eq(agentCronJobs.userId, this.userId)];
|
|
256
|
+
|
|
257
|
+
if (agentId) {
|
|
258
|
+
whereConditions.push(eq(agentCronJobs.agentId, agentId));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (enabled !== undefined) {
|
|
262
|
+
whereConditions.push(eq(agentCronJobs.enabled, enabled));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const whereClause = and(...whereConditions);
|
|
266
|
+
|
|
267
|
+
// Get total count
|
|
268
|
+
const countResult = await this.db
|
|
269
|
+
.select({ count: sql<number>`count(*)` })
|
|
270
|
+
.from(agentCronJobs)
|
|
271
|
+
.where(whereClause);
|
|
272
|
+
|
|
273
|
+
const total = Number(countResult[0].count);
|
|
274
|
+
|
|
275
|
+
// Get paginated results
|
|
276
|
+
const jobs = await this.db
|
|
277
|
+
.select()
|
|
278
|
+
.from(agentCronJobs)
|
|
279
|
+
.where(whereClause)
|
|
280
|
+
.orderBy(desc(agentCronJobs.createdAt))
|
|
281
|
+
.limit(limit)
|
|
282
|
+
.offset(offset);
|
|
283
|
+
|
|
284
|
+
return { jobs, total };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
2
|
+
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
|
+
|
|
6
|
+
import { idGenerator } from '../utils/idGenerator';
|
|
7
|
+
import { timestamps } from './_helpers';
|
|
8
|
+
import { agents } from './agent';
|
|
9
|
+
import { chatGroups } from './chatGroup';
|
|
10
|
+
import { users } from './user';
|
|
11
|
+
|
|
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
|
+
// Agent cron jobs table - supports multiple cron jobs per agent
|
|
23
|
+
export const agentCronJobs = pgTable(
|
|
24
|
+
'agent_cron_jobs',
|
|
25
|
+
{
|
|
26
|
+
id: text('id')
|
|
27
|
+
.primaryKey()
|
|
28
|
+
.$defaultFn(() => idGenerator('agentCronJobs'))
|
|
29
|
+
.notNull(),
|
|
30
|
+
|
|
31
|
+
// Foreign keys
|
|
32
|
+
agentId: text('agent_id')
|
|
33
|
+
.references(() => agents.id, { onDelete: 'cascade' })
|
|
34
|
+
.notNull(),
|
|
35
|
+
groupId: text('group_id')
|
|
36
|
+
.references(() => chatGroups.id, { onDelete: 'cascade' }),
|
|
37
|
+
userId: text('user_id')
|
|
38
|
+
.references(() => users.id, { onDelete: 'cascade' })
|
|
39
|
+
.notNull(),
|
|
40
|
+
|
|
41
|
+
// Task identification
|
|
42
|
+
name: text('name'), // Optional task name like "Daily Report", "Data Monitoring"
|
|
43
|
+
description: text('description'), // Optional task description
|
|
44
|
+
|
|
45
|
+
// Core configuration
|
|
46
|
+
enabled: boolean('enabled').default(true),
|
|
47
|
+
cronPattern: text('cron_pattern').notNull(), // e.g., "0 */30 * * *"
|
|
48
|
+
timezone: text('timezone').default('UTC'),
|
|
49
|
+
|
|
50
|
+
// Content fields
|
|
51
|
+
content: text('content').notNull(), // Simple text content
|
|
52
|
+
editData: jsonb('edit_data'), // Rich content data (markdown, files, images, etc.)
|
|
53
|
+
|
|
54
|
+
// Execution count management
|
|
55
|
+
maxExecutions: integer('max_executions'), // null = unlimited
|
|
56
|
+
remainingExecutions: integer('remaining_executions'), // null = unlimited
|
|
57
|
+
|
|
58
|
+
// Execution conditions (stored as JSONB)
|
|
59
|
+
executionConditions: jsonb('execution_conditions').$type<ExecutionConditions>(),
|
|
60
|
+
|
|
61
|
+
// Execution statistics
|
|
62
|
+
lastExecutedAt: timestamp('last_executed_at'),
|
|
63
|
+
totalExecutions: integer('total_executions').default(0),
|
|
64
|
+
|
|
65
|
+
...timestamps,
|
|
66
|
+
},
|
|
67
|
+
(t) => [
|
|
68
|
+
// Indexes for performance
|
|
69
|
+
index('agent_cron_jobs_agent_id_idx').on(t.agentId),
|
|
70
|
+
index('agent_cron_jobs_group_id_idx').on(t.groupId),
|
|
71
|
+
index('agent_cron_jobs_user_id_idx').on(t.userId),
|
|
72
|
+
index('agent_cron_jobs_enabled_idx').on(t.enabled),
|
|
73
|
+
index('agent_cron_jobs_remaining_executions_idx').on(t.remainingExecutions),
|
|
74
|
+
index('agent_cron_jobs_last_executed_at_idx').on(t.lastExecutedAt),
|
|
75
|
+
],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Validation schemas
|
|
79
|
+
export const cronPatternSchema = z
|
|
80
|
+
.string()
|
|
81
|
+
.regex(
|
|
82
|
+
/^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})$/,
|
|
83
|
+
'Invalid cron pattern',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Minimum 30 minutes validation
|
|
87
|
+
export const minimumIntervalSchema = z.string().refine((pattern) => {
|
|
88
|
+
// For simplicity, we'll validate common patterns
|
|
89
|
+
// More complex validation can be added later
|
|
90
|
+
const thirtyMinPatterns = [
|
|
91
|
+
'0 */30 * * *', // Every 30 minutes
|
|
92
|
+
'0 0 * * *', // Every hour
|
|
93
|
+
'0 0 */2 * *', // Every 2 hours
|
|
94
|
+
'0 0 */6 * *', // Every 6 hours
|
|
95
|
+
'0 0 0 * *', // Daily
|
|
96
|
+
'0 0 0 * * 1', // Weekly
|
|
97
|
+
'0 0 0 1 *', // Monthly
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Check if it matches allowed patterns or follows 30+ minute intervals
|
|
101
|
+
return (
|
|
102
|
+
thirtyMinPatterns.includes(pattern) ||
|
|
103
|
+
pattern.includes('*/30') ||
|
|
104
|
+
pattern.includes('*/60') ||
|
|
105
|
+
/0 \d+ \* \* \*/.test(pattern)
|
|
106
|
+
); // Hours pattern
|
|
107
|
+
}, 'Minimum execution interval is 30 minutes');
|
|
108
|
+
|
|
109
|
+
export const executionConditionsSchema = z
|
|
110
|
+
.object({
|
|
111
|
+
maxExecutionsPerDay: z.number().min(1).max(100).optional(),
|
|
112
|
+
timeRange: z
|
|
113
|
+
.object({
|
|
114
|
+
end: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
|
|
115
|
+
start: z.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, 'Invalid time format'),
|
|
116
|
+
})
|
|
117
|
+
.optional(),
|
|
118
|
+
weekdays: z.array(z.number().min(0).max(6)).optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional();
|
|
121
|
+
|
|
122
|
+
export const insertAgentCronJobSchema = createInsertSchema(agentCronJobs, {
|
|
123
|
+
cronPattern: minimumIntervalSchema,
|
|
124
|
+
content: z.string().min(1).max(2000),
|
|
125
|
+
editData: z.record(z.any()).optional(), // Allow any JSON structure for rich content
|
|
126
|
+
name: z.string().max(100).optional(),
|
|
127
|
+
description: z.string().max(500).optional(),
|
|
128
|
+
maxExecutions: z.number().min(1).max(10_000).optional(),
|
|
129
|
+
executionConditions: executionConditionsSchema,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const updateAgentCronJobSchema = insertAgentCronJobSchema.partial();
|
|
133
|
+
|
|
134
|
+
// Type exports
|
|
135
|
+
export type NewAgentCronJob = typeof agentCronJobs.$inferInsert;
|
|
136
|
+
export type AgentCronJob = typeof agentCronJobs.$inferSelect;
|
|
137
|
+
export type CreateAgentCronJobData = z.infer<typeof insertAgentCronJobSchema>;
|
|
138
|
+
export type UpdateAgentCronJobData = z.infer<typeof updateAgentCronJobSchema>;
|
|
@@ -31,6 +31,8 @@ export const topics = pgTable(
|
|
|
31
31
|
clientId: text('client_id'),
|
|
32
32
|
historySummary: text('history_summary'),
|
|
33
33
|
metadata: jsonb('metadata').$type<ChatTopicMetadata | undefined>(),
|
|
34
|
+
trigger: text('trigger'), // 'cron' | 'chat' | 'api' - topic creation trigger source
|
|
35
|
+
mode: text('mode'), // 'temp' | 'test' | 'default' - topic usage scenario
|
|
34
36
|
...timestamps,
|
|
35
37
|
},
|
|
36
38
|
(t) => [
|
|
@@ -12,7 +12,7 @@ const HeaderActions = memo(() => {
|
|
|
12
12
|
const { menuItems } = useMenu();
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<DropdownMenu items={menuItems}>
|
|
15
|
+
<DropdownMenu items={menuItems} nativeButton={false}>
|
|
16
16
|
<ActionIcon icon={MoreHorizontal} size={DESKTOP_HEADER_ICON_SIZE} />
|
|
17
17
|
</DropdownMenu>
|
|
18
18
|
);
|
|
@@ -9,7 +9,7 @@ import { useAgentStore } from '@/store/agent';
|
|
|
9
9
|
import { agentByIdSelectors } from '@/store/agent/selectors';
|
|
10
10
|
|
|
11
11
|
import { useAgentId } from '../../hooks/useAgentId';
|
|
12
|
-
import CheckboxItem from '../components/
|
|
12
|
+
import CheckboxItem from '../components/CheckboxWithLoading';
|
|
13
13
|
|
|
14
14
|
export const useControls = ({
|
|
15
15
|
setModalOpen,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Flexbox, Icon, type ItemType, Segmented, usePopoverContext } from '@lobehub/ui';
|
|
2
|
+
import { createStaticStyles, cssVar } from 'antd-style';
|
|
3
|
+
import { ChevronRight, Store } from 'lucide-react';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import ToolsList, { toolsListStyles } from './ToolsList';
|
|
8
|
+
|
|
9
|
+
const styles = createStaticStyles(({ css }) => ({
|
|
10
|
+
footer: css`
|
|
11
|
+
padding: 4px;
|
|
12
|
+
border-block-start: 1px solid ${cssVar.colorBorderSecondary};
|
|
13
|
+
`,
|
|
14
|
+
header: css`
|
|
15
|
+
padding: 8px;
|
|
16
|
+
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
|
17
|
+
`,
|
|
18
|
+
trailingIcon: css`
|
|
19
|
+
opacity: 0.5;
|
|
20
|
+
`,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
type TabType = 'all' | 'installed';
|
|
24
|
+
|
|
25
|
+
interface PopoverContentProps {
|
|
26
|
+
activeTab: TabType;
|
|
27
|
+
currentItems: ItemType[];
|
|
28
|
+
enableKlavis: boolean;
|
|
29
|
+
onOpenStore: () => void;
|
|
30
|
+
onTabChange: (tab: TabType) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PopoverContent = memo<PopoverContentProps>(
|
|
34
|
+
({ activeTab, currentItems, enableKlavis, onTabChange, onOpenStore }) => {
|
|
35
|
+
const { t } = useTranslation('setting');
|
|
36
|
+
|
|
37
|
+
const { close: closePopover } = usePopoverContext();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Flexbox gap={0}>
|
|
41
|
+
<div className={styles.header}>
|
|
42
|
+
<Segmented
|
|
43
|
+
block
|
|
44
|
+
onChange={(v) => onTabChange(v as TabType)}
|
|
45
|
+
options={[
|
|
46
|
+
{
|
|
47
|
+
label: t('tools.tabs.all', { defaultValue: 'all' }),
|
|
48
|
+
value: 'all',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: t('tools.tabs.installed', { defaultValue: 'Installed' }),
|
|
52
|
+
value: 'installed',
|
|
53
|
+
},
|
|
54
|
+
]}
|
|
55
|
+
size="small"
|
|
56
|
+
value={activeTab}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<div
|
|
60
|
+
style={{
|
|
61
|
+
maxHeight: 500,
|
|
62
|
+
minHeight: enableKlavis ? 500 : undefined,
|
|
63
|
+
overflowY: 'auto',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<ToolsList items={currentItems} />
|
|
67
|
+
</div>
|
|
68
|
+
<div className={styles.footer}>
|
|
69
|
+
<div
|
|
70
|
+
className={toolsListStyles.item}
|
|
71
|
+
onClick={() => {
|
|
72
|
+
closePopover();
|
|
73
|
+
onOpenStore();
|
|
74
|
+
}}
|
|
75
|
+
role="button"
|
|
76
|
+
tabIndex={0}
|
|
77
|
+
>
|
|
78
|
+
<div className={toolsListStyles.itemIcon}>
|
|
79
|
+
<Icon icon={Store} size={20} />
|
|
80
|
+
</div>
|
|
81
|
+
<div className={toolsListStyles.itemContent}>{t('tools.plugins.store')}</div>
|
|
82
|
+
<Icon className={styles.trailingIcon} icon={ChevronRight} size={16} />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</Flexbox>
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
PopoverContent.displayName = 'PopoverContent';
|
|
91
|
+
|
|
92
|
+
export default PopoverContent;
|
|
@@ -5,7 +5,7 @@ import PluginTag from '@/components/Plugins/PluginTag';
|
|
|
5
5
|
import { useToolStore } from '@/store/tool';
|
|
6
6
|
import { customPluginSelectors } from '@/store/tool/selectors';
|
|
7
7
|
|
|
8
|
-
import CheckboxItem, { type CheckboxItemProps } from '../components/
|
|
8
|
+
import CheckboxItem, { type CheckboxItemProps } from '../components/CheckboxWithLoading';
|
|
9
9
|
|
|
10
10
|
const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
|
|
11
11
|
const isCustom = useToolStore((s) => customPluginSelectors.isCustomPlugin(id)(s));
|
|
@@ -13,6 +13,7 @@ const ToolItem = memo<CheckboxItemProps>(({ id, onUpdate, label, checked }) => {
|
|
|
13
13
|
return (
|
|
14
14
|
<CheckboxItem
|
|
15
15
|
checked={checked}
|
|
16
|
+
hasPadding={false}
|
|
16
17
|
id={id}
|
|
17
18
|
label={
|
|
18
19
|
<Flexbox align={'center'} gap={8} horizontal>
|