@lobehub/lobehub 2.0.0-next.327 → 2.0.0-next.329
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/.env.example +0 -3
- package/.env.example.development +0 -3
- package/CHANGELOG.md +58 -0
- package/Dockerfile +1 -2
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/advanced/auth.mdx +5 -6
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
- package/docs/self-hosting/environment-variables/auth.mdx +0 -7
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
- package/locales/en-US/chat.json +6 -1
- package/locales/en-US/discover.json +1 -0
- package/locales/zh-CN/chat.json +5 -0
- package/locales/zh-CN/discover.json +1 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
- package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
- package/packages/agent-runtime/src/types/instruction.ts +46 -2
- package/packages/builtin-tool-gtd/src/const.ts +1 -0
- package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
- package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
- package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
- package/packages/builtin-tool-gtd/src/types.ts +55 -33
- package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
- package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
- package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
- package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
- package/packages/database/src/models/message.ts +8 -1
- package/packages/database/src/models/thread.ts +1 -1
- package/packages/types/src/message/ui/chat.ts +2 -0
- package/packages/types/src/topic/thread.ts +20 -0
- package/scripts/prebuild.mts +2 -2
- package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/envs/__tests__/app.test.ts +81 -0
- package/src/envs/app.ts +14 -2
- package/src/envs/auth.test.ts +0 -13
- package/src/envs/auth.ts +0 -41
- package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
- package/src/features/Conversation/Messages/Task/index.tsx +11 -6
- package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
- package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
- package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
- package/src/features/Conversation/components/Thinking/index.tsx +9 -30
- package/src/features/Conversation/store/slices/data/action.ts +2 -3
- package/src/features/NavPanel/components/BackButton.tsx +10 -13
- package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
- package/src/hooks/useAutoScroll.ts +117 -0
- package/src/libs/better-auth/auth-client.ts +0 -9
- package/src/libs/better-auth/define-config.ts +13 -12
- package/src/libs/better-auth/sso/index.ts +2 -1
- package/src/libs/better-auth/utils/config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +4 -6
- package/src/locales/default/chat.ts +6 -1
- package/src/locales/default/discover.ts +2 -0
- package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/routers/lambda/topic.ts +6 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
- package/src/server/services/message/__tests__/index.test.ts +37 -0
- package/src/server/services/message/index.ts +6 -1
- package/src/services/aiAgent.ts +51 -0
- package/src/services/topic/index.ts +4 -0
- package/src/store/chat/agents/createAgentExecutors.ts +714 -12
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
- package/src/store/chat/slices/message/actions/query.ts +33 -1
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
- package/src/store/chat/slices/operation/types.ts +4 -0
- package/src/store/chat/slices/topic/action.test.ts +2 -1
- package/src/store/chat/slices/topic/action.ts +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GenericOAuthConfig } from 'better-auth/plugins';
|
|
2
2
|
import type { SocialProviders } from 'better-auth/social-providers';
|
|
3
3
|
|
|
4
|
+
import { appEnv } from '@/envs/app';
|
|
4
5
|
import { authEnv } from '@/envs/auth';
|
|
5
6
|
import { BUILTIN_BETTER_AUTH_PROVIDERS } from '@/libs/better-auth/constants';
|
|
6
7
|
import { parseSSOProviders } from '@/libs/better-auth/utils/server';
|
|
@@ -106,7 +107,7 @@ export const initBetterAuthSSOProviders = () => {
|
|
|
106
107
|
if (config) {
|
|
107
108
|
// the generic oidc callback url is /api/auth/oauth2/callback/{providerId}
|
|
108
109
|
// different from builtin providers' /api/auth/callback/{providerId}
|
|
109
|
-
config.redirectURI = `${
|
|
110
|
+
config.redirectURI = `${appEnv.APP_URL}/api/auth/callback/${definition.id}`;
|
|
110
111
|
genericOAuthProviders.push(config);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appEnv } from '@/envs/app';
|
|
1
2
|
import { authEnv } from '@/envs/auth';
|
|
2
3
|
import { getRedisConfig } from '@/envs/redis';
|
|
3
4
|
import { initializeRedis, isRedisEnabled } from '@/libs/redis';
|
|
@@ -48,8 +49,7 @@ export const getTrustedOrigins = (enabledSSOProviders: string[]) => {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const defaults = [
|
|
51
|
-
|
|
52
|
-
normalizeOrigin(process.env.APP_URL),
|
|
52
|
+
normalizeOrigin(appEnv.APP_URL),
|
|
53
53
|
normalizeOrigin(process.env.VERCEL_BRANCH_URL),
|
|
54
54
|
normalizeOrigin(process.env.VERCEL_URL),
|
|
55
55
|
MOBILE_APP_SCHEME,
|
|
@@ -237,9 +237,8 @@ export function defineConfig() {
|
|
|
237
237
|
// ref: https://authjs.dev/getting-started/session-management/protecting
|
|
238
238
|
if (isProtected) {
|
|
239
239
|
logNextAuth('Request a protected route, redirecting to sign-in page');
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const nextLoginUrl = new URL('/next-auth/signin', authUrl);
|
|
240
|
+
const callbackUrl = `${appEnv.APP_URL}${req.nextUrl.pathname}${req.nextUrl.search}`;
|
|
241
|
+
const nextLoginUrl = new URL('/next-auth/signin', appEnv.APP_URL);
|
|
243
242
|
nextLoginUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
244
243
|
const hl = req.nextUrl.searchParams.get('hl');
|
|
245
244
|
if (hl) {
|
|
@@ -325,9 +324,8 @@ export function defineConfig() {
|
|
|
325
324
|
// If request a protected route, redirect to sign-in page
|
|
326
325
|
if (isProtected) {
|
|
327
326
|
logBetterAuth('Request a protected route, redirecting to sign-in page');
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const signInUrl = new URL('/signin', authUrl);
|
|
327
|
+
const callbackUrl = `${appEnv.APP_URL}${req.nextUrl.pathname}${req.nextUrl.search}`;
|
|
328
|
+
const signInUrl = new URL('/signin', appEnv.APP_URL);
|
|
331
329
|
signInUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
332
330
|
const hl = req.nextUrl.searchParams.get('hl');
|
|
333
331
|
if (hl) {
|
|
@@ -230,6 +230,7 @@ export default {
|
|
|
230
230
|
'noSelectedAgents': 'No members selected yet',
|
|
231
231
|
'openInNewWindow': 'Open in New Window',
|
|
232
232
|
'operation.execAgentRuntime': 'Preparing response',
|
|
233
|
+
'operation.execClientTask': 'Executing task',
|
|
233
234
|
'operation.sendMessage': 'Sending message',
|
|
234
235
|
'owner': 'Group owner',
|
|
235
236
|
'pageCopilot.title': 'Page Agent',
|
|
@@ -354,13 +355,17 @@ export default {
|
|
|
354
355
|
'tab.profile': 'Agent Profile',
|
|
355
356
|
'tab.search': 'Search',
|
|
356
357
|
'task.activity.calling': 'Calling Skill...',
|
|
358
|
+
'task.activity.clientExecuting': 'Executing locally...',
|
|
357
359
|
'task.activity.generating': 'Generating response...',
|
|
358
360
|
'task.activity.gotResult': 'Tool result received',
|
|
359
361
|
'task.activity.toolCalling': 'Calling {{toolName}}...',
|
|
360
362
|
'task.activity.toolResult': '{{toolName}} result received',
|
|
361
363
|
'task.batchTasks': '{{count}} Batch Subtasks',
|
|
364
|
+
'task.instruction': 'Task Instruction',
|
|
365
|
+
'task.intermediateSteps': '{{count}} intermediate steps',
|
|
366
|
+
'task.metrics.duration': '(took {{duration}})',
|
|
362
367
|
'task.metrics.stepsShort': 'steps',
|
|
363
|
-
'task.metrics.toolCallsShort': '
|
|
368
|
+
'task.metrics.toolCallsShort': 'skill uses',
|
|
364
369
|
'task.status.cancelled': 'Task Cancelled',
|
|
365
370
|
'task.status.failed': 'Task Failed',
|
|
366
371
|
'task.status.initializing': 'Initializing task...',
|
|
@@ -285,6 +285,80 @@ describe('Topic Router Integration Tests', () => {
|
|
|
285
285
|
});
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
describe('batchDeleteByAgentId', () => {
|
|
289
|
+
it('should batch delete topics by agentId (new data)', async () => {
|
|
290
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
291
|
+
|
|
292
|
+
// Create topics with agentId directly (new data structure)
|
|
293
|
+
const topicId1 = await caller.createTopic({
|
|
294
|
+
title: 'Agent Topic 1',
|
|
295
|
+
agentId: testAgentId,
|
|
296
|
+
});
|
|
297
|
+
const topicId2 = await caller.createTopic({
|
|
298
|
+
title: 'Agent Topic 2',
|
|
299
|
+
agentId: testAgentId,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Batch delete by agentId
|
|
303
|
+
await caller.batchDeleteByAgentId({
|
|
304
|
+
agentId: testAgentId,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const remainingTopics = await serverDB.select().from(topics).where(eq(topics.userId, userId));
|
|
308
|
+
|
|
309
|
+
expect(remainingTopics).toHaveLength(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should batch delete topics by agentId (legacy sessionId data)', async () => {
|
|
313
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
314
|
+
|
|
315
|
+
// Create topics with sessionId (legacy data structure)
|
|
316
|
+
await caller.createTopic({
|
|
317
|
+
title: 'Legacy Topic 1',
|
|
318
|
+
sessionId: testSessionId,
|
|
319
|
+
});
|
|
320
|
+
await caller.createTopic({
|
|
321
|
+
title: 'Legacy Topic 2',
|
|
322
|
+
sessionId: testSessionId,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Batch delete by agentId should also delete legacy topics via sessionId mapping
|
|
326
|
+
await caller.batchDeleteByAgentId({
|
|
327
|
+
agentId: testAgentId,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const remainingTopics = await serverDB
|
|
331
|
+
.select()
|
|
332
|
+
.from(topics)
|
|
333
|
+
.where(eq(topics.sessionId, testSessionId));
|
|
334
|
+
|
|
335
|
+
expect(remainingTopics).toHaveLength(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should batch delete topics by agentId (mixed data)', async () => {
|
|
339
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
340
|
+
|
|
341
|
+
// Create both new (agentId) and legacy (sessionId) topics
|
|
342
|
+
await caller.createTopic({
|
|
343
|
+
title: 'New Agent Topic',
|
|
344
|
+
agentId: testAgentId,
|
|
345
|
+
});
|
|
346
|
+
await caller.createTopic({
|
|
347
|
+
title: 'Legacy Session Topic',
|
|
348
|
+
sessionId: testSessionId,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Batch delete by agentId should delete both
|
|
352
|
+
await caller.batchDeleteByAgentId({
|
|
353
|
+
agentId: testAgentId,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const remainingTopics = await serverDB.select().from(topics).where(eq(topics.userId, userId));
|
|
357
|
+
|
|
358
|
+
expect(remainingTopics).toHaveLength(0);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
288
362
|
describe('searchTopics', () => {
|
|
289
363
|
it('should search topics using agentId', async () => {
|
|
290
364
|
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type AgentRuntimeContext } from '@lobechat/agent-runtime';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type TaskCurrentActivity,
|
|
4
|
+
type TaskStatusResult,
|
|
5
|
+
ThreadStatus,
|
|
6
|
+
ThreadType,
|
|
7
|
+
} from '@lobechat/types';
|
|
3
8
|
import { TRPCError } from '@trpc/server';
|
|
4
9
|
import debug from 'debug';
|
|
5
10
|
import pMap from 'p-map';
|
|
@@ -154,6 +159,49 @@ const ExecSubAgentTaskSchema = z.object({
|
|
|
154
159
|
topicId: z.string(),
|
|
155
160
|
});
|
|
156
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Schema for createClientTaskThread - create Thread for client-side task execution
|
|
164
|
+
* This is used when runInClient=true on desktop client
|
|
165
|
+
*/
|
|
166
|
+
const CreateClientTaskThreadSchema = z.object({
|
|
167
|
+
/** The Agent ID to execute the task */
|
|
168
|
+
agentId: z.string(),
|
|
169
|
+
/** The Group ID (optional, only for Group mode) */
|
|
170
|
+
groupId: z.string().optional(),
|
|
171
|
+
/** Initial user message content (task instruction) */
|
|
172
|
+
instruction: z.string(),
|
|
173
|
+
/** The parent message ID (task message) */
|
|
174
|
+
parentMessageId: z.string(),
|
|
175
|
+
/** Task title (shown in UI, used as thread title) */
|
|
176
|
+
title: z.string().optional(),
|
|
177
|
+
/** The Topic ID */
|
|
178
|
+
topicId: z.string(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Schema for updateClientTaskThreadStatus - update Thread status after client-side execution
|
|
183
|
+
*/
|
|
184
|
+
const UpdateClientTaskThreadStatusSchema = z.object({
|
|
185
|
+
/** Completion reason */
|
|
186
|
+
completionReason: z.enum(['done', 'error', 'interrupted']),
|
|
187
|
+
/** Error message if failed */
|
|
188
|
+
error: z.string().optional(),
|
|
189
|
+
/** Thread metadata to update */
|
|
190
|
+
metadata: z
|
|
191
|
+
.object({
|
|
192
|
+
totalCost: z.number().optional(),
|
|
193
|
+
totalMessages: z.number().optional(),
|
|
194
|
+
totalSteps: z.number().optional(),
|
|
195
|
+
totalTokens: z.number().optional(),
|
|
196
|
+
totalToolCalls: z.number().optional(),
|
|
197
|
+
})
|
|
198
|
+
.optional(),
|
|
199
|
+
/** Result content (last assistant message) */
|
|
200
|
+
resultContent: z.string().optional(),
|
|
201
|
+
/** The Thread ID */
|
|
202
|
+
threadId: z.string(),
|
|
203
|
+
});
|
|
204
|
+
|
|
157
205
|
/**
|
|
158
206
|
* Schema for interruptTask - interrupt a running task
|
|
159
207
|
*/
|
|
@@ -184,6 +232,100 @@ const aiAgentProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
184
232
|
});
|
|
185
233
|
|
|
186
234
|
export const aiAgentRouter = router({
|
|
235
|
+
/**
|
|
236
|
+
* Create Thread for client-side task execution
|
|
237
|
+
*
|
|
238
|
+
* This endpoint is called by desktop client when runInClient=true.
|
|
239
|
+
* It creates the Thread but does NOT execute the task - execution happens on client side.
|
|
240
|
+
*/
|
|
241
|
+
createClientTaskThread: aiAgentProcedure
|
|
242
|
+
.input(CreateClientTaskThreadSchema)
|
|
243
|
+
.mutation(async ({ input, ctx }) => {
|
|
244
|
+
const { agentId, groupId, instruction, parentMessageId, title, topicId } = input;
|
|
245
|
+
|
|
246
|
+
log('createClientTaskThread: agentId=%s, groupId=%s', agentId, groupId);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// 1. Create Thread for isolated task execution
|
|
250
|
+
const startedAt = new Date().toISOString();
|
|
251
|
+
const thread = await ctx.threadModel.create({
|
|
252
|
+
agentId,
|
|
253
|
+
groupId,
|
|
254
|
+
metadata: { clientMode: true, startedAt },
|
|
255
|
+
sourceMessageId: parentMessageId,
|
|
256
|
+
status: ThreadStatus.Processing,
|
|
257
|
+
title,
|
|
258
|
+
topicId,
|
|
259
|
+
type: ThreadType.Isolation,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!thread) {
|
|
263
|
+
throw new TRPCError({
|
|
264
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
265
|
+
message: 'Failed to create thread for task execution',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
log('createClientTaskThread: created thread %s', thread.id);
|
|
270
|
+
|
|
271
|
+
// 2. Create initial user message (persisted to database)
|
|
272
|
+
const userMessage = await ctx.messageModel.create({
|
|
273
|
+
agentId,
|
|
274
|
+
content: instruction,
|
|
275
|
+
parentId: parentMessageId,
|
|
276
|
+
role: 'user',
|
|
277
|
+
threadId: thread.id,
|
|
278
|
+
topicId,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
log('createClientTaskThread: created user message %s', userMessage.id);
|
|
282
|
+
|
|
283
|
+
// 3. Query thread messages and main chat messages in parallel
|
|
284
|
+
const [threadMessages, messages] = await Promise.all([
|
|
285
|
+
// Thread messages (messages within this thread)
|
|
286
|
+
ctx.messageModel.query({
|
|
287
|
+
agentId,
|
|
288
|
+
threadId: thread.id,
|
|
289
|
+
topicId,
|
|
290
|
+
}),
|
|
291
|
+
// Main chat messages (messages without threadId, includes updated taskDetail)
|
|
292
|
+
ctx.messageModel.query({
|
|
293
|
+
agentId,
|
|
294
|
+
topicId,
|
|
295
|
+
// No threadId - matchThread will filter for threadId IS NULL (main chat)
|
|
296
|
+
}),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
log(
|
|
300
|
+
'createClientTaskThread: queried %d thread messages, %d main messages',
|
|
301
|
+
threadMessages.length,
|
|
302
|
+
messages.length,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// 4. Return Thread, userMessageId, threadMessages and messages
|
|
306
|
+
return {
|
|
307
|
+
messages,
|
|
308
|
+
startedAt,
|
|
309
|
+
success: true,
|
|
310
|
+
threadId: thread.id,
|
|
311
|
+
threadMessages,
|
|
312
|
+
userMessageId: userMessage.id,
|
|
313
|
+
};
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
log('createClientTaskThread failed: %O', error);
|
|
316
|
+
|
|
317
|
+
if (error instanceof TRPCError) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw new TRPCError({
|
|
322
|
+
cause: error,
|
|
323
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
324
|
+
message: `Failed to create client task thread: ${error.message}`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}),
|
|
328
|
+
|
|
187
329
|
createOperation: aiAgentProcedure
|
|
188
330
|
.input(CreateAgentOperationSchema)
|
|
189
331
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -846,4 +988,100 @@ export const aiAgentRouter = router({
|
|
|
846
988
|
timestamp: new Date().toISOString(),
|
|
847
989
|
};
|
|
848
990
|
}),
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Update Thread status after client-side task execution completes
|
|
994
|
+
*
|
|
995
|
+
* This endpoint is called by desktop client after task execution finishes.
|
|
996
|
+
* It updates the Thread status and metadata similar to server-side completion.
|
|
997
|
+
*/
|
|
998
|
+
updateClientTaskThreadStatus: aiAgentProcedure
|
|
999
|
+
.input(UpdateClientTaskThreadStatusSchema)
|
|
1000
|
+
.mutation(async ({ input, ctx }) => {
|
|
1001
|
+
const { threadId, completionReason, error, resultContent, metadata } = input;
|
|
1002
|
+
|
|
1003
|
+
log('updateClientTaskThreadStatus: threadId=%s, reason=%s', threadId, completionReason);
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
// Find thread
|
|
1007
|
+
const thread = await ctx.threadModel.findById(threadId);
|
|
1008
|
+
if (!thread) {
|
|
1009
|
+
throw new TRPCError({
|
|
1010
|
+
code: 'NOT_FOUND',
|
|
1011
|
+
message: 'Thread not found',
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const completedAt = new Date().toISOString();
|
|
1016
|
+
const startedAt = thread.metadata?.startedAt;
|
|
1017
|
+
const duration = startedAt ? Date.now() - new Date(startedAt).getTime() : undefined;
|
|
1018
|
+
|
|
1019
|
+
// Determine thread status based on completion reason
|
|
1020
|
+
let status: ThreadStatus;
|
|
1021
|
+
switch (completionReason) {
|
|
1022
|
+
case 'done': {
|
|
1023
|
+
status = ThreadStatus.Completed;
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
case 'error': {
|
|
1027
|
+
status = ThreadStatus.Failed;
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case 'interrupted': {
|
|
1031
|
+
status = ThreadStatus.Cancel;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
default: {
|
|
1035
|
+
status = ThreadStatus.Completed;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Update Thread metadata and status
|
|
1040
|
+
await ctx.threadModel.update(threadId, {
|
|
1041
|
+
metadata: {
|
|
1042
|
+
...thread.metadata,
|
|
1043
|
+
completedAt,
|
|
1044
|
+
duration,
|
|
1045
|
+
error: error || undefined,
|
|
1046
|
+
totalCost: metadata?.totalCost,
|
|
1047
|
+
totalMessages: metadata?.totalMessages,
|
|
1048
|
+
totalSteps: metadata?.totalSteps,
|
|
1049
|
+
totalTokens: metadata?.totalTokens,
|
|
1050
|
+
totalToolCalls: metadata?.totalToolCalls,
|
|
1051
|
+
},
|
|
1052
|
+
status,
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// Update task message (sourceMessageId) with result content if provided
|
|
1056
|
+
if (resultContent && thread.sourceMessageId) {
|
|
1057
|
+
await ctx.messageModel.update(thread.sourceMessageId, {
|
|
1058
|
+
content: resultContent,
|
|
1059
|
+
});
|
|
1060
|
+
log(
|
|
1061
|
+
'updateClientTaskThreadStatus: updated task message %s with result',
|
|
1062
|
+
thread.sourceMessageId,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
log('updateClientTaskThreadStatus: thread %s completed with status %s', threadId, status);
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
status,
|
|
1070
|
+
success: true,
|
|
1071
|
+
threadId,
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error: any) {
|
|
1074
|
+
log('updateClientTaskThreadStatus failed: %O', error);
|
|
1075
|
+
|
|
1076
|
+
if (error instanceof TRPCError) {
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
throw new TRPCError({
|
|
1081
|
+
cause: error,
|
|
1082
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1083
|
+
message: `Failed to update client task thread status: ${error.message}`,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}),
|
|
849
1087
|
});
|
|
@@ -21,6 +21,7 @@ const threadProcedure = authedProcedure.use(serverDatabase).use(async (opts) =>
|
|
|
21
21
|
export const threadRouter = router({
|
|
22
22
|
createThread: threadProcedure.input(createThreadSchema).mutation(async ({ input, ctx }) => {
|
|
23
23
|
const thread = await ctx.threadModel.create({
|
|
24
|
+
metadata: input.metadata,
|
|
24
25
|
parentThreadId: input.parentThreadId,
|
|
25
26
|
sourceMessageId: input.sourceMessageId,
|
|
26
27
|
title: input.title,
|
|
@@ -38,6 +39,7 @@ export const threadRouter = router({
|
|
|
38
39
|
)
|
|
39
40
|
.mutation(async ({ input, ctx }) => {
|
|
40
41
|
const thread = await ctx.threadModel.create({
|
|
42
|
+
metadata: input.metadata,
|
|
41
43
|
parentThreadId: input.parentThreadId,
|
|
42
44
|
sourceMessageId: input.sourceMessageId,
|
|
43
45
|
title: input.message.content.slice(0, 20),
|
|
@@ -76,6 +76,12 @@ export const topicRouter = router({
|
|
|
76
76
|
return ctx.topicModel.batchDelete(input.ids);
|
|
77
77
|
}),
|
|
78
78
|
|
|
79
|
+
batchDeleteByAgentId: topicProcedure
|
|
80
|
+
.input(z.object({ agentId: z.string() }))
|
|
81
|
+
.mutation(async ({ input, ctx }) => {
|
|
82
|
+
return ctx.topicModel.batchDeleteByAgentId(input.agentId);
|
|
83
|
+
}),
|
|
84
|
+
|
|
79
85
|
batchDeleteBySessionId: topicProcedure
|
|
80
86
|
.input(
|
|
81
87
|
z.object({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
1
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
5
|
|
|
3
6
|
import { AgentRuntimeService } from './AgentRuntimeService';
|
|
@@ -166,7 +169,7 @@ describe('AgentRuntimeService', () => {
|
|
|
166
169
|
it('should initialize with default base URL', () => {
|
|
167
170
|
delete process.env.AGENT_RUNTIME_BASE_URL;
|
|
168
171
|
const newService = new AgentRuntimeService(mockDb, mockUserId);
|
|
169
|
-
expect((newService as any).baseURL).toBe('http://localhost:
|
|
172
|
+
expect((newService as any).baseURL).toBe('http://localhost:3210/api/agent');
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
it('should initialize with custom base URL from environment', () => {
|
|
@@ -10,6 +10,7 @@ import urlJoin from 'url-join';
|
|
|
10
10
|
|
|
11
11
|
import { MessageModel } from '@/database/models/message';
|
|
12
12
|
import { type LobeChatDatabase } from '@/database/type';
|
|
13
|
+
import { appEnv } from '@/envs/app';
|
|
13
14
|
import {
|
|
14
15
|
AgentRuntimeCoordinator,
|
|
15
16
|
type AgentRuntimeCoordinatorOptions,
|
|
@@ -126,7 +127,7 @@ export class AgentRuntimeService {
|
|
|
126
127
|
private stepCallbacks: Map<string, StepLifecycleCallbacks> = new Map();
|
|
127
128
|
private get baseURL() {
|
|
128
129
|
const baseUrl =
|
|
129
|
-
process.env.AGENT_RUNTIME_BASE_URL ||
|
|
130
|
+
process.env.AGENT_RUNTIME_BASE_URL || appEnv.APP_URL || 'http://localhost:3010';
|
|
130
131
|
|
|
131
132
|
return urlJoin(baseUrl, '/api/agent');
|
|
132
133
|
}
|
|
@@ -293,6 +293,7 @@ describe('MessageService', () => {
|
|
|
293
293
|
current: 0,
|
|
294
294
|
groupId: undefined,
|
|
295
295
|
pageSize: 9999,
|
|
296
|
+
threadId: undefined,
|
|
296
297
|
topicId: undefined,
|
|
297
298
|
},
|
|
298
299
|
expect.objectContaining({
|
|
@@ -327,6 +328,42 @@ describe('MessageService', () => {
|
|
|
327
328
|
current: 0,
|
|
328
329
|
groupId: 'group-1',
|
|
329
330
|
pageSize: 9999,
|
|
331
|
+
threadId: undefined,
|
|
332
|
+
topicId: 'topic-1',
|
|
333
|
+
},
|
|
334
|
+
expect.objectContaining({
|
|
335
|
+
postProcessUrl: expect.any(Function),
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
expect(result.id).toBe('msg-1');
|
|
339
|
+
expect(result.messages).toEqual(mockMessages);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should create message with threadId and query thread messages', async () => {
|
|
343
|
+
const params = {
|
|
344
|
+
agentId: 'agent-1',
|
|
345
|
+
content: 'Hello in thread',
|
|
346
|
+
groupId: 'group-1',
|
|
347
|
+
role: 'user' as const,
|
|
348
|
+
threadId: 'thread-1',
|
|
349
|
+
topicId: 'topic-1',
|
|
350
|
+
};
|
|
351
|
+
const createdMessage = { id: 'msg-1', ...params };
|
|
352
|
+
const mockMessages = [createdMessage];
|
|
353
|
+
|
|
354
|
+
vi.mocked(mockMessageModel.create).mockResolvedValue(createdMessage as any);
|
|
355
|
+
vi.mocked(mockMessageModel.query).mockResolvedValue(mockMessages as any);
|
|
356
|
+
|
|
357
|
+
const result = await messageService.createMessage(params as any);
|
|
358
|
+
|
|
359
|
+
expect(mockMessageModel.create).toHaveBeenCalledWith(params);
|
|
360
|
+
expect(mockMessageModel.query).toHaveBeenCalledWith(
|
|
361
|
+
{
|
|
362
|
+
agentId: 'agent-1',
|
|
363
|
+
current: 0,
|
|
364
|
+
groupId: 'group-1',
|
|
365
|
+
pageSize: 9999,
|
|
366
|
+
threadId: 'thread-1',
|
|
330
367
|
topicId: 'topic-1',
|
|
331
368
|
},
|
|
332
369
|
expect.objectContaining({
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type LobeChatDatabase } from '@lobechat/database';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CreateMessageParams,
|
|
4
|
+
type UIChatMessage,
|
|
5
|
+
type UpdateMessageParams,
|
|
6
|
+
} from '@lobechat/types';
|
|
3
7
|
|
|
4
8
|
import { MessageModel } from '@/database/models/message';
|
|
5
9
|
|
|
@@ -95,6 +99,7 @@ export class MessageService {
|
|
|
95
99
|
current: 0,
|
|
96
100
|
groupId: params.groupId,
|
|
97
101
|
pageSize: 9999,
|
|
102
|
+
threadId: params.threadId,
|
|
98
103
|
topicId: params.topicId,
|
|
99
104
|
},
|
|
100
105
|
{
|
package/src/services/aiAgent.ts
CHANGED
|
@@ -40,6 +40,38 @@ export interface InterruptTaskParams {
|
|
|
40
40
|
threadId?: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Parameters for createClientTaskThread
|
|
45
|
+
* Creates a Thread for client-side task execution (desktop only)
|
|
46
|
+
*/
|
|
47
|
+
export interface CreateClientTaskThreadParams {
|
|
48
|
+
agentId: string;
|
|
49
|
+
groupId?: string;
|
|
50
|
+
/** Initial user message content (task instruction) */
|
|
51
|
+
instruction: string;
|
|
52
|
+
parentMessageId: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
topicId: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parameters for updateClientTaskThreadStatus
|
|
59
|
+
* Updates Thread status after client-side execution completes
|
|
60
|
+
*/
|
|
61
|
+
export interface UpdateClientTaskThreadStatusParams {
|
|
62
|
+
completionReason: 'done' | 'error' | 'interrupted';
|
|
63
|
+
error?: string;
|
|
64
|
+
metadata?: {
|
|
65
|
+
totalCost?: number;
|
|
66
|
+
totalMessages?: number;
|
|
67
|
+
totalSteps?: number;
|
|
68
|
+
totalTokens?: number;
|
|
69
|
+
totalToolCalls?: number;
|
|
70
|
+
};
|
|
71
|
+
resultContent?: string;
|
|
72
|
+
threadId: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
43
75
|
class AiAgentService {
|
|
44
76
|
/**
|
|
45
77
|
* Execute a single Agent task
|
|
@@ -72,6 +104,25 @@ class AiAgentService {
|
|
|
72
104
|
async interruptTask(params: InterruptTaskParams) {
|
|
73
105
|
return await lambdaClient.aiAgent.interruptTask.mutate(params);
|
|
74
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create Thread for client-side task execution (desktop only)
|
|
110
|
+
*
|
|
111
|
+
* This method is called when runInClient=true on desktop client.
|
|
112
|
+
* It creates the Thread but does NOT execute the task - execution happens locally.
|
|
113
|
+
*/
|
|
114
|
+
async createClientTaskThread(params: CreateClientTaskThreadParams) {
|
|
115
|
+
return await lambdaClient.aiAgent.createClientTaskThread.mutate(params);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Update Thread status after client-side task execution completes
|
|
120
|
+
*
|
|
121
|
+
* This method is called by desktop client after task execution finishes.
|
|
122
|
+
*/
|
|
123
|
+
async updateClientTaskThreadStatus(params: UpdateClientTaskThreadStatusParams) {
|
|
124
|
+
return await lambdaClient.aiAgent.updateClientTaskThreadStatus.mutate(params);
|
|
125
|
+
}
|
|
75
126
|
}
|
|
76
127
|
|
|
77
128
|
export const aiAgentService = new AiAgentService();
|
|
@@ -109,6 +109,10 @@ export class TopicService {
|
|
|
109
109
|
return lambdaClient.topic.batchDeleteBySessionId.mutate({ id: this.toDbSessionId(sessionId) });
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
removeTopicsByAgentId = (agentId: string) => {
|
|
113
|
+
return lambdaClient.topic.batchDeleteByAgentId.mutate({ agentId });
|
|
114
|
+
};
|
|
115
|
+
|
|
112
116
|
batchRemoveTopics = (topics: string[]) => {
|
|
113
117
|
return lambdaClient.topic.batchDelete.mutate({ ids: topics });
|
|
114
118
|
};
|