evolclaw 2.1.2 → 2.3.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 (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -1,664 +0,0 @@
1
- import { DatabaseSync } from 'node:sqlite';
2
- import { ensureDir } from '../config.js';
3
- import { resolvePaths } from '../paths.js';
4
- import { logger } from '../utils/logger.js';
5
- import { encodePath } from '../utils/platform.js';
6
- import path from 'path';
7
- import fs from 'fs';
8
- import os from 'os';
9
- export class SessionManager {
10
- db;
11
- constructor(dbPath = resolvePaths().db) {
12
- ensureDir(path.dirname(dbPath));
13
- this.db = new DatabaseSync(dbPath);
14
- this.initDatabase();
15
- }
16
- getDatabase() {
17
- return this.db;
18
- }
19
- getProjectDirName(projectPath) {
20
- return encodePath(projectPath);
21
- }
22
- getSessionFilePath(projectPath, sessionId) {
23
- const homeDir = os.homedir();
24
- const encodedPath = this.getProjectDirName(projectPath);
25
- return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
26
- }
27
- rowToSession(row) {
28
- return {
29
- id: row.id,
30
- channel: row.channel,
31
- channelId: row.channel_id,
32
- projectPath: row.project_path,
33
- threadId: row.thread_id || '',
34
- agentType: row.agent_type || 'claude',
35
- agentSessionId: row.agent_session_id,
36
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
37
- name: row.name,
38
- isActive: row.is_active === 1,
39
- createdAt: row.created_at,
40
- updatedAt: row.updated_at
41
- };
42
- }
43
- deactivateAll(channel, channelId) {
44
- this.db.prepare(`
45
- UPDATE sessions SET is_active = 0, updated_at = ?
46
- WHERE channel = ? AND channel_id = ? AND is_active = 1
47
- `).run(Date.now(), channel, channelId);
48
- }
49
- validateSessionFile(row) {
50
- const agentSessionId = row.agent_session_id;
51
- if (!agentSessionId)
52
- return undefined;
53
- const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
54
- if (fs.existsSync(sessionFile))
55
- return agentSessionId;
56
- logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
57
- this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
58
- return undefined;
59
- }
60
- insertSession(session) {
61
- this.db.prepare(`
62
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
63
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
64
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId || '', session.agentType || 'claude', session.agentSessionId ?? null, session.name ?? null, session.isActive ? 1 : 0, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
65
- }
66
- extractUserMessageText(messageContent) {
67
- if (typeof messageContent === 'string') {
68
- const text = messageContent.trim().replace(/\s+/g, ' ');
69
- return text.substring(0, 50) + (text.length > 50 ? '...' : '');
70
- }
71
- else if (Array.isArray(messageContent)) {
72
- const textContent = messageContent.find((c) => c.type === 'text');
73
- if (textContent?.text) {
74
- const text = textContent.text.trim().replace(/\s+/g, ' ');
75
- return text.substring(0, 50) + (text.length > 50 ? '...' : '');
76
- }
77
- }
78
- return null;
79
- }
80
- initDatabase() {
81
- const tableInfo = this.db.prepare('PRAGMA table_info(sessions)').all();
82
- const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
83
- const hasName = tableInfo.some((col) => col.name === 'name');
84
- const hasThreadId = tableInfo.some((col) => col.name === 'thread_id');
85
- const hasAgentType = tableInfo.some((col) => col.name === 'agent_type');
86
- const hasAgentSessionId = tableInfo.some((col) => col.name === 'agent_session_id');
87
- const hasMetadata = tableInfo.some((col) => col.name === 'metadata');
88
- // 检查是否有唯一约束
89
- const indexes = this.db.prepare('PRAGMA index_list(sessions)').all();
90
- const hasUniqueConstraint = indexes.some((idx) => idx.origin === 'u');
91
- // 迁移到新表结构(添加 thread_id, agent_type, agent_session_id, metadata)
92
- if (!hasThreadId && tableInfo.length > 0) {
93
- logger.info('Migrating database schema (adding thread support)...');
94
- this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
95
- this.db.exec(`
96
- CREATE TABLE sessions_new (
97
- id TEXT PRIMARY KEY,
98
- channel TEXT NOT NULL,
99
- channel_id TEXT NOT NULL,
100
- project_path TEXT NOT NULL,
101
- thread_id TEXT NOT NULL DEFAULT '',
102
- agent_type TEXT NOT NULL DEFAULT 'claude',
103
- agent_session_id TEXT,
104
- name TEXT,
105
- is_active INTEGER NOT NULL DEFAULT 0,
106
- created_at INTEGER NOT NULL,
107
- updated_at INTEGER NOT NULL,
108
- metadata TEXT
109
- );
110
- INSERT INTO sessions_new (id, channel, channel_id, project_path, thread_id, agent_type, agent_session_id, name, is_active, created_at, updated_at, metadata)
111
- SELECT id, channel, channel_id, project_path, '', 'claude', claude_session_id, name, is_active, created_at, updated_at, NULL FROM sessions;
112
- DROP TABLE sessions;
113
- ALTER TABLE sessions_new RENAME TO sessions;
114
- `);
115
- // 话题会话唯一约束(thread_id 非空时才生效)
116
- this.db.exec(`
117
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
118
- ON sessions(channel, channel_id, project_path, thread_id)
119
- WHERE thread_id != ''
120
- `);
121
- logger.info('✓ Database migration completed (thread support added)');
122
- }
123
- // 创建新表(首次初始化)
124
- this.db.exec(`
125
- CREATE TABLE IF NOT EXISTS sessions (
126
- id TEXT PRIMARY KEY,
127
- channel TEXT NOT NULL,
128
- channel_id TEXT NOT NULL,
129
- project_path TEXT NOT NULL,
130
- thread_id TEXT NOT NULL DEFAULT '',
131
- agent_type TEXT NOT NULL DEFAULT 'claude',
132
- agent_session_id TEXT,
133
- name TEXT,
134
- is_active INTEGER NOT NULL DEFAULT 0,
135
- created_at INTEGER NOT NULL,
136
- updated_at INTEGER NOT NULL,
137
- metadata TEXT
138
- )
139
- `);
140
- // 话题会话唯一约束(thread_id 非空时才生效)
141
- this.db.exec(`
142
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
143
- ON sessions(channel, channel_id, project_path, thread_id)
144
- WHERE thread_id != ''
145
- `);
146
- // 创建消息去重表
147
- this.db.exec(`
148
- CREATE TABLE IF NOT EXISTS processed_messages (
149
- message_id TEXT PRIMARY KEY,
150
- channel TEXT NOT NULL,
151
- channel_id TEXT NOT NULL,
152
- processed_at INTEGER NOT NULL
153
- );
154
- CREATE INDEX IF NOT EXISTS idx_processed_at ON processed_messages(processed_at);
155
- `);
156
- // 创建会话健康状态表
157
- this.db.exec(`
158
- CREATE TABLE IF NOT EXISTS session_health (
159
- session_id TEXT PRIMARY KEY,
160
- consecutive_errors INTEGER NOT NULL DEFAULT 0,
161
- last_error TEXT,
162
- last_error_type TEXT,
163
- safe_mode INTEGER NOT NULL DEFAULT 0,
164
- last_success_time INTEGER NOT NULL,
165
- created_at INTEGER NOT NULL,
166
- updated_at INTEGER NOT NULL,
167
- FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
168
- );
169
- CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
170
- `);
171
- }
172
- async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
173
- // 话题会话:独立查找/创建,不参与 isActive 竞争
174
- if (threadId) {
175
- return this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name);
176
- }
177
- // 主会话:查找活跃会话
178
- const active = this.db.prepare(`
179
- SELECT * FROM sessions
180
- WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
181
- `).get(channel, channelId);
182
- if (active) {
183
- const validSessionId = this.validateSessionFile(active);
184
- return { ...this.rowToSession(active), agentSessionId: validSessionId };
185
- }
186
- // 查找默认项目的主会话
187
- const existing = this.db.prepare(`
188
- SELECT * FROM sessions
189
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ''
190
- ORDER BY updated_at DESC LIMIT 1
191
- `).get(channel, channelId, defaultProjectPath);
192
- if (existing) {
193
- const validSessionId = this.validateSessionFile(existing);
194
- this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), existing.id);
195
- return { ...this.rowToSession(existing), agentSessionId: validSessionId, isActive: true };
196
- }
197
- // 创建新主会话
198
- const session = {
199
- id: `${channel}-${channelId}-${Date.now()}`,
200
- channel,
201
- channelId,
202
- projectPath: defaultProjectPath,
203
- threadId: '',
204
- agentType: 'claude',
205
- metadata,
206
- name: name || '默认会话',
207
- isActive: true,
208
- createdAt: Date.now(),
209
- updatedAt: Date.now()
210
- };
211
- this.insertSession(session);
212
- return session;
213
- }
214
- async updateSession(sessionId, updates) {
215
- const fields = Object.keys(updates).filter(k => k !== 'id').map(k => `${k} = ?`).join(', ');
216
- const values = Object.keys(updates).filter(k => k !== 'id').map(k => {
217
- const v = updates[k];
218
- if (v === undefined)
219
- return null;
220
- if (typeof v === 'boolean')
221
- return v ? 1 : 0;
222
- if (typeof v === 'object' && v !== null)
223
- return JSON.stringify(v);
224
- return v;
225
- });
226
- this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
227
- }
228
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
229
- // 查找已有话题会话
230
- const existing = this.db.prepare(`
231
- SELECT * FROM sessions
232
- WHERE channel = ? AND channel_id = ? AND thread_id = ?
233
- `).get(channel, channelId, threadId);
234
- if (existing) {
235
- const validSessionId = this.validateSessionFile(existing);
236
- // 合并 metadata(如果提供)
237
- if (metadata) {
238
- const existingMeta = this.rowToSession(existing).metadata;
239
- const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
240
- this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
241
- .run(JSON.stringify(merged), Date.now(), existing.id);
242
- return { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: merged };
243
- }
244
- return { ...this.rowToSession(existing), agentSessionId: validSessionId };
245
- }
246
- // 继承当前活跃主会话的项目路径
247
- const activeMain = this.db.prepare(`
248
- SELECT project_path FROM sessions
249
- WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
250
- `).get(channel, channelId);
251
- const projectPath = activeMain?.project_path || defaultProjectPath;
252
- // 创建新话题会话(isActive 固定为 false)
253
- const session = {
254
- id: `${channel}-${channelId}-${Date.now()}`,
255
- channel,
256
- channelId,
257
- projectPath,
258
- threadId,
259
- agentType: 'claude',
260
- metadata,
261
- name: name || '话题会话',
262
- isActive: false,
263
- createdAt: Date.now(),
264
- updatedAt: Date.now()
265
- };
266
- this.insertSession(session);
267
- return session;
268
- }
269
- async switchProject(channel, channelId, newProjectPath) {
270
- // 1. 取消当前活跃会话
271
- this.deactivateAll(channel, channelId);
272
- // 2. 查找目标项目的会话
273
- const target = this.db.prepare(`
274
- SELECT * FROM sessions
275
- WHERE channel = ? AND channel_id = ? AND project_path = ?
276
- ORDER BY updated_at DESC LIMIT 1
277
- `).get(channel, channelId, newProjectPath);
278
- if (target) {
279
- const validSessionId = this.validateSessionFile(target);
280
- // 激活已有会话
281
- this.db.prepare(`
282
- UPDATE sessions SET is_active = 1, updated_at = ?
283
- WHERE id = ?
284
- `).run(Date.now(), target.id);
285
- return { ...this.rowToSession(target), agentSessionId: validSessionId, isActive: true };
286
- }
287
- // 3. 创建新会话
288
- const session = {
289
- id: `${channel}-${channelId}-${Date.now()}`,
290
- channel,
291
- channelId,
292
- projectPath: newProjectPath,
293
- threadId: '',
294
- agentType: 'claude',
295
- name: '默认会话',
296
- isActive: true,
297
- createdAt: Date.now(),
298
- updatedAt: Date.now()
299
- };
300
- this.insertSession(session);
301
- return session;
302
- }
303
- async updateAgentSessionId(channel, channelId, agentSessionId) {
304
- // 只更新当前活跃会话的 Agent Session ID
305
- this.db.prepare(`
306
- UPDATE sessions
307
- SET agent_session_id = ?, updated_at = ?
308
- WHERE channel = ? AND channel_id = ? AND is_active = 1
309
- `).run(agentSessionId, Date.now(), channel, channelId);
310
- }
311
- async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
312
- // 根据 sessionId 直接更新
313
- logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
314
- this.db.prepare(`
315
- UPDATE sessions
316
- SET agent_session_id = ?, updated_at = ?
317
- WHERE id = ?
318
- `).run(agentSessionId, Date.now(), sessionId);
319
- }
320
- async clearActiveSession(channel, channelId) {
321
- // 清除当前活跃会话的 Agent Session ID
322
- this.db.prepare(`
323
- UPDATE sessions
324
- SET agent_session_id = NULL, updated_at = ?
325
- WHERE channel = ? AND channel_id = ? AND is_active = 1
326
- `).run(Date.now(), channel, channelId);
327
- }
328
- async getActiveSession(channel, channelId) {
329
- const row = this.db.prepare(`
330
- SELECT * FROM sessions
331
- WHERE channel = ? AND channel_id = ? AND is_active = 1
332
- `).get(channel, channelId);
333
- if (!row)
334
- return undefined;
335
- return this.rowToSession(row);
336
- }
337
- async listSessions(channel, channelId) {
338
- // 列出该聊天的所有会话
339
- const rows = this.db.prepare(`
340
- SELECT * FROM sessions
341
- WHERE channel = ? AND channel_id = ?
342
- ORDER BY updated_at DESC
343
- `).all(channel, channelId);
344
- return rows.map(row => this.rowToSession(row));
345
- }
346
- async getSessionByProjectPath(channel, channelId, projectPath) {
347
- const row = this.db.prepare(`
348
- SELECT * FROM sessions
349
- WHERE channel = ? AND channel_id = ? AND project_path = ?
350
- `).get(channel, channelId, projectPath);
351
- if (!row)
352
- return undefined;
353
- return this.rowToSession(row);
354
- }
355
- async getSessionByName(channel, channelId, name) {
356
- const row = this.db.prepare(`
357
- SELECT * FROM sessions
358
- WHERE channel = ? AND channel_id = ? AND name = ?
359
- `).get(channel, channelId, name);
360
- if (!row)
361
- return undefined;
362
- return this.rowToSession(row);
363
- }
364
- async switchToSession(channel, channelId, targetSessionId) {
365
- // 验证目标会话存在
366
- const target = this.db.prepare(`
367
- SELECT * FROM sessions WHERE id = ? AND channel = ? AND channel_id = ?
368
- `).get(targetSessionId, channel, channelId);
369
- if (!target)
370
- return null;
371
- // 取消当前活跃会话
372
- this.deactivateAll(channel, channelId);
373
- // 激活目标会话
374
- this.db.prepare(`
375
- UPDATE sessions SET is_active = 1, updated_at = ?
376
- WHERE id = ?
377
- `).run(Date.now(), targetSessionId);
378
- return { ...this.rowToSession(target), isActive: true, updatedAt: Date.now() };
379
- }
380
- async renameSession(sessionId, newName) {
381
- const result = this.db.prepare(`
382
- UPDATE sessions SET name = ?, updated_at = ? WHERE id = ?
383
- `).run(newName, Date.now(), sessionId);
384
- return result.changes > 0;
385
- }
386
- async unbindSession(sessionId) {
387
- const result = this.db.prepare(`
388
- DELETE FROM sessions WHERE id = ?
389
- `).run(sessionId);
390
- return result.changes > 0;
391
- }
392
- async createNewSession(channel, channelId, projectPath, name) {
393
- // 取消当前活跃会话
394
- this.deactivateAll(channel, channelId);
395
- // 创建新会话
396
- const session = {
397
- id: `${channel}-${channelId}-${Date.now()}`,
398
- channel,
399
- channelId,
400
- projectPath,
401
- threadId: '',
402
- agentType: 'claude',
403
- name: name || '默认会话',
404
- isActive: true,
405
- createdAt: Date.now(),
406
- updatedAt: Date.now()
407
- };
408
- this.insertSession(session);
409
- return session;
410
- }
411
- /**
412
- * 基于现有会话创建分支会话
413
- */
414
- async createForkedSession(sourceSession, forkedAgentSessionId, name) {
415
- // 取消当前活跃会话
416
- this.deactivateAll(sourceSession.channel, sourceSession.channelId);
417
- const session = {
418
- id: `${sourceSession.channel}-${sourceSession.channelId}-${Date.now()}`,
419
- channel: sourceSession.channel,
420
- channelId: sourceSession.channelId,
421
- projectPath: sourceSession.projectPath,
422
- threadId: sourceSession.threadId || '',
423
- agentType: sourceSession.agentType || 'claude',
424
- agentSessionId: forkedAgentSessionId,
425
- name: name || `${sourceSession.name || '会话'}-分支`,
426
- isActive: true,
427
- createdAt: Date.now(),
428
- updatedAt: Date.now()
429
- };
430
- this.insertSession(session);
431
- return session;
432
- }
433
- async scanCliSessions(projectPath) {
434
- const homeDir = os.homedir();
435
- const encodedPath = this.getProjectDirName(projectPath);
436
- const sessionDir = path.join(homeDir, '.claude', 'projects', encodedPath);
437
- if (!fs.existsSync(sessionDir))
438
- return [];
439
- const files = fs.readdirSync(sessionDir)
440
- .filter(f => f.endsWith('.jsonl'))
441
- .filter(f => !f.startsWith('agent-')) // 过滤子代理会话
442
- .map(f => {
443
- const filePath = path.join(sessionDir, f);
444
- const stat = fs.statSync(filePath);
445
- return { uuid: f.replace('.jsonl', ''), mtime: stat.mtimeMs, size: stat.size };
446
- })
447
- .filter(f => f.size > 0) // 过滤空文件
448
- .sort((a, b) => b.mtime - a.mtime)
449
- .slice(0, 10);
450
- return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
451
- }
452
- checkSessionFileExists(projectPath, agentSessionId) {
453
- const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
454
- return fs.existsSync(sessionFile);
455
- }
456
- readSessionFirstMessage(projectPath, agentSessionId) {
457
- const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
458
- if (!fs.existsSync(sessionFile))
459
- return null;
460
- try {
461
- const content = fs.readFileSync(sessionFile, 'utf-8');
462
- const lines = content.split('\n').filter(l => l.trim());
463
- for (const line of lines) {
464
- const event = JSON.parse(line);
465
- if (event.type === 'user' && event.message?.role === 'user') {
466
- const text = this.extractUserMessageText(event.message.content);
467
- if (text)
468
- return text;
469
- }
470
- }
471
- }
472
- catch (error) {
473
- logger.warn(`Failed to read session file: ${sessionFile}`, error);
474
- }
475
- return null;
476
- }
477
- readSessionLastUserMessage(projectPath, agentSessionId) {
478
- const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
479
- if (!fs.existsSync(sessionFile))
480
- return null;
481
- try {
482
- const content = fs.readFileSync(sessionFile, 'utf-8');
483
- const lines = content.split('\n').filter(l => l.trim());
484
- let lastMessage = null;
485
- for (const line of lines) {
486
- const event = JSON.parse(line);
487
- if (event.type === 'user' && event.message?.role === 'user') {
488
- lastMessage = this.extractUserMessageText(event.message.content) ?? lastMessage;
489
- }
490
- }
491
- return lastMessage;
492
- }
493
- catch (error) {
494
- logger.warn(`Failed to read last message from session file: ${sessionFile}`, error);
495
- }
496
- return null;
497
- }
498
- /**
499
- * 获取会话文件信息(回合数 + 标题)
500
- */
501
- getSessionFileInfo(projectPath, agentSessionId) {
502
- const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
503
- if (!fs.existsSync(sessionFile))
504
- return { turns: 0 };
505
- try {
506
- const content = fs.readFileSync(sessionFile, 'utf-8');
507
- const lines = content.split('\n').filter(l => l.trim());
508
- let turns = 0;
509
- let title;
510
- for (const line of lines) {
511
- const event = JSON.parse(line);
512
- if (event.type === 'user' && event.message?.role === 'user') {
513
- // Only count real user input, skip auto-generated tool_result messages
514
- const content = event.message.content;
515
- const isToolResult = Array.isArray(content) && content.every((c) => c.type === 'tool_result');
516
- if (!isToolResult) {
517
- turns++;
518
- }
519
- }
520
- // 提取会话标题(从 session 元数据中)
521
- if (event.title && !title) {
522
- title = event.title;
523
- }
524
- if (event.sessionTitle && !title) {
525
- title = event.sessionTitle;
526
- }
527
- }
528
- return { turns, title };
529
- }
530
- catch (error) {
531
- logger.warn(`Failed to read session file info: ${sessionFile}`, error);
532
- return { turns: 0 };
533
- }
534
- }
535
- async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
536
- const rows = this.db.prepare(`
537
- SELECT * FROM sessions
538
- WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ?
539
- `).all(channel, channelId, `${uuidPrefix}%`);
540
- if (rows.length === 0)
541
- return undefined;
542
- if (rows.length > 1) {
543
- logger.warn(`Multiple sessions found with UUID prefix: ${uuidPrefix}`);
544
- }
545
- return this.rowToSession(rows[0]);
546
- }
547
- async importCliSession(channel, channelId, projectPath, agentSessionId) {
548
- // 取消当前活跃会话
549
- this.deactivateAll(channel, channelId);
550
- // 从 CLI 会话文件读取标题
551
- const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId);
552
- const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
553
- // 创建新会话记录
554
- const session = {
555
- id: `${channel}-${channelId}-${Date.now()}`,
556
- channel,
557
- channelId,
558
- projectPath,
559
- threadId: '',
560
- agentType: 'claude',
561
- agentSessionId,
562
- name,
563
- isActive: true,
564
- createdAt: Date.now(),
565
- updatedAt: Date.now()
566
- };
567
- this.insertSession(session);
568
- return session;
569
- }
570
- // ==================== 健康状态管理 ====================
571
- /**
572
- * 获取会话健康状态
573
- */
574
- async getHealthStatus(sessionId) {
575
- const row = this.db.prepare(`
576
- SELECT * FROM session_health WHERE session_id = ?
577
- `).get(sessionId);
578
- if (!row) {
579
- // 首次查询,创建默认记录
580
- const now = Date.now();
581
- this.db.prepare(`
582
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
583
- VALUES (?, 0, 0, ?, ?, ?)
584
- `).run(sessionId, now, now, now);
585
- return {
586
- consecutiveErrors: 0,
587
- safeMode: false,
588
- lastSuccessTime: now
589
- };
590
- }
591
- return {
592
- consecutiveErrors: row.consecutive_errors,
593
- lastError: row.last_error,
594
- lastErrorType: row.last_error_type,
595
- safeMode: row.safe_mode === 1,
596
- lastSuccessTime: row.last_success_time
597
- };
598
- }
599
- /**
600
- * 记录成功响应(重置错误计数)
601
- */
602
- async recordSuccess(sessionId) {
603
- const now = Date.now();
604
- this.db.prepare(`
605
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
606
- VALUES (?, 0, 0, ?, ?, ?)
607
- ON CONFLICT(session_id) DO UPDATE SET
608
- consecutive_errors = 0,
609
- last_success_time = ?,
610
- updated_at = ?
611
- `).run(sessionId, now, now, now, now, now);
612
- }
613
- /**
614
- * 记录错误(增加计数)
615
- */
616
- async recordError(sessionId, errorType, errorMessage) {
617
- const now = Date.now();
618
- const health = await this.getHealthStatus(sessionId);
619
- const newCount = health.consecutiveErrors + 1;
620
- this.db.prepare(`
621
- INSERT INTO session_health (session_id, consecutive_errors, last_error, last_error_type, safe_mode, last_success_time, created_at, updated_at)
622
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
623
- ON CONFLICT(session_id) DO UPDATE SET
624
- consecutive_errors = consecutive_errors + 1,
625
- last_error = ?,
626
- last_error_type = ?,
627
- updated_at = ?
628
- `).run(sessionId, newCount, errorMessage, errorType, health.safeMode ? 1 : 0, health.lastSuccessTime, now, now, errorMessage, errorType, now);
629
- return newCount;
630
- }
631
- /**
632
- * 设置安全模式
633
- */
634
- async setSafeMode(sessionId, enabled) {
635
- const now = Date.now();
636
- const health = await this.getHealthStatus(sessionId);
637
- this.db.prepare(`
638
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
639
- VALUES (?, ?, ?, ?, ?, ?)
640
- ON CONFLICT(session_id) DO UPDATE SET
641
- safe_mode = ?,
642
- updated_at = ?
643
- `).run(sessionId, health.consecutiveErrors, enabled ? 1 : 0, health.lastSuccessTime, now, now, enabled ? 1 : 0, now);
644
- }
645
- /**
646
- * 重置健康状态(用于修复后)
647
- */
648
- async resetHealthStatus(sessionId) {
649
- const now = Date.now();
650
- this.db.prepare(`
651
- INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
652
- VALUES (?, 0, 0, ?, ?, ?)
653
- ON CONFLICT(session_id) DO UPDATE SET
654
- consecutive_errors = 0,
655
- last_error = NULL,
656
- last_error_type = NULL,
657
- safe_mode = 0,
658
- updated_at = ?
659
- `).run(sessionId, now, now, now, now);
660
- }
661
- close() {
662
- this.db.close();
663
- }
664
- }