agentstudio 0.1.0

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