agentstudio 0.1.5 → 0.1.7

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.
@@ -1,884 +0,0 @@
1
- import express from 'express';
2
- import { z } from 'zod';
3
- import * as path from 'path';
4
- import * as fs from 'fs';
5
- import * as os from 'os';
6
- import { spawn } from 'child_process';
7
- import { exec } from 'child_process';
8
- import { promisify } from 'util';
9
- import { query, Options } from '@anthropic-ai/claude-code';
10
- import { AgentStorage } from '../../shared/utils/agentStorage.js';
11
- import { AgentConfig } from '../../shared/types/agents.js';
12
- import { ProjectMetadataStorage } from '../../shared/utils/projectMetadataStorage.js';
13
- import { sessionManager } from '../services/sessionManager.js';
14
- import { getAllVersions, getDefaultVersionId } from '../../shared/utils/claudeVersionStorage.js';
15
-
16
- const router: express.Router = express.Router();
17
- const execAsync = promisify(exec);
18
-
19
- // Storage instances
20
- const globalAgentStorage = new AgentStorage();
21
- const projectStorage = new ProjectMetadataStorage();
22
-
23
-
24
-
25
-
26
- // Validation schemas
27
- const CreateAgentSchema = z.object({
28
- id: z.string().min(1).regex(/^[a-z0-9-_]+$/, 'ID must contain only lowercase letters, numbers, hyphens, and underscores'),
29
- name: z.string().min(1),
30
- description: z.string(),
31
- systemPrompt: z.string().min(1),
32
- maxTurns: z.number().min(1).max(100).optional().default(25),
33
- permissionMode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().default('acceptEdits'),
34
- model: z.string().min(1).optional().default('claude-3-5-sonnet-20241022'),
35
- allowedTools: z.array(z.object({
36
- name: z.string(),
37
- enabled: z.boolean(),
38
- permissions: z.object({
39
- requireConfirmation: z.boolean().optional(),
40
- allowedPaths: z.array(z.string()).optional(),
41
- blockedPaths: z.array(z.string()).optional(),
42
- }).optional()
43
- })),
44
- ui: z.object({
45
- icon: z.string().optional().default('🤖'),
46
- primaryColor: z.string().optional().default('#3B82F6'),
47
- headerTitle: z.string(),
48
- headerDescription: z.string(),
49
- welcomeMessage: z.string().optional(),
50
- componentType: z.enum(['slides', 'chat', 'documents', 'code', 'custom']),
51
- customComponent: z.string().optional()
52
- }),
53
- workingDirectory: z.string().optional(),
54
- dataDirectory: z.string().optional(),
55
- fileTypes: z.array(z.string()).optional(),
56
- author: z.string().min(1),
57
- homepage: z.string().url().optional(),
58
- tags: z.array(z.string()).optional().default([]),
59
- enabled: z.boolean().optional().default(true)
60
- });
61
-
62
- const UpdateAgentSchema = CreateAgentSchema.partial().omit({ id: true });
63
-
64
-
65
- // 获取活跃会话列表 (需要在通用获取agents路由之前)
66
- router.get('/sessions', (req, res) => {
67
- try {
68
- const activeCount = sessionManager.getActiveSessionCount();
69
- const sessionsInfo = sessionManager.getSessionsInfo();
70
-
71
- res.json({
72
- activeSessionCount: activeCount,
73
- sessions: sessionsInfo,
74
- message: `${activeCount} active Claude sessions`
75
- });
76
- } catch (error) {
77
- console.error('Failed to get sessions:', error);
78
- res.status(500).json({ error: 'Failed to retrieve session info' });
79
- }
80
- });
81
-
82
- // 手动关闭指定会话
83
- router.delete('/sessions/:sessionId', async (req, res) => {
84
- try {
85
- const { sessionId } = req.params;
86
- const removed = await sessionManager.removeSession(sessionId);
87
-
88
- if (removed) {
89
- res.json({ success: true, message: `Session ${sessionId} closed` });
90
- } else {
91
- res.status(404).json({ error: 'Session not found' });
92
- }
93
- } catch (error) {
94
- console.error('Failed to close session:', error);
95
- res.status(500).json({ error: 'Failed to close session' });
96
- }
97
- });
98
-
99
- // 中断指定会话的当前请求
100
- router.post('/sessions/:sessionId/interrupt', async (req, res) => {
101
- try {
102
- const { sessionId } = req.params;
103
- console.log(`🛑 API: Interrupt request for session: ${sessionId}`);
104
-
105
- const result = await sessionManager.interruptSession(sessionId);
106
-
107
- if (result.success) {
108
- res.json({
109
- success: true,
110
- message: `Session ${sessionId} interrupted successfully`
111
- });
112
- } else {
113
- res.status(result.error === 'Session not found' ? 404 : 500).json({
114
- success: false,
115
- error: result.error || 'Failed to interrupt session'
116
- });
117
- }
118
- } catch (error) {
119
- console.error('Failed to interrupt session:', error);
120
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
121
- res.status(500).json({
122
- success: false,
123
- error: 'Failed to interrupt session',
124
- details: errorMessage
125
- });
126
- }
127
- });
128
-
129
- // Get all agents
130
- router.get('/', (req, res) => {
131
- try {
132
- const { enabled, type } = req.query;
133
- let agents = globalAgentStorage.getAllAgents();
134
-
135
- // Filter by enabled status
136
- if (enabled !== undefined) {
137
- const isEnabled = enabled === 'true';
138
- agents = agents.filter(agent => agent.enabled === isEnabled);
139
- }
140
-
141
- // Filter by component type
142
- if (type && typeof type === 'string') {
143
- agents = agents.filter(agent => agent.ui.componentType === type);
144
- }
145
-
146
- res.json({ agents });
147
- } catch (error) {
148
- console.error('Failed to get agents:', error);
149
- res.status(500).json({ error: 'Failed to retrieve agents' });
150
- }
151
- });
152
-
153
-
154
-
155
-
156
- // Get specific agent
157
- router.get('/:agentId', (req, res) => {
158
- try {
159
- const { agentId } = req.params;
160
- const agent = globalAgentStorage.getAgent(agentId);
161
-
162
- if (!agent) {
163
- return res.status(404).json({ error: 'Agent not found' });
164
- }
165
-
166
- res.json({ agent });
167
- } catch (error) {
168
- console.error('Failed to get agent:', error);
169
- res.status(500).json({ error: 'Failed to retrieve agent' });
170
- }
171
- });
172
-
173
- // Create new agent
174
- router.post('/', (req, res) => {
175
- try {
176
- const validation = CreateAgentSchema.safeParse(req.body);
177
- if (!validation.success) {
178
- return res.status(400).json({ error: 'Invalid agent data', details: validation.error });
179
- }
180
-
181
- const agentData = validation.data;
182
-
183
- // Check if agent ID already exists
184
- const existingAgent = globalAgentStorage.getAgent(agentData.id);
185
- if (existingAgent) {
186
- return res.status(409).json({ error: 'Agent with this ID already exists' });
187
- }
188
-
189
- const agent = globalAgentStorage.createAgent({
190
- version: '1.0.0',
191
- ...agentData
192
- } as Omit<AgentConfig, 'createdAt' | 'updatedAt'>);
193
-
194
- res.json({ agent, message: 'Agent created successfully' });
195
- } catch (error) {
196
- console.error('Failed to create agent:', error);
197
- res.status(500).json({ error: 'Failed to create agent' });
198
- }
199
- });
200
-
201
- // Update agent
202
- router.put('/:agentId', (req, res) => {
203
- try {
204
- const { agentId } = req.params;
205
- const validation = UpdateAgentSchema.safeParse(req.body);
206
-
207
- if (!validation.success) {
208
- return res.status(400).json({ error: 'Invalid agent data', details: validation.error });
209
- }
210
-
211
- const existingAgent = globalAgentStorage.getAgent(agentId);
212
- if (!existingAgent) {
213
- return res.status(404).json({ error: 'Agent not found' });
214
- }
215
-
216
- const updatedAgent: AgentConfig = {
217
- ...existingAgent,
218
- ...validation.data,
219
- id: agentId, // Ensure ID doesn't change
220
- updatedAt: new Date().toISOString()
221
- };
222
-
223
- globalAgentStorage.saveAgent(updatedAgent);
224
- res.json({ agent: updatedAgent, message: 'Agent updated successfully' });
225
- } catch (error) {
226
- console.error('Failed to update agent:', error);
227
- res.status(500).json({ error: 'Failed to update agent' });
228
- }
229
- });
230
-
231
- // Delete agent
232
- router.delete('/:agentId', (req, res) => {
233
- try {
234
- const { agentId } = req.params;
235
- const deleted = globalAgentStorage.deleteAgent(agentId);
236
-
237
- if (!deleted) {
238
- return res.status(404).json({ error: 'Agent not found' });
239
- }
240
-
241
- res.json({ success: true, message: 'Agent deleted successfully' });
242
- } catch (error) {
243
- console.error('Failed to delete agent:', error);
244
- res.status(500).json({ error: 'Failed to delete agent' });
245
- }
246
- });
247
-
248
-
249
- // Validation schemas for chat
250
- const ImageSchema = z.object({
251
- id: z.string(),
252
- data: z.string(), // base64 encoded image data
253
- mediaType: z.enum(['image/jpeg', 'image/png', 'image/gif', 'image/webp']),
254
- filename: z.string().optional()
255
- });
256
-
257
- const ChatRequestSchema = z.object({
258
- message: z.string(),
259
- images: z.array(ImageSchema).optional(),
260
- agentId: z.string().min(1),
261
- sessionId: z.string().optional().nullable(),
262
- projectPath: z.string().optional(),
263
- mcpTools: z.array(z.string()).optional(),
264
- permissionMode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional(),
265
- model: z.string().optional(),
266
- claudeVersion: z.string().optional(), // Claude版本ID
267
- context: z.object({
268
- currentSlide: z.number().optional().nullable(),
269
- slideContent: z.string().optional(),
270
- allSlides: z.array(z.object({
271
- index: z.number(),
272
- title: z.string(),
273
- path: z.string(),
274
- exists: z.boolean().optional()
275
- })).optional(),
276
- // Generic context for other agent types
277
- currentItem: z.any().optional(),
278
- allItems: z.array(z.any()).optional(),
279
- customContext: z.record(z.any()).optional()
280
- }).optional()
281
- }).refine(data => data.message.trim().length > 0 || (data.images && data.images.length > 0), {
282
- message: "Either message text or images must be provided"
283
- });
284
-
285
- // Function to get the path to system claude command
286
- async function getClaudeExecutablePath(): Promise<string | null> {
287
- try {
288
- const { stdout: claudePath } = await execAsync('which claude');
289
- if (!claudePath) return null;
290
-
291
- const cleanPath = claudePath.trim();
292
-
293
- // Skip local node_modules paths - we want global installation
294
- if (cleanPath.includes('node_modules/.bin')) {
295
- // Try to find global installation by checking PATH without local node_modules
296
- try {
297
- const { stdout: allClaudes } = await execAsync('which -a claude');
298
- const claudes = allClaudes.trim().split('\n');
299
-
300
- // Find the first non-local installation
301
- for (const claudePathOption of claudes) {
302
- if (!claudePathOption.includes('node_modules/.bin')) {
303
- return claudePathOption.trim();
304
- }
305
- }
306
- } catch (error) {
307
- // Fallback to the first path found
308
- }
309
- }
310
-
311
- return cleanPath;
312
- } catch (error) {
313
- console.error('Failed to get claude executable path:', error);
314
- return null;
315
- }
316
- }
317
-
318
- // Helper functions for chat endpoint
319
-
320
- /**
321
- * 设置 SSE 连接管理
322
- */
323
- function setupSSEConnectionManagement(req: express.Request, res: express.Response, agentId: string) {
324
- // 连接管理变量
325
- let isConnectionClosed = false;
326
- let connectionTimeout: NodeJS.Timeout | null = null;
327
- let currentRequestId: string | null = null;
328
- let claudeSession: any; // 会话实例,稍后赋值
329
-
330
- // 安全关闭连接的函数
331
- const safeCloseConnection = (reason: string) => {
332
- if (isConnectionClosed) return;
333
-
334
- isConnectionClosed = true;
335
- console.log(`🔚 Closing SSE connection for agent ${agentId}: ${reason}`);
336
-
337
- // 清理超时定时器
338
- if (connectionTimeout) {
339
- clearTimeout(connectionTimeout);
340
- connectionTimeout = null;
341
- }
342
-
343
- // 清理 Claude 请求回调
344
- if (currentRequestId && claudeSession) {
345
- claudeSession.cancelRequest(currentRequestId);
346
- if (reason === 'request completed') {
347
- console.log(`✅ Cleaned up Claude request ${currentRequestId}: ${reason}`);
348
- } else {
349
- console.log(`🚫 Cancelled Claude request ${currentRequestId} due to: ${reason}`);
350
- }
351
- }
352
-
353
- // 确保连接关闭
354
- if (!res.headersSent) {
355
- try {
356
- res.write(`data: ${JSON.stringify({
357
- type: 'connection_closed',
358
- reason: reason,
359
- timestamp: Date.now()
360
- })}\n\n`);
361
- } catch (writeError: unknown) {
362
- console.error('Failed to write connection close event:', writeError);
363
- }
364
- }
365
-
366
- try {
367
- if (!res.destroyed) {
368
- res.end();
369
- }
370
- } catch (endError: unknown) {
371
- console.error('Failed to end response:', endError);
372
- }
373
- };
374
-
375
- // 监听客户端断开连接 - 只在响应阶段监听
376
- res.on('close', () => {
377
- if (!isConnectionClosed) {
378
- safeCloseConnection('client disconnected');
379
- }
380
- });
381
-
382
- // 监听请求完成
383
- req.on('end', () => {
384
- console.log('📤 Request data received completely');
385
- });
386
-
387
- // 监听连接错误
388
- req.on('error', (error) => {
389
- console.error('SSE request error:', error);
390
- safeCloseConnection(`request error: ${error.message}`);
391
- });
392
-
393
- // 监听响应错误
394
- res.on('error', (error) => {
395
- console.error('SSE response error:', error);
396
- safeCloseConnection(`response error: ${error.message}`);
397
- });
398
-
399
- // 设置连接超时保护(30分钟)
400
- const CONNECTION_TIMEOUT_MS = 30 * 60 * 1000;
401
- connectionTimeout = setTimeout(() => {
402
- safeCloseConnection('connection timeout');
403
- }, CONNECTION_TIMEOUT_MS);
404
-
405
- return {
406
- isConnectionClosed: () => isConnectionClosed,
407
- safeCloseConnection,
408
- setCurrentRequestId: (id: string | null) => { currentRequestId = id; },
409
- setClaudeSession: (session: any) => { claudeSession = session; }
410
- };
411
- }
412
-
413
- /**
414
- * 构建查询选项
415
- */
416
- async function buildQueryOptions(agent: any, projectPath: string | undefined, mcpTools: string[] | undefined, permissionMode: string | undefined, model: string | undefined, claudeVersion?: string | undefined): Promise<any> {
417
- // Use Claude Code SDK with agent-specific settings
418
- // If projectPath is provided, use it as cwd; otherwise fall back to agent's workingDirectory
419
- let cwd = process.cwd();
420
- if (projectPath) {
421
- cwd = projectPath;
422
- } else if (agent.workingDirectory) {
423
- cwd = path.resolve(process.cwd(), agent.workingDirectory);
424
- }
425
-
426
- // Determine permission mode: request > agent config > system default
427
- let finalPermissionMode = 'default';
428
- if (permissionMode) {
429
- finalPermissionMode = permissionMode;
430
- } else if (agent.permissionMode) {
431
- finalPermissionMode = agent.permissionMode;
432
- }
433
-
434
- // Determine model: request > agent config > system default (sonnet)
435
- let finalModel = 'sonnet';
436
- if (model) {
437
- finalModel = model
438
- } else if (agent.model) {
439
- finalModel = agent.model;
440
- }
441
-
442
- // Build allowed tools list from agent configuration
443
- const allowedTools = agent.allowedTools
444
- .filter((tool: any) => tool.enabled)
445
- .map((tool: any) => tool.name);
446
-
447
- // Add MCP tools if provided
448
- if (mcpTools && mcpTools.length > 0) {
449
- allowedTools.push(...mcpTools);
450
- }
451
-
452
- // 获取Claude可执行路径 - 支持版本选择
453
- let executablePath: string | null = null;
454
- let environmentVariables: Record<string, string> = {};
455
-
456
- try {
457
- if (claudeVersion) {
458
- // 使用指定版本
459
- const versions = await getAllVersions();
460
- const selectedVersion = versions.find(v => v.id === claudeVersion);
461
- if (selectedVersion) {
462
- if (selectedVersion.executablePath) {
463
- executablePath = selectedVersion.executablePath.trim();
464
- } else {
465
- executablePath = await getClaudeExecutablePath();
466
- }
467
- environmentVariables = selectedVersion.environmentVariables || {};
468
- console.log(`🎯 Using specified Claude version: ${selectedVersion.alias} (${executablePath})`);
469
- } else {
470
- console.warn(`⚠️ Specified Claude version not found: ${claudeVersion}, falling back to default`);
471
- executablePath = await getClaudeExecutablePath();
472
- }
473
- } else {
474
- // 使用默认版本
475
- const defaultVersionId = await getDefaultVersionId();
476
- if (defaultVersionId) {
477
- const versions = await getAllVersions();
478
- const defaultVersion = versions.find(v => v.id === defaultVersionId);
479
- if (defaultVersion) {
480
- if (defaultVersion.executablePath) {
481
- executablePath = defaultVersion.executablePath;
482
- } else {
483
- executablePath = await getClaudeExecutablePath();
484
- }
485
- environmentVariables = defaultVersion.environmentVariables || {};
486
- console.log(`🎯 Using default Claude version: ${defaultVersion.alias} (${executablePath})`);
487
- } else {
488
- executablePath = await getClaudeExecutablePath();
489
- }
490
- } else {
491
- executablePath = await getClaudeExecutablePath();
492
- }
493
- }
494
- } catch (error) {
495
- console.error('Failed to get Claude executable path:', error);
496
- executablePath = await getClaudeExecutablePath();
497
- }
498
-
499
- console.log(`🎯 Using Claude executable path: ${executablePath}`);
500
-
501
- const queryOptions: any = {
502
- appendSystemPrompt: agent.systemPrompt,
503
- allowedTools,
504
- maxTurns: agent.maxTurns,
505
- cwd,
506
- permissionMode: finalPermissionMode as any,
507
- model: finalModel,
508
- };
509
-
510
- // Only add pathToClaudeCodeExecutable if we have a valid path
511
- if (executablePath) {
512
- queryOptions.pathToClaudeCodeExecutable = executablePath;
513
- }
514
-
515
- // Add environment variables if any
516
- if (Object.keys(environmentVariables).length > 0) {
517
- // 合并用户环境变量和当前进程环境变量,避免丢失关键的系统环境变量如PATH
518
- queryOptions.env = { ...process.env, ...environmentVariables };
519
- console.log(`🌍 Using environment variables:`, environmentVariables);
520
- }
521
-
522
- // Add MCP configuration if MCP tools are selected
523
- if (mcpTools && mcpTools.length > 0) {
524
- try {
525
- const mcpConfigContent = readMcpConfig();
526
-
527
- // Extract unique server names from mcpTools
528
- const serverNames = new Set<string>();
529
- for (const tool of mcpTools) {
530
- // Tool format: mcp__serverName__toolName or mcp__serverName
531
- const parts = tool.split('__');
532
- if (parts.length >= 2 && parts[0] === 'mcp') {
533
- serverNames.add(parts[1]);
534
- }
535
- }
536
-
537
- // Build mcpServers configuration
538
- const mcpServers: Record<string, any> = {};
539
- for (const serverName of serverNames) {
540
- const serverConfig = mcpConfigContent.mcpServers?.[serverName];
541
- if (serverConfig && serverConfig.status === 'active') {
542
- if (serverConfig.type === 'http') {
543
- mcpServers[serverName] = {
544
- type: 'http',
545
- url: serverConfig.url
546
- };
547
- } else if (serverConfig.type === 'stdio') {
548
- mcpServers[serverName] = {
549
- type: 'stdio',
550
- command: serverConfig.command,
551
- args: serverConfig.args || [],
552
- env: serverConfig.env || {}
553
- };
554
- }
555
- }
556
- }
557
-
558
- if (Object.keys(mcpServers).length > 0) {
559
- queryOptions.mcpServers = mcpServers;
560
- console.log('🔧 MCP Servers configured:', Object.keys(mcpServers));
561
- }
562
- } catch (error) {
563
- console.error('Failed to parse MCP configuration:', error);
564
- }
565
- }
566
-
567
- return queryOptions;
568
- }
569
-
570
- /**
571
- * 处理会话管理逻辑
572
- */
573
- async function handleSessionManagement(agentId: string, sessionId: string | null, projectPath: string | undefined, queryOptions: any, claudeVersionId?: string) {
574
- let claudeSession: any;
575
- let actualSessionId: string | null = sessionId || null;
576
-
577
- if (sessionId) {
578
- // 尝试复用现有会话
579
- console.log(`🔍 Looking for existing session: ${sessionId} for agent: ${agentId}`);
580
- claudeSession = sessionManager.getSession(sessionId);
581
- if (claudeSession) {
582
- console.log(`♻️ Using existing persistent Claude session: ${sessionId} for agent: ${agentId}`);
583
- } else {
584
- console.log(`❌ Session ${sessionId} not found in memory for agent: ${agentId}`);
585
-
586
- // 检查项目目录中是否存在会话历史
587
- console.log(`🔍 Checking project directory for session history: ${sessionId}, projectPath: ${projectPath}`);
588
- const sessionExists = sessionManager.checkSessionExists(sessionId, projectPath);
589
- console.log(`📁 Session history exists: ${sessionExists} for sessionId: ${sessionId}`);
590
-
591
- if (sessionExists) {
592
- // 会话历史存在,使用 resume 参数恢复会话
593
- console.log(`🔄 Found session history for ${sessionId}, resuming session for agent: ${agentId}`);
594
- claudeSession = sessionManager.createNewSession(agentId, queryOptions, sessionId, claudeVersionId);
595
- } else {
596
- // 会话历史不存在,创建新会话但保持原始 sessionId 用于前端识别
597
- console.log(`⚠️ Session ${sessionId} not found in memory or project history, creating new session for agent: ${agentId}`);
598
- claudeSession = sessionManager.createNewSession(agentId, queryOptions, undefined, claudeVersionId);
599
- }
600
- }
601
- } else {
602
- // 创建新的持续会话
603
- claudeSession = sessionManager.createNewSession(agentId, queryOptions, undefined, claudeVersionId);
604
- console.log(`🆕 Created new persistent Claude session for agent: ${agentId}`);
605
- }
606
-
607
- return { claudeSession, actualSessionId };
608
- }
609
-
610
- /**
611
- * 构建用户消息内容
612
- */
613
- function buildUserMessageContent(message: string, images?: any[]) {
614
- const messageContent: any[] = [];
615
-
616
- // Add text content if provided
617
- if (message && message.trim()) {
618
- messageContent.push({
619
- type: "text",
620
- text: message
621
- });
622
- }
623
-
624
- // Add image content
625
- if (images && images.length > 0) {
626
- console.log('📸 Processing images:', images.map(img => ({
627
- id: img.id,
628
- mediaType: img.mediaType,
629
- filename: img.filename,
630
- size: img.data.length
631
- })));
632
-
633
- for (const image of images) {
634
- messageContent.push({
635
- type: "image",
636
- source: {
637
- type: "base64",
638
- media_type: image.mediaType,
639
- data: image.data
640
- }
641
- });
642
- }
643
- }
644
-
645
- return {
646
- type: "user" as const,
647
- message: {
648
- role: "user" as const,
649
- content: messageContent
650
- }
651
- };
652
- }
653
-
654
- // POST /api/agents/chat - Agent-based AI chat using Claude Code SDK with session management
655
- router.post('/chat', async (req, res) => {
656
- try {
657
- console.log('Chat request received:', req.body);
658
-
659
- // 输出当前Session Manager的状态
660
- console.log('📊 SessionManager状态 - 收到/chat消息时:');
661
- console.log(` 活跃会话总数: ${sessionManager.getActiveSessionCount()}`);
662
- const sessionsInfo = sessionManager.getSessionsInfo();
663
- console.log(' 会话详情:');
664
- sessionsInfo.forEach(session => {
665
- console.log(` - SessionId: ${session.sessionId}`);
666
- console.log(` AgentId: ${session.agentId}`);
667
- console.log(` 状态: ${session.status}`);
668
- console.log(` 是否活跃: ${session.isActive}`);
669
- console.log(` 空闲时间: ${Math.round(session.idleTimeMs / 1000)}秒`);
670
- console.log(` 最后活动: ${new Date(session.lastActivity).toISOString()}`);
671
- });
672
-
673
- // 验证请求数据
674
- const validation = ChatRequestSchema.safeParse(req.body);
675
- if (!validation.success) {
676
- console.log('Validation failed:', validation.error);
677
- return res.status(400).json({ error: 'Invalid request body', details: validation.error });
678
- }
679
-
680
- const { message, images, agentId, sessionId, projectPath, mcpTools, permissionMode, model, claudeVersion } = validation.data;
681
-
682
- // 获取 agent 配置
683
- const agent = globalAgentStorage.getAgent(agentId);
684
- if (!agent) {
685
- return res.status(404).json({ error: 'Agent not found' });
686
- }
687
-
688
- if (!agent.enabled) {
689
- return res.status(403).json({ error: 'Agent is disabled' });
690
- }
691
-
692
- // 设置 SSE 响应头
693
- res.setHeader('Content-Type', 'text/event-stream');
694
- res.setHeader('Cache-Control', 'no-cache');
695
- res.setHeader('Connection', 'keep-alive');
696
- res.setHeader('Access-Control-Allow-Origin', '*');
697
- res.setHeader('Access-Control-Allow-Headers', 'Cache-Control');
698
-
699
- // 设置连接管理
700
- const connectionManager = setupSSEConnectionManagement(req, res, agentId);
701
-
702
- try {
703
- // 构建查询选项
704
- const queryOptions = await buildQueryOptions(agent, projectPath, mcpTools, permissionMode, model, claudeVersion);
705
-
706
- // 处理会话管理
707
- const { claudeSession, actualSessionId: initialSessionId } = await handleSessionManagement(agentId, sessionId || null, projectPath, queryOptions, claudeVersion);
708
- let actualSessionId = initialSessionId;
709
-
710
- // 设置会话到连接管理器
711
- connectionManager.setClaudeSession(claudeSession);
712
-
713
- // 构建用户消息
714
- const userMessage = buildUserMessageContent(message, images);
715
-
716
- // 为这个特定请求创建一个独立的query调用,但复用session context
717
- const currentSessionId = claudeSession.getClaudeSessionId();
718
-
719
- // 构建完整的query options,如果有现有session则使用resume
720
- const requestQueryOptions = { ...queryOptions };
721
- if (currentSessionId) {
722
- requestQueryOptions.resume = currentSessionId;
723
- console.log(`🔄 Using resume sessionId: ${currentSessionId} for this request`);
724
- }
725
-
726
- // 使用会话的 sendMessage 方法发送消息
727
- const currentRequestId = await claudeSession.sendMessage(userMessage, (sdkMessage: any) => {
728
- // 检查连接是否已关闭
729
- if (connectionManager.isConnectionClosed()) {
730
- console.log(`⚠️ Skipping response for closed connection, agent: ${agentId}`);
731
- return;
732
- }
733
-
734
- // 当收到 init 消息时,确认会话 ID
735
- const responseSessionId = sdkMessage.session_id || sdkMessage.sessionId;
736
- if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init' && responseSessionId) {
737
- if (!actualSessionId || !currentSessionId) {
738
- // 新会话:保存session ID
739
- claudeSession.setClaudeSessionId(responseSessionId);
740
- sessionManager.confirmSessionId(claudeSession, responseSessionId);
741
- console.log(`✅ Confirmed session ${responseSessionId} for agent: ${agentId}`);
742
- } else if (currentSessionId && responseSessionId !== currentSessionId) {
743
- // Resume场景:Claude SDK返回了新的session ID,需要通知前端
744
- console.log(`🔄 Session resumed: ${currentSessionId} -> ${responseSessionId} for agent: ${agentId}`);
745
-
746
- // 更新会话管理器中的session ID映射
747
- sessionManager.replaceSessionId(claudeSession, currentSessionId, responseSessionId);
748
- claudeSession.setClaudeSessionId(responseSessionId);
749
-
750
- // 发送session resume通知给前端
751
- const resumeNotification = {
752
- type: 'session_resumed',
753
- subtype: 'new_branch',
754
- originalSessionId: currentSessionId,
755
- newSessionId: responseSessionId,
756
- sessionId: responseSessionId,
757
- message: `会话已从历史记录恢复并创建新分支。原始会话ID: ${currentSessionId},新会话ID: ${responseSessionId}`,
758
- timestamp: Date.now()
759
- };
760
-
761
- try {
762
- if (!res.destroyed && !connectionManager.isConnectionClosed()) {
763
- res.write(`data: ${JSON.stringify(resumeNotification)}\n\n`);
764
- console.log(`🔄 Sent session resume notification: ${currentSessionId} -> ${responseSessionId}`);
765
- }
766
- } catch (writeError: unknown) {
767
- console.error('Failed to write session resume notification:', writeError);
768
- }
769
-
770
- // 更新实际的session ID为新的ID
771
- actualSessionId = responseSessionId;
772
- } else {
773
- // 继续会话:使用现有session ID
774
- console.log(`♻️ Continued session ${currentSessionId} for agent: ${agentId}`);
775
- }
776
- }
777
-
778
- const eventData = {
779
- ...sdkMessage,
780
- agentId: agentId,
781
- sessionId: actualSessionId || responseSessionId || currentSessionId,
782
- timestamp: Date.now()
783
- };
784
-
785
- // 确保返回的 session_id 字段与 sessionId 一致
786
- if (actualSessionId || currentSessionId) {
787
- eventData.session_id = actualSessionId || currentSessionId;
788
- }
789
-
790
- try {
791
- if (!res.destroyed && !connectionManager.isConnectionClosed()) {
792
- res.write(`data: ${JSON.stringify(eventData)}\n\n`);
793
- }
794
- } catch (writeError: unknown) {
795
- console.error('Failed to write SSE data:', writeError);
796
- const errorMessage = writeError instanceof Error ? writeError.message : 'unknown write error';
797
- connectionManager.safeCloseConnection(`write error: ${errorMessage}`);
798
- return;
799
- }
800
-
801
- // 当收到 result 事件时,正常结束 SSE 连接
802
- if (sdkMessage.type === 'result') {
803
- console.log(`✅ Received result event, closing SSE connection for sessionId: ${actualSessionId || currentSessionId}`);
804
- connectionManager.safeCloseConnection('request completed');
805
- }
806
- });
807
-
808
- // 设置当前请求ID到连接管理器
809
- connectionManager.setCurrentRequestId(currentRequestId);
810
-
811
- console.log(`📨 Started Claude request for agent: ${agentId}, sessionId: ${currentSessionId || 'new'}, requestId: ${currentRequestId}`);
812
-
813
- } catch (sessionError) {
814
- console.error('Claude session error:', sessionError);
815
-
816
- const errorMessage = sessionError instanceof Error ? sessionError.message : 'Unknown error';
817
-
818
- if (!connectionManager.isConnectionClosed()) {
819
- try {
820
- res.write(`data: ${JSON.stringify({
821
- type: 'error',
822
- error: 'Claude session failed',
823
- message: errorMessage,
824
- timestamp: Date.now()
825
- })}\n\n`);
826
- } catch (writeError) {
827
- console.error('Failed to write error message:', writeError);
828
- }
829
- connectionManager.safeCloseConnection(`session error: ${errorMessage}`);
830
- }
831
- }
832
-
833
- } catch (error) {
834
- console.error('Error in AI chat:', error);
835
-
836
- // 使用安全关闭连接函数(如果在 try 块内部定义的话)
837
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
838
-
839
- if (!res.headersSent) {
840
- // 如果还没有设置为 SSE,返回 JSON 错误
841
- res.status(500).json({ error: 'AI request failed', message: errorMessage });
842
- } else {
843
- // 如果已经是 SSE 连接,发送错误事件并关闭
844
- try {
845
- if (!res.destroyed) {
846
- res.write(`data: ${JSON.stringify({
847
- type: 'error',
848
- error: 'AI request failed',
849
- message: errorMessage,
850
- timestamp: Date.now()
851
- })}\n\n`);
852
- res.end();
853
- }
854
- } catch (writeError) {
855
- console.error('Failed to write final error message:', writeError);
856
- try {
857
- if (!res.destroyed) {
858
- res.end();
859
- }
860
- } catch (endError) {
861
- console.error('Failed to end response in error handler:', endError);
862
- }
863
- }
864
- }
865
- }
866
- });
867
-
868
-
869
-
870
- // Helper function to read MCP config (needed for chat functionality)
871
- const readMcpConfig = () => {
872
- const mcpConfigPath = path.join(os.homedir(), '.claude-agent', 'mcp-server.json');
873
- if (fs.existsSync(mcpConfigPath)) {
874
- try {
875
- return JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
876
- } catch (error) {
877
- console.error('Failed to parse MCP configuration:', error);
878
- return { mcpServers: {} };
879
- }
880
- }
881
- return { mcpServers: {} };
882
- };
883
-
884
- export default router;