evolclaw 2.0.0 → 2.0.2

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.
@@ -23,6 +23,56 @@ export class SessionManager {
23
23
  const encodedPath = this.getProjectDirName(projectPath);
24
24
  return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
25
25
  }
26
+ rowToSession(row) {
27
+ return {
28
+ id: row.id,
29
+ channel: row.channel,
30
+ channelId: row.channel_id,
31
+ projectPath: row.project_path,
32
+ claudeSessionId: row.claude_session_id,
33
+ name: row.name,
34
+ isActive: row.is_active === 1,
35
+ createdAt: row.created_at,
36
+ updatedAt: row.updated_at
37
+ };
38
+ }
39
+ deactivateAll(channel, channelId) {
40
+ this.db.prepare(`
41
+ UPDATE sessions SET is_active = 0, updated_at = ?
42
+ WHERE channel = ? AND channel_id = ? AND is_active = 1
43
+ `).run(Date.now(), channel, channelId);
44
+ }
45
+ validateSessionFile(row) {
46
+ const claudeSessionId = row.claude_session_id;
47
+ if (!claudeSessionId)
48
+ return undefined;
49
+ const sessionFile = this.getSessionFilePath(row.project_path, claudeSessionId);
50
+ if (fs.existsSync(sessionFile))
51
+ return claudeSessionId;
52
+ logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
53
+ this.db.prepare(`UPDATE sessions SET claude_session_id = NULL WHERE id = ?`).run(row.id);
54
+ return undefined;
55
+ }
56
+ insertSession(session) {
57
+ this.db.prepare(`
58
+ INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
60
+ `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, session.isActive ? 1 : 0, session.createdAt, session.updatedAt);
61
+ }
62
+ extractUserMessageText(messageContent) {
63
+ if (typeof messageContent === 'string') {
64
+ const text = messageContent.trim();
65
+ return text.substring(0, 50) + (text.length > 50 ? '...' : '');
66
+ }
67
+ else if (Array.isArray(messageContent)) {
68
+ const textContent = messageContent.find((c) => c.type === 'text');
69
+ if (textContent?.text) {
70
+ const text = textContent.text.trim();
71
+ return text.substring(0, 50) + (text.length > 50 ? '...' : '');
72
+ }
73
+ }
74
+ return null;
75
+ }
26
76
  initDatabase() {
27
77
  const tableInfo = this.db.prepare('PRAGMA table_info(sessions)').all();
28
78
  const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
@@ -144,26 +194,8 @@ export class SessionManager {
144
194
  WHERE channel = ? AND channel_id = ? AND is_active = 1
145
195
  `).get(channel, channelId);
146
196
  if (active) {
147
- // 验证会话文件是否存在
148
- let validSessionId = active.claude_session_id;
149
- if (validSessionId) {
150
- const sessionFile = this.getSessionFilePath(active.project_path, validSessionId);
151
- if (!fs.existsSync(sessionFile)) {
152
- logger.warn(`Session file not found: ${sessionFile}`);
153
- validSessionId = null;
154
- }
155
- }
156
- return {
157
- id: active.id,
158
- channel: active.channel,
159
- channelId: active.channel_id,
160
- projectPath: active.project_path,
161
- claudeSessionId: validSessionId,
162
- name: active.name,
163
- isActive: active.is_active === 1,
164
- createdAt: active.created_at,
165
- updatedAt: active.updated_at
166
- };
197
+ const validSessionId = this.validateSessionFile(active);
198
+ return { ...this.rowToSession(active), claudeSessionId: validSessionId };
167
199
  }
168
200
  // 2. 没有活跃会话,查找该聊天在默认项目的会话
169
201
  const existing = this.db.prepare(`
@@ -172,32 +204,13 @@ export class SessionManager {
172
204
  ORDER BY updated_at DESC LIMIT 1
173
205
  `).get(channel, channelId, defaultProjectPath);
174
206
  if (existing) {
175
- // 验证会话文件是否存在
176
- let validSessionId = existing.claude_session_id;
177
- if (validSessionId) {
178
- const sessionFile = this.getSessionFilePath(existing.project_path, validSessionId);
179
- if (!fs.existsSync(sessionFile)) {
180
- logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
181
- validSessionId = null;
182
- this.db.prepare(`UPDATE sessions SET claude_session_id = NULL WHERE id = ?`).run(existing.id);
183
- }
184
- }
207
+ const validSessionId = this.validateSessionFile(existing);
185
208
  // 激活该会话
186
209
  this.db.prepare(`
187
210
  UPDATE sessions SET is_active = 1, updated_at = ?
188
211
  WHERE id = ?
189
212
  `).run(Date.now(), existing.id);
190
- return {
191
- id: existing.id,
192
- channel: existing.channel,
193
- channelId: existing.channel_id,
194
- projectPath: existing.project_path,
195
- claudeSessionId: validSessionId,
196
- name: existing.name,
197
- isActive: true,
198
- createdAt: existing.created_at,
199
- updatedAt: existing.updated_at
200
- };
213
+ return { ...this.rowToSession(existing), claudeSessionId: validSessionId, isActive: true };
201
214
  }
202
215
  // 3. 创建新会话(默认为活跃)
203
216
  const session = {
@@ -217,23 +230,13 @@ export class SessionManager {
217
230
  `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt);
218
231
  // 如果插入被忽略(已存在),重新查询
219
232
  if (result.changes === 0) {
220
- const existing = this.db.prepare(`
233
+ const recheck = this.db.prepare(`
221
234
  SELECT * FROM sessions
222
235
  WHERE channel = ? AND channel_id = ? AND project_path = ?
223
236
  `).get(channel, channelId, defaultProjectPath);
224
- if (existing) {
225
- this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), existing.id);
226
- return {
227
- id: existing.id,
228
- channel: existing.channel,
229
- channelId: existing.channel_id,
230
- projectPath: existing.project_path,
231
- claudeSessionId: existing.claude_session_id,
232
- name: existing.name,
233
- isActive: true,
234
- createdAt: existing.created_at,
235
- updatedAt: Date.now()
236
- };
237
+ if (recheck) {
238
+ this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
239
+ return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
237
240
  }
238
241
  }
239
242
  return session;
@@ -252,10 +255,7 @@ export class SessionManager {
252
255
  }
253
256
  async switchProject(channel, channelId, newProjectPath) {
254
257
  // 1. 取消当前活跃会话
255
- this.db.prepare(`
256
- UPDATE sessions SET is_active = 0, updated_at = ?
257
- WHERE channel = ? AND channel_id = ? AND is_active = 1
258
- `).run(Date.now(), channel, channelId);
258
+ this.deactivateAll(channel, channelId);
259
259
  // 2. 查找目标项目的会话
260
260
  const target = this.db.prepare(`
261
261
  SELECT * FROM sessions
@@ -263,32 +263,13 @@ export class SessionManager {
263
263
  ORDER BY updated_at DESC LIMIT 1
264
264
  `).get(channel, channelId, newProjectPath);
265
265
  if (target) {
266
- // 验证会话文件是否存在
267
- let validSessionId = target.claude_session_id;
268
- if (validSessionId) {
269
- const sessionFile = this.getSessionFilePath(newProjectPath, validSessionId);
270
- if (!fs.existsSync(sessionFile)) {
271
- logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
272
- validSessionId = null;
273
- this.db.prepare(`UPDATE sessions SET claude_session_id = NULL WHERE id = ?`).run(target.id);
274
- }
275
- }
266
+ const validSessionId = this.validateSessionFile(target);
276
267
  // 激活已有会话
277
268
  this.db.prepare(`
278
269
  UPDATE sessions SET is_active = 1, updated_at = ?
279
270
  WHERE id = ?
280
271
  `).run(Date.now(), target.id);
281
- return {
282
- id: target.id,
283
- channel: target.channel,
284
- channelId: target.channel_id,
285
- projectPath: target.project_path,
286
- claudeSessionId: validSessionId,
287
- name: target.name,
288
- isActive: true,
289
- createdAt: target.created_at,
290
- updatedAt: target.updated_at
291
- };
272
+ return { ...this.rowToSession(target), claudeSessionId: validSessionId, isActive: true };
292
273
  }
293
274
  // 3. 创建新会话
294
275
  const session = {
@@ -301,16 +282,9 @@ export class SessionManager {
301
282
  createdAt: Date.now(),
302
283
  updatedAt: Date.now()
303
284
  };
304
- this.db.prepare(`
305
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
306
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
307
- `).run(session.id, session.channel, session.channelId, session.projectPath, null, session.name ?? null, 1, session.createdAt, session.updatedAt);
285
+ this.insertSession(session);
308
286
  return session;
309
287
  }
310
- async updateProjectPath(channel, channelId, projectPath) {
311
- this.db.prepare('UPDATE sessions SET project_path = ?, updated_at = ? WHERE channel = ? AND channel_id = ?')
312
- .run(projectPath, Date.now(), channel, channelId);
313
- }
314
288
  async updateClaudeSessionId(channel, channelId, claudeSessionId) {
315
289
  // 只更新当前活跃会话的 Claude Session ID
316
290
  this.db.prepare(`
@@ -336,35 +310,14 @@ export class SessionManager {
336
310
  WHERE channel = ? AND channel_id = ? AND is_active = 1
337
311
  `).run(Date.now(), channel, channelId);
338
312
  }
339
- async clearClaudeSessionId(channel, channelId) {
340
- // 向后兼容的别名
341
- await this.clearActiveSession(channel, channelId);
342
- }
343
- async getSession(channel, channelId) {
344
- // 获取活跃会话
313
+ async getActiveSession(channel, channelId) {
345
314
  const row = this.db.prepare(`
346
315
  SELECT * FROM sessions
347
316
  WHERE channel = ? AND channel_id = ? AND is_active = 1
348
317
  `).get(channel, channelId);
349
318
  if (!row)
350
319
  return undefined;
351
- return {
352
- id: row.id,
353
- channel: row.channel,
354
- channelId: row.channel_id,
355
- projectPath: row.project_path,
356
- claudeSessionId: row.claude_session_id,
357
- name: row.name,
358
- isActive: row.is_active === 1,
359
- createdAt: row.created_at,
360
- updatedAt: row.updated_at
361
- };
362
- }
363
- /**
364
- * 获取活跃会话(getSession 的别名,语义更清晰)
365
- */
366
- async getActiveSession(channel, channelId) {
367
- return this.getSession(channel, channelId);
320
+ return this.rowToSession(row);
368
321
  }
369
322
  async listSessions(channel, channelId) {
370
323
  // 列出该聊天的所有会话
@@ -373,17 +326,7 @@ export class SessionManager {
373
326
  WHERE channel = ? AND channel_id = ?
374
327
  ORDER BY updated_at DESC
375
328
  `).all(channel, channelId);
376
- return rows.map(row => ({
377
- id: row.id,
378
- channel: row.channel,
379
- channelId: row.channel_id,
380
- projectPath: row.project_path,
381
- claudeSessionId: row.claude_session_id,
382
- name: row.name,
383
- isActive: row.is_active === 1,
384
- createdAt: row.created_at,
385
- updatedAt: row.updated_at
386
- }));
329
+ return rows.map(row => this.rowToSession(row));
387
330
  }
388
331
  async getSessionByProjectPath(channel, channelId, projectPath) {
389
332
  const row = this.db.prepare(`
@@ -392,17 +335,7 @@ export class SessionManager {
392
335
  `).get(channel, channelId, projectPath);
393
336
  if (!row)
394
337
  return undefined;
395
- return {
396
- id: row.id,
397
- channel: row.channel,
398
- channelId: row.channel_id,
399
- projectPath: row.project_path,
400
- claudeSessionId: row.claude_session_id,
401
- name: row.name,
402
- isActive: row.is_active === 1,
403
- createdAt: row.created_at,
404
- updatedAt: row.updated_at
405
- };
338
+ return this.rowToSession(row);
406
339
  }
407
340
  async getSessionByName(channel, channelId, name) {
408
341
  const row = this.db.prepare(`
@@ -411,17 +344,7 @@ export class SessionManager {
411
344
  `).get(channel, channelId, name);
412
345
  if (!row)
413
346
  return undefined;
414
- return {
415
- id: row.id,
416
- channel: row.channel,
417
- channelId: row.channel_id,
418
- projectPath: row.project_path,
419
- claudeSessionId: row.claude_session_id,
420
- name: row.name,
421
- isActive: row.is_active === 1,
422
- createdAt: row.created_at,
423
- updatedAt: row.updated_at
424
- };
347
+ return this.rowToSession(row);
425
348
  }
426
349
  async switchToSession(channel, channelId, targetSessionId) {
427
350
  // 验证目标会话存在
@@ -431,26 +354,13 @@ export class SessionManager {
431
354
  if (!target)
432
355
  return null;
433
356
  // 取消当前活跃会话
434
- this.db.prepare(`
435
- UPDATE sessions SET is_active = 0, updated_at = ?
436
- WHERE channel = ? AND channel_id = ? AND is_active = 1
437
- `).run(Date.now(), channel, channelId);
357
+ this.deactivateAll(channel, channelId);
438
358
  // 激活目标会话
439
359
  this.db.prepare(`
440
360
  UPDATE sessions SET is_active = 1, updated_at = ?
441
361
  WHERE id = ?
442
362
  `).run(Date.now(), targetSessionId);
443
- return {
444
- id: target.id,
445
- channel: target.channel,
446
- channelId: target.channel_id,
447
- projectPath: target.project_path,
448
- claudeSessionId: target.claude_session_id,
449
- name: target.name,
450
- isActive: true,
451
- createdAt: target.created_at,
452
- updatedAt: Date.now()
453
- };
363
+ return { ...this.rowToSession(target), isActive: true, updatedAt: Date.now() };
454
364
  }
455
365
  async renameSession(sessionId, newName) {
456
366
  const result = this.db.prepare(`
@@ -460,10 +370,7 @@ export class SessionManager {
460
370
  }
461
371
  async createNewSession(channel, channelId, projectPath, name) {
462
372
  // 取消当前活跃会话
463
- this.db.prepare(`
464
- UPDATE sessions SET is_active = 0, updated_at = ?
465
- WHERE channel = ? AND channel_id = ? AND is_active = 1
466
- `).run(Date.now(), channel, channelId);
373
+ this.deactivateAll(channel, channelId);
467
374
  // 创建新会话
468
375
  const session = {
469
376
  id: `${channel}-${channelId}-${Date.now()}`,
@@ -475,10 +382,7 @@ export class SessionManager {
475
382
  createdAt: Date.now(),
476
383
  updatedAt: Date.now()
477
384
  };
478
- this.db.prepare(`
479
- INSERT INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
480
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
481
- `).run(session.id, session.channel, session.channelId, session.projectPath, null, session.name ?? null, 1, session.createdAt, session.updatedAt);
385
+ this.insertSession(session);
482
386
  return session;
483
387
  }
484
388
  /**
@@ -486,10 +390,7 @@ export class SessionManager {
486
390
  */
487
391
  async createForkedSession(sourceSession, forkedClaudeSessionId, name) {
488
392
  // 取消当前活跃会话
489
- this.db.prepare(`
490
- UPDATE sessions SET is_active = 0, updated_at = ?
491
- WHERE channel = ? AND channel_id = ? AND is_active = 1
492
- `).run(Date.now(), sourceSession.channel, sourceSession.channelId);
393
+ this.deactivateAll(sourceSession.channel, sourceSession.channelId);
493
394
  const session = {
494
395
  id: `${sourceSession.channel}-${sourceSession.channelId}-${Date.now()}`,
495
396
  channel: sourceSession.channel,
@@ -501,10 +402,7 @@ export class SessionManager {
501
402
  createdAt: Date.now(),
502
403
  updatedAt: Date.now()
503
404
  };
504
- this.db.prepare(`
505
- INSERT INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
506
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
507
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt);
405
+ this.insertSession(session);
508
406
  return session;
509
407
  }
510
408
  async scanCliSessions(projectPath) {
@@ -539,21 +437,10 @@ export class SessionManager {
539
437
  const lines = content.split('\n').filter(l => l.trim());
540
438
  for (const line of lines) {
541
439
  const event = JSON.parse(line);
542
- // 格式: {type: "user", message: {role: "user", content: ...}}
543
440
  if (event.type === 'user' && event.message?.role === 'user') {
544
- const messageContent = event.message.content;
545
- // content 可能是字符串或数组
546
- if (typeof messageContent === 'string') {
547
- const text = messageContent.trim();
548
- return text.substring(0, 50) + (text.length > 50 ? '...' : '');
549
- }
550
- else if (Array.isArray(messageContent)) {
551
- const textContent = messageContent.find((c) => c.type === 'text');
552
- if (textContent?.text) {
553
- const text = textContent.text.trim();
554
- return text.substring(0, 50) + (text.length > 50 ? '...' : '');
555
- }
556
- }
441
+ const text = this.extractUserMessageText(event.message.content);
442
+ if (text)
443
+ return text;
557
444
  }
558
445
  }
559
446
  }
@@ -573,18 +460,7 @@ export class SessionManager {
573
460
  for (const line of lines) {
574
461
  const event = JSON.parse(line);
575
462
  if (event.type === 'user' && event.message?.role === 'user') {
576
- const messageContent = event.message.content;
577
- if (typeof messageContent === 'string') {
578
- const text = messageContent.trim();
579
- lastMessage = text.substring(0, 50) + (text.length > 50 ? '...' : '');
580
- }
581
- else if (Array.isArray(messageContent)) {
582
- const textContent = messageContent.find((c) => c.type === 'text');
583
- if (textContent?.text) {
584
- const text = textContent.text.trim();
585
- lastMessage = text.substring(0, 50) + (text.length > 50 ? '...' : '');
586
- }
587
- }
463
+ lastMessage = this.extractUserMessageText(event.message.content) ?? lastMessage;
588
464
  }
589
465
  }
590
466
  return lastMessage;
@@ -626,12 +502,6 @@ export class SessionManager {
626
502
  return { turns: 0 };
627
503
  }
628
504
  }
629
- /**
630
- * 统计会话回合数(用户消息数)— 兼容旧调用
631
- */
632
- countSessionTurns(projectPath, claudeSessionId) {
633
- return this.getSessionFileInfo(projectPath, claudeSessionId).turns;
634
- }
635
505
  async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
636
506
  const rows = this.db.prepare(`
637
507
  SELECT * FROM sessions
@@ -642,18 +512,7 @@ export class SessionManager {
642
512
  if (rows.length > 1) {
643
513
  logger.warn(`Multiple sessions found with UUID prefix: ${uuidPrefix}`);
644
514
  }
645
- const row = rows[0];
646
- return {
647
- id: row.id,
648
- channel: row.channel,
649
- channelId: row.channel_id,
650
- projectPath: row.project_path,
651
- claudeSessionId: row.claude_session_id,
652
- name: row.name,
653
- isActive: row.is_active === 1,
654
- createdAt: row.created_at,
655
- updatedAt: row.updated_at
656
- };
515
+ return this.rowToSession(rows[0]);
657
516
  }
658
517
  async importCliSession(channel, channelId, projectPath, claudeSessionId) {
659
518
  // 检查是否已存在相同项目路径的会话
@@ -671,23 +530,10 @@ export class SessionManager {
671
530
  UPDATE sessions SET claude_session_id = ?, is_active = 1, updated_at = ?
672
531
  WHERE id = ?
673
532
  `).run(claudeSessionId, Date.now(), existingByPath.id);
674
- return {
675
- id: existingByPath.id,
676
- channel: existingByPath.channel,
677
- channelId: existingByPath.channel_id,
678
- projectPath: existingByPath.project_path,
679
- claudeSessionId,
680
- name: existingByPath.name,
681
- isActive: true,
682
- createdAt: existingByPath.created_at,
683
- updatedAt: Date.now()
684
- };
533
+ return { ...this.rowToSession(existingByPath), claudeSessionId, isActive: true, updatedAt: Date.now() };
685
534
  }
686
535
  // 取消当前活跃会话
687
- this.db.prepare(`
688
- UPDATE sessions SET is_active = 0, updated_at = ?
689
- WHERE channel = ? AND channel_id = ? AND is_active = 1
690
- `).run(Date.now(), channel, channelId);
536
+ this.deactivateAll(channel, channelId);
691
537
  // 创建会话记录
692
538
  const session = {
693
539
  id: `${channel}-${channelId}-${Date.now()}`,
@@ -700,10 +546,7 @@ export class SessionManager {
700
546
  createdAt: Date.now(),
701
547
  updatedAt: Date.now()
702
548
  };
703
- this.db.prepare(`
704
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
705
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
706
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt);
549
+ this.insertSession(session);
707
550
  return session;
708
551
  }
709
552
  // ==================== 健康状态管理 ====================