evolclaw 2.1.1 → 2.2.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 (43) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +571 -223
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/error-utils.js +4 -2
  27. package/dist/utils/init-feishu.js +2 -0
  28. package/dist/utils/init-wechat.js +2 -0
  29. package/dist/utils/init.js +285 -53
  30. package/dist/utils/ipc-client.js +36 -0
  31. package/dist/utils/migrate-project.js +122 -0
  32. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  33. package/dist/utils/rich-content-renderer.js +228 -0
  34. package/dist/utils/session-file-health.js +11 -34
  35. package/dist/utils/stream-debouncer.js +122 -0
  36. package/dist/utils/stream-idle-monitor.js +1 -1
  37. package/package.json +3 -1
  38. package/dist/core/agent-runner.js +0 -348
  39. package/dist/core/message-stream.js +0 -59
  40. package/dist/index.js.bak +0 -340
  41. package/dist/utils/markdown-to-feishu.js +0 -94
  42. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  43. /package/dist/{core → utils}/message-cache.js +0 -0
@@ -2,17 +2,32 @@ import { DatabaseSync } from 'node:sqlite';
2
2
  import { ensureDir } from '../config.js';
3
3
  import { resolvePaths } from '../paths.js';
4
4
  import { logger } from '../utils/logger.js';
5
- import { encodePath } from '../utils/platform.js';
5
+ import { encodePath } from '../utils/cross-platform.js';
6
6
  import path from 'path';
7
7
  import fs from 'fs';
8
8
  import os from 'os';
9
9
  export class SessionManager {
10
10
  db;
11
- constructor(dbPath = resolvePaths().db) {
11
+ eventBus;
12
+ ownerResolver;
13
+ fileAdapters = new Map();
14
+ constructor(dbPath = resolvePaths().db, eventBus, ownerResolver) {
12
15
  ensureDir(path.dirname(dbPath));
13
16
  this.db = new DatabaseSync(dbPath);
17
+ this.eventBus = eventBus;
18
+ this.ownerResolver = ownerResolver;
14
19
  this.initDatabase();
15
20
  }
21
+ setOwnerResolver(resolver) {
22
+ this.ownerResolver = resolver;
23
+ }
24
+ registerFileAdapter(adapter) {
25
+ this.fileAdapters.set(adapter.agentId, adapter);
26
+ logger.debug(`[SessionManager] Registered file adapter: ${adapter.agentId}`);
27
+ }
28
+ getFileAdapter(agentId) {
29
+ return this.fileAdapters.get(agentId);
30
+ }
16
31
  getDatabase() {
17
32
  return this.db;
18
33
  }
@@ -25,43 +40,76 @@ export class SessionManager {
25
40
  return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
26
41
  }
27
42
  rowToSession(row) {
43
+ const metadata = row.metadata ? JSON.parse(row.metadata) : undefined;
28
44
  return {
29
45
  id: row.id,
30
46
  channel: row.channel,
31
47
  channelId: row.channel_id,
32
48
  projectPath: row.project_path,
33
49
  threadId: row.thread_id || '',
34
- agentType: row.agent_type || 'claude',
50
+ agentId: row.agent_id || 'claude',
51
+ chatType: row.chat_type || 'private',
52
+ sessionMode: row.session_mode || 'interactive',
35
53
  agentSessionId: row.agent_session_id,
36
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
54
+ metadata,
37
55
  name: row.name,
38
- isActive: row.is_active === 1,
56
+ processingState: row.processing_state || undefined,
39
57
  createdAt: row.created_at,
40
- updatedAt: row.updated_at
58
+ updatedAt: row.updated_at,
59
+ deletedAt: row.deleted_at ?? undefined,
41
60
  };
42
61
  }
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);
62
+ /** 根据 userId 计算身份 */
63
+ resolveIdentity(channel, userId) {
64
+ if (!userId)
65
+ return { role: 'anonymous', mode: 'interactive' };
66
+ const isOwner = this.ownerResolver?.(channel, userId) ?? false;
67
+ return { role: isOwner ? 'owner' : 'guest', mode: 'interactive' };
68
+ }
69
+ /** 更新 session 的 identity(owner 绑定后调用) */
70
+ async updateIdentity(sessionId, identity) {
71
+ // identity 不持久化到 DB,仅更新内存中的返回值
72
+ // 调用方应直接修改持有的 session 对象
73
+ logger.debug(`[SessionManager] updateIdentity: sessionId=${sessionId}, role=${identity.role}`);
74
+ }
75
+ /** 取消所有活跃会话(通过 metadata.isActive) */
76
+ deactivateAllMetadata(channel, channelId) {
77
+ const rows = this.db.prepare(`
78
+ SELECT id, metadata FROM sessions
79
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
80
+ `).all(channel, channelId);
81
+ for (const row of rows) {
82
+ const metadata = row.metadata ? JSON.parse(row.metadata) : {};
83
+ metadata.isActive = false;
84
+ this.db.prepare(`
85
+ UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?
86
+ `).run(JSON.stringify(metadata), Date.now(), row.id);
87
+ }
48
88
  }
49
89
  validateSessionFile(row) {
50
90
  const agentSessionId = row.agent_session_id;
51
91
  if (!agentSessionId)
52
92
  return undefined;
53
- const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
54
- if (fs.existsSync(sessionFile))
93
+ const agentId = row.agent_id || 'claude';
94
+ const adapter = this.getFileAdapter(agentId);
95
+ if (!adapter) {
96
+ // 无适配器时回退到 Claude 路径检查
97
+ const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
98
+ if (fs.existsSync(sessionFile))
99
+ return agentSessionId;
100
+ }
101
+ else if (adapter.checkExists(row.project_path, agentSessionId)) {
55
102
  return agentSessionId;
56
- logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
103
+ }
104
+ logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
57
105
  this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
58
106
  return undefined;
59
107
  }
60
108
  insertSession(session) {
61
109
  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);
110
+ INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_id, chat_type, session_mode, agent_session_id, name, created_at, updated_at, metadata)
111
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
112
+ `).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId || '', session.agentId || 'claude', session.chatType || 'private', session.sessionMode || 'interactive', session.agentSessionId ?? null, session.name ?? null, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
65
113
  }
66
114
  extractUserMessageText(messageContent) {
67
115
  if (typeof messageContent === 'string') {
@@ -83,42 +131,165 @@ export class SessionManager {
83
131
  const hasName = tableInfo.some((col) => col.name === 'name');
84
132
  const hasThreadId = tableInfo.some((col) => col.name === 'thread_id');
85
133
  const hasAgentType = tableInfo.some((col) => col.name === 'agent_type');
134
+ const hasAgentId = tableInfo.some((col) => col.name === 'agent_id');
86
135
  const hasAgentSessionId = tableInfo.some((col) => col.name === 'agent_session_id');
87
136
  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)...');
137
+ const hasIsGroup = tableInfo.some((col) => col.name === 'is_group');
138
+ const hasChatType = tableInfo.some((col) => col.name === 'chat_type');
139
+ const hasSessionMode = tableInfo.some((col) => col.name === 'session_mode');
140
+ const hasDeletedAt = tableInfo.some((col) => col.name === 'deleted_at');
141
+ // 检测是否需要 schema 重构迁移(旧字段存在,新字段不存在)
142
+ const needsSchemaRefactor = tableInfo.length > 0 && (hasIsGroup || hasIsActive || hasAgentType) && (!hasChatType || !hasAgentId || !hasSessionMode);
143
+ // Schema 重构迁移:is_group → chat_type, agent_type → agent_id, 移除 is_active
144
+ if (needsSchemaRefactor) {
145
+ logger.info('Migrating database schema (session model refactor)...');
94
146
  this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
95
147
  this.db.exec(`
96
148
  CREATE TABLE sessions_new (
97
149
  id TEXT PRIMARY KEY,
98
150
  channel TEXT NOT NULL,
99
151
  channel_id TEXT NOT NULL,
100
- project_path TEXT NOT NULL,
152
+ agent_id TEXT NOT NULL DEFAULT 'claude',
101
153
  thread_id TEXT NOT NULL DEFAULT '',
102
- agent_type TEXT NOT NULL DEFAULT 'claude',
154
+ chat_type TEXT NOT NULL DEFAULT 'private',
155
+ session_mode TEXT NOT NULL DEFAULT 'interactive',
156
+ project_path TEXT NOT NULL,
103
157
  agent_session_id TEXT,
104
158
  name TEXT,
105
- is_active INTEGER NOT NULL DEFAULT 0,
106
159
  created_at INTEGER NOT NULL,
107
160
  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;
161
+ metadata TEXT,
162
+ deleted_at INTEGER
163
+ )
114
164
  `);
115
- // 话题会话唯一约束(thread_id 非空时才生效)
165
+ // 迁移数据:is_group → chat_type, agent_type → agent_id
116
166
  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 != ''
167
+ INSERT INTO sessions_new (id, channel, channel_id, agent_id, thread_id, chat_type, session_mode, project_path, agent_session_id, name, created_at, updated_at, metadata, deleted_at)
168
+ SELECT
169
+ id,
170
+ channel,
171
+ channel_id,
172
+ COALESCE(agent_type, 'claude'),
173
+ COALESCE(thread_id, ''),
174
+ CASE WHEN is_group = 1 THEN 'group' ELSE 'private' END,
175
+ 'interactive',
176
+ project_path,
177
+ agent_session_id,
178
+ name,
179
+ created_at,
180
+ updated_at,
181
+ metadata,
182
+ deleted_at
183
+ FROM sessions
120
184
  `);
121
- logger.info('✓ Database migration completed (thread support added)');
185
+ this.db.exec(`DROP TABLE sessions`);
186
+ this.db.exec(`ALTER TABLE sessions_new RENAME TO sessions`);
187
+ // 创建新索引
188
+ this.db.exec(`
189
+ CREATE INDEX IF NOT EXISTS idx_session_space
190
+ ON sessions(channel, channel_id, agent_id, thread_id)
191
+ WHERE deleted_at IS NULL
192
+ `);
193
+ this.db.exec(`
194
+ CREATE INDEX IF NOT EXISTS idx_session_active
195
+ ON sessions(channel, channel_id)
196
+ WHERE deleted_at IS NULL
197
+ `);
198
+ logger.info('✓ Database migration completed (session model refactored)');
199
+ }
200
+ // ── 旧 schema 迁移(仅当旧字段存在、新字段还未迁移时运行)──
201
+ // 这些迁移按顺序将最旧的 schema 逐步升级到包含 is_group 的中间格式,
202
+ // 然后由上面的 needsSchemaRefactor 迁移一步到位转为新 schema。
203
+ if (!needsSchemaRefactor && tableInfo.length > 0 && hasAgentType) {
204
+ // 检查是否有唯一约束
205
+ const indexes = this.db.prepare('PRAGMA index_list(sessions)').all();
206
+ const hasUniqueConstraint = indexes.some((idx) => idx.origin === 'u');
207
+ // 迁移到新表结构(添加 thread_id, agent_type, agent_session_id, metadata)
208
+ if (!hasThreadId) {
209
+ logger.info('Migrating database schema (adding thread support)...');
210
+ this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
211
+ this.db.exec(`
212
+ CREATE TABLE sessions_new (
213
+ id TEXT PRIMARY KEY,
214
+ channel TEXT NOT NULL,
215
+ channel_id TEXT NOT NULL,
216
+ project_path TEXT NOT NULL,
217
+ thread_id TEXT NOT NULL DEFAULT '',
218
+ agent_type TEXT NOT NULL DEFAULT 'claude',
219
+ agent_session_id TEXT,
220
+ name TEXT,
221
+ is_active INTEGER NOT NULL DEFAULT 0,
222
+ created_at INTEGER NOT NULL,
223
+ updated_at INTEGER NOT NULL,
224
+ metadata TEXT
225
+ );
226
+ 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)
227
+ SELECT id, channel, channel_id, project_path, '', 'claude', claude_session_id, name, is_active, created_at, updated_at, NULL FROM sessions;
228
+ DROP TABLE sessions;
229
+ ALTER TABLE sessions_new RENAME TO sessions;
230
+ `);
231
+ // 话题会话唯一约束(thread_id 非空时才生效)
232
+ this.db.exec(`
233
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
234
+ ON sessions(channel, channel_id, project_path, thread_id)
235
+ WHERE thread_id != ''
236
+ `);
237
+ logger.info('✓ Database migration completed (thread support added)');
238
+ }
239
+ // Migration: add is_group column
240
+ if (!hasIsGroup) {
241
+ logger.info('Migrating database schema (adding is_group)...');
242
+ const addIsGroupCol = 'ALTER TABLE sessions ADD COLUMN is_group INTEGER NOT NULL DEFAULT 0';
243
+ this.db.exec(addIsGroupCol);
244
+ logger.info('✓ Database migration completed (is_group added)');
245
+ }
246
+ // Reset incorrect is_group values (oc_ prefix doesn't reliably indicate group chat)
247
+ if (hasIsGroup) {
248
+ this.db.exec("UPDATE sessions SET is_group = 0 WHERE channel = 'feishu'");
249
+ }
250
+ // Migration: add deleted_at column
251
+ if (!hasDeletedAt) {
252
+ logger.info('Migrating database schema (adding deleted_at)...');
253
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN deleted_at INTEGER`);
254
+ logger.info('✓ Database migration completed (deleted_at added)');
255
+ }
256
+ }
257
+ // Migration: add processing_state column (独立于 schema 重构)
258
+ if (tableInfo.length > 0) {
259
+ const hasProcessingState = tableInfo.some((col) => col.name === 'processing_state');
260
+ if (!hasProcessingState) {
261
+ logger.info('Migrating database schema (adding processing_state)...');
262
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN processing_state TEXT`);
263
+ logger.info('✓ Database migration completed (processing_state added)');
264
+ }
265
+ }
266
+ // Migration: normalize legacy metadata rootId → replyContext
267
+ if (hasMetadata && tableInfo.length > 0) {
268
+ const rows = this.db.prepare(`SELECT id, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
269
+ let migrated = 0;
270
+ for (const row of rows) {
271
+ try {
272
+ const meta = JSON.parse(row.metadata);
273
+ const rootId = meta.feishu?.rootId ?? meta.threadRootId ?? meta.replyOpts?.rootId;
274
+ if (!rootId && !meta.feishu && !meta.threadRootId && !meta.replyOpts)
275
+ continue;
276
+ // Generate replyContext from rootId if missing
277
+ if (rootId && !meta.replyContext) {
278
+ meta.replyContext = { replyToMessageId: rootId, replyInThread: true };
279
+ }
280
+ // Clean up all legacy fields
281
+ delete meta.feishu;
282
+ delete meta.threadRootId;
283
+ delete meta.replyOpts;
284
+ this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
285
+ .run(JSON.stringify(meta), row.id);
286
+ migrated++;
287
+ }
288
+ catch { /* skip malformed JSON */ }
289
+ }
290
+ if (migrated > 0) {
291
+ logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
292
+ }
122
293
  }
123
294
  // 创建新表(首次初始化)
124
295
  this.db.exec(`
@@ -126,22 +297,30 @@ export class SessionManager {
126
297
  id TEXT PRIMARY KEY,
127
298
  channel TEXT NOT NULL,
128
299
  channel_id TEXT NOT NULL,
129
- project_path TEXT NOT NULL,
300
+ agent_id TEXT NOT NULL DEFAULT 'claude',
130
301
  thread_id TEXT NOT NULL DEFAULT '',
131
- agent_type TEXT NOT NULL DEFAULT 'claude',
302
+ chat_type TEXT NOT NULL DEFAULT 'private',
303
+ session_mode TEXT NOT NULL DEFAULT 'interactive',
304
+ project_path TEXT NOT NULL,
132
305
  agent_session_id TEXT,
133
306
  name TEXT,
134
- is_active INTEGER NOT NULL DEFAULT 0,
307
+ processing_state TEXT,
135
308
  created_at INTEGER NOT NULL,
136
309
  updated_at INTEGER NOT NULL,
137
- metadata TEXT
310
+ metadata TEXT,
311
+ deleted_at INTEGER
138
312
  )
139
313
  `);
140
- // 话题会话唯一约束(thread_id 非空时才生效)
314
+ // 会话空间索引(查询优化,无唯一约束)
315
+ this.db.exec(`
316
+ CREATE INDEX IF NOT EXISTS idx_session_space
317
+ ON sessions(channel, channel_id, agent_id, thread_id)
318
+ WHERE deleted_at IS NULL
319
+ `);
141
320
  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 != ''
321
+ CREATE INDEX IF NOT EXISTS idx_session_active
322
+ ON sessions(channel, channel_id)
323
+ WHERE deleted_at IS NULL
145
324
  `);
146
325
  // 创建消息去重表
147
326
  this.db.exec(`
@@ -169,67 +348,172 @@ export class SessionManager {
169
348
  CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
170
349
  `);
171
350
  }
172
- async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
173
- // 话题会话:独立查找/创建,不参与 isActive 竞争
351
+ /**
352
+ * 获取指定渠道所有已知的 thread_id(用于重启后预填充 seenThreads)
353
+ */
354
+ getKnownThreadIds(channel) {
355
+ const rows = this.db.prepare(`
356
+ SELECT DISTINCT thread_id FROM sessions
357
+ WHERE channel = ? AND thread_id != '' AND deleted_at IS NULL
358
+ `).all(channel);
359
+ return rows.map(r => r.thread_id);
360
+ }
361
+ /**
362
+ * 标记会话为处理中(实时写 DB,crash 也能恢复)
363
+ */
364
+ markProcessing(sessionId) {
365
+ const now = Date.now();
366
+ this.db.prepare(`UPDATE sessions SET processing_state = ?, updated_at = ? WHERE id = ?`)
367
+ .run(String(now), now, sessionId);
368
+ }
369
+ /**
370
+ * 清除会话处理中状态
371
+ */
372
+ clearProcessing(sessionId) {
373
+ this.db.prepare(`UPDATE sessions SET processing_state = NULL, updated_at = ? WHERE id = ?`)
374
+ .run(Date.now(), sessionId);
375
+ }
376
+ /**
377
+ * 获取所有处于 processing 状态的会话(用于重启后恢复)
378
+ * @param maxAgeMs 最大存活时间(超过则视为超时,清除状态)默认 1 小时
379
+ */
380
+ getPendingProcessingSessions(maxAgeMs = 60 * 60 * 1000) {
381
+ const rows = this.db.prepare(`
382
+ SELECT * FROM sessions
383
+ WHERE processing_state IS NOT NULL AND deleted_at IS NULL
384
+ `).all();
385
+ const now = Date.now();
386
+ const result = [];
387
+ for (const row of rows) {
388
+ const ts = parseInt(row.processing_state, 10);
389
+ if (!isNaN(ts) && (now - ts) < maxAgeMs) {
390
+ result.push(this.rowToSession(row));
391
+ }
392
+ else {
393
+ // 超时:清除过期状态
394
+ this.db.prepare(`UPDATE sessions SET processing_state = NULL WHERE id = ?`)
395
+ .run(row.id);
396
+ }
397
+ }
398
+ return result;
399
+ }
400
+ async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId) {
401
+ // 话题会话:独立查找/创建
174
402
  if (threadId) {
175
- return this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name);
403
+ const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId);
404
+ session.identity = this.resolveIdentity(channel, userId);
405
+ return session;
176
406
  }
177
407
  // 主会话:查找活跃会话
178
408
  const active = this.db.prepare(`
179
409
  SELECT * FROM sessions
180
- WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
410
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = '' AND deleted_at IS NULL
181
411
  `).get(channel, channelId);
182
412
  if (active) {
183
413
  const validSessionId = this.validateSessionFile(active);
184
- return { ...this.rowToSession(active), agentSessionId: validSessionId };
414
+ const session = { ...this.rowToSession(active), agentSessionId: validSessionId };
415
+ session.identity = this.resolveIdentity(channel, userId);
416
+ // 补写 peerId/peerName(私聊 session 可能在此字段引入前创建)
417
+ if (chatType === 'private' && userId) {
418
+ const activeMeta = active.metadata ? JSON.parse(active.metadata) : {};
419
+ let updated = false;
420
+ if (!activeMeta.peerId) {
421
+ activeMeta.peerId = userId;
422
+ updated = true;
423
+ }
424
+ if (!activeMeta.peerName && metadata?.peerName) {
425
+ activeMeta.peerName = metadata.peerName;
426
+ updated = true;
427
+ }
428
+ if (updated) {
429
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
430
+ .run(JSON.stringify(activeMeta), Date.now(), active.id);
431
+ session.metadata = activeMeta;
432
+ }
433
+ }
434
+ return session;
185
435
  }
186
436
  // 查找默认项目的主会话
187
437
  const existing = this.db.prepare(`
188
438
  SELECT * FROM sessions
189
- WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ''
439
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = '' AND deleted_at IS NULL
190
440
  ORDER BY updated_at DESC LIMIT 1
191
441
  `).get(channel, channelId, defaultProjectPath);
192
442
  if (existing) {
193
443
  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 };
444
+ // 激活此会话
445
+ const existingMeta = existing.metadata ? JSON.parse(existing.metadata) : {};
446
+ existingMeta.isActive = true;
447
+ // 补写 peerId/peerName
448
+ if (chatType === 'private' && userId && !existingMeta.peerId) {
449
+ existingMeta.peerId = userId;
450
+ }
451
+ if (chatType === 'private' && metadata?.peerName && !existingMeta.peerName) {
452
+ existingMeta.peerName = metadata.peerName;
453
+ }
454
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
455
+ .run(JSON.stringify(existingMeta), Date.now(), existing.id);
456
+ const session = { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: existingMeta };
457
+ session.identity = this.resolveIdentity(channel, userId);
458
+ return session;
196
459
  }
197
460
  // 创建新主会话
461
+ const sessionMetadata = { ...metadata, isActive: true };
198
462
  const session = {
199
463
  id: `${channel}-${channelId}-${Date.now()}`,
200
464
  channel,
201
465
  channelId,
202
466
  projectPath: defaultProjectPath,
203
467
  threadId: '',
204
- agentType: 'claude',
205
- metadata,
468
+ agentId: agentId || 'claude',
469
+ chatType: chatType || 'private',
470
+ sessionMode: 'interactive',
471
+ metadata: sessionMetadata,
206
472
  name: name || '默认会话',
207
- isActive: true,
208
473
  createdAt: Date.now(),
209
474
  updatedAt: Date.now()
210
475
  };
476
+ session.identity = this.resolveIdentity(channel, userId);
211
477
  this.insertSession(session);
478
+ this.eventBus.publish({
479
+ type: 'session:created',
480
+ sessionId: session.id,
481
+ channel,
482
+ channelId,
483
+ projectPath: defaultProjectPath,
484
+ name: session.name,
485
+ chatType: session.chatType,
486
+ timestamp: Date.now()
487
+ });
212
488
  return session;
213
489
  }
214
490
  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);
491
+ const sets = [];
492
+ const values = [];
493
+ if (updates.chatType !== undefined) {
494
+ sets.push('chat_type = ?');
495
+ values.push(updates.chatType);
496
+ }
497
+ if (updates.name !== undefined) {
498
+ sets.push('name = ?');
499
+ values.push(updates.name);
500
+ }
501
+ if (updates.metadata !== undefined) {
502
+ sets.push('metadata = ?');
503
+ values.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
504
+ }
505
+ if (sets.length === 0)
506
+ return;
507
+ sets.push('updated_at = ?');
508
+ values.push(Date.now());
509
+ values.push(sessionId);
510
+ this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...values);
227
511
  }
228
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
512
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId) {
229
513
  // 查找已有话题会话
230
514
  const existing = this.db.prepare(`
231
515
  SELECT * FROM sessions
232
- WHERE channel = ? AND channel_id = ? AND thread_id = ?
516
+ WHERE channel = ? AND channel_id = ? AND thread_id = ? AND deleted_at IS NULL
233
517
  `).get(channel, channelId, threadId);
234
518
  if (existing) {
235
519
  const validSessionId = this.validateSessionFile(existing);
@@ -246,43 +530,54 @@ export class SessionManager {
246
530
  // 继承当前活跃主会话的项目路径
247
531
  const activeMain = this.db.prepare(`
248
532
  SELECT project_path FROM sessions
249
- WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
533
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = ''
250
534
  `).get(channel, channelId);
251
535
  const projectPath = activeMain?.project_path || defaultProjectPath;
252
- // 创建新话题会话(isActive 固定为 false)
536
+ // 创建新话题会话
253
537
  const session = {
254
538
  id: `${channel}-${channelId}-${Date.now()}`,
255
539
  channel,
256
540
  channelId,
257
541
  projectPath,
258
542
  threadId,
259
- agentType: 'claude',
543
+ agentId: agentId || 'claude',
544
+ chatType: 'private',
545
+ sessionMode: 'interactive',
260
546
  metadata,
261
547
  name: name || '话题会话',
262
- isActive: false,
263
548
  createdAt: Date.now(),
264
549
  updatedAt: Date.now()
265
550
  };
266
551
  this.insertSession(session);
552
+ this.eventBus.publish({
553
+ type: 'session:created',
554
+ sessionId: session.id,
555
+ channel,
556
+ channelId,
557
+ projectPath,
558
+ name: session.name,
559
+ timestamp: Date.now()
560
+ });
267
561
  return session;
268
562
  }
269
- async switchProject(channel, channelId, newProjectPath) {
563
+ async switchProject(channel, channelId, newProjectPath, currentAgentId) {
564
+ const agentId = currentAgentId || 'claude';
270
565
  // 1. 取消当前活跃会话
271
- this.deactivateAll(channel, channelId);
272
- // 2. 查找目标项目的会话
566
+ this.deactivateAllMetadata(channel, channelId);
567
+ // 2. 查找目标项目 + 当前 agent 的会话
273
568
  const target = this.db.prepare(`
274
569
  SELECT * FROM sessions
275
- WHERE channel = ? AND channel_id = ? AND project_path = ?
570
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
276
571
  ORDER BY updated_at DESC LIMIT 1
277
- `).get(channel, channelId, newProjectPath);
572
+ `).get(channel, channelId, newProjectPath, agentId);
278
573
  if (target) {
279
574
  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 };
575
+ // 激活目标会话
576
+ const metadata = target.metadata ? JSON.parse(target.metadata) : {};
577
+ metadata.isActive = true;
578
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
579
+ .run(JSON.stringify(metadata), Date.now(), target.id);
580
+ return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
286
581
  }
287
582
  // 3. 创建新会话
288
583
  const session = {
@@ -291,13 +586,24 @@ export class SessionManager {
291
586
  channelId,
292
587
  projectPath: newProjectPath,
293
588
  threadId: '',
294
- agentType: 'claude',
589
+ agentId,
590
+ chatType: 'private',
591
+ sessionMode: 'interactive',
592
+ metadata: { isActive: true },
295
593
  name: '默认会话',
296
- isActive: true,
297
594
  createdAt: Date.now(),
298
595
  updatedAt: Date.now()
299
596
  };
300
597
  this.insertSession(session);
598
+ this.eventBus.publish({
599
+ type: 'session:created',
600
+ sessionId: session.id,
601
+ channel,
602
+ channelId,
603
+ projectPath: newProjectPath,
604
+ name: session.name,
605
+ timestamp: Date.now()
606
+ });
301
607
  return session;
302
608
  }
303
609
  async updateAgentSessionId(channel, channelId, agentSessionId) {
@@ -305,7 +611,7 @@ export class SessionManager {
305
611
  this.db.prepare(`
306
612
  UPDATE sessions
307
613
  SET agent_session_id = ?, updated_at = ?
308
- WHERE channel = ? AND channel_id = ? AND is_active = 1
614
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
309
615
  `).run(agentSessionId, Date.now(), channel, channelId);
310
616
  }
311
617
  async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
@@ -317,28 +623,97 @@ export class SessionManager {
317
623
  WHERE id = ?
318
624
  `).run(agentSessionId, Date.now(), sessionId);
319
625
  }
626
+ async switchAgent(channel, channelId, projectPath, newAgentId) {
627
+ // 1. 取消当前活跃会话
628
+ this.deactivateAllMetadata(channel, channelId);
629
+ // 2. 查找目标 agent 在当前项目下的会话
630
+ const target = this.db.prepare(`
631
+ SELECT * FROM sessions
632
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
633
+ ORDER BY updated_at DESC LIMIT 1
634
+ `).get(channel, channelId, projectPath, newAgentId);
635
+ if (target) {
636
+ const validSessionId = this.validateSessionFile(target);
637
+ // 激活目标会话
638
+ const metadata = target.metadata ? JSON.parse(target.metadata) : {};
639
+ metadata.isActive = true;
640
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
641
+ .run(JSON.stringify(metadata), Date.now(), target.id);
642
+ return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
643
+ }
644
+ // 3. 创建新会话(与 switchProject 保持一致)
645
+ const session = {
646
+ id: `${channel}-${channelId}-${Date.now()}`,
647
+ channel,
648
+ channelId,
649
+ projectPath,
650
+ threadId: '',
651
+ agentId: newAgentId,
652
+ chatType: 'private',
653
+ sessionMode: 'interactive',
654
+ metadata: { isActive: true },
655
+ name: '默认会话',
656
+ createdAt: Date.now(),
657
+ updatedAt: Date.now()
658
+ };
659
+ this.insertSession(session);
660
+ this.eventBus.publish({
661
+ type: 'session:created',
662
+ sessionId: session.id,
663
+ channel,
664
+ channelId,
665
+ projectPath,
666
+ name: session.name,
667
+ timestamp: Date.now()
668
+ });
669
+ return session;
670
+ }
320
671
  async clearActiveSession(channel, channelId) {
321
672
  // 清除当前活跃会话的 Agent Session ID
322
673
  this.db.prepare(`
323
674
  UPDATE sessions
324
675
  SET agent_session_id = NULL, updated_at = ?
325
- WHERE channel = ? AND channel_id = ? AND is_active = 1
676
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
326
677
  `).run(Date.now(), channel, channelId);
327
678
  }
679
+ /** 查找 owner 在目标通道的私聊 channelId(用于跨通道文件投递) */
680
+ getOwnerChatId(targetChannel, ownerPeerId) {
681
+ const row = this.db.prepare(`
682
+ SELECT channel_id FROM sessions
683
+ WHERE channel = ? AND chat_type = 'private'
684
+ AND json_extract(metadata, '$.peerId') = ?
685
+ AND deleted_at IS NULL
686
+ ORDER BY updated_at DESC LIMIT 1
687
+ `).get(targetChannel, ownerPeerId);
688
+ return row?.channel_id;
689
+ }
328
690
  async getActiveSession(channel, channelId) {
329
691
  const row = this.db.prepare(`
330
692
  SELECT * FROM sessions
331
- WHERE channel = ? AND channel_id = ? AND is_active = 1
693
+ WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL
332
694
  `).get(channel, channelId);
333
695
  if (!row)
334
696
  return undefined;
335
697
  return this.rowToSession(row);
336
698
  }
699
+ /**
700
+ * 查询话题会话(不创建)
701
+ */
702
+ async getThreadSession(channel, channelId, threadId) {
703
+ const row = this.db.prepare(`
704
+ SELECT * FROM sessions
705
+ WHERE channel = ? AND channel_id = ? AND thread_id = ? AND deleted_at IS NULL
706
+ `).get(channel, channelId, threadId);
707
+ if (!row)
708
+ return undefined;
709
+ const validSessionId = this.validateSessionFile(row);
710
+ return { ...this.rowToSession(row), agentSessionId: validSessionId };
711
+ }
337
712
  async listSessions(channel, channelId) {
338
713
  // 列出该聊天的所有会话
339
714
  const rows = this.db.prepare(`
340
715
  SELECT * FROM sessions
341
- WHERE channel = ? AND channel_id = ?
716
+ WHERE channel = ? AND channel_id = ? AND deleted_at IS NULL
342
717
  ORDER BY updated_at DESC
343
718
  `).all(channel, channelId);
344
719
  return rows.map(row => this.rowToSession(row));
@@ -346,7 +721,9 @@ export class SessionManager {
346
721
  async getSessionByProjectPath(channel, channelId, projectPath) {
347
722
  const row = this.db.prepare(`
348
723
  SELECT * FROM sessions
349
- WHERE channel = ? AND channel_id = ? AND project_path = ?
724
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND deleted_at IS NULL
725
+ ORDER BY processing_state IS NOT NULL DESC, updated_at DESC
726
+ LIMIT 1
350
727
  `).get(channel, channelId, projectPath);
351
728
  if (!row)
352
729
  return undefined;
@@ -355,7 +732,7 @@ export class SessionManager {
355
732
  async getSessionByName(channel, channelId, name) {
356
733
  const row = this.db.prepare(`
357
734
  SELECT * FROM sessions
358
- WHERE channel = ? AND channel_id = ? AND name = ?
735
+ WHERE channel = ? AND channel_id = ? AND name = ? AND deleted_at IS NULL
359
736
  `).get(channel, channelId, name);
360
737
  if (!row)
361
738
  return undefined;
@@ -364,18 +741,18 @@ export class SessionManager {
364
741
  async switchToSession(channel, channelId, targetSessionId) {
365
742
  // 验证目标会话存在
366
743
  const target = this.db.prepare(`
367
- SELECT * FROM sessions WHERE id = ? AND channel = ? AND channel_id = ?
744
+ SELECT * FROM sessions WHERE id = ? AND channel = ? AND channel_id = ? AND deleted_at IS NULL
368
745
  `).get(targetSessionId, channel, channelId);
369
746
  if (!target)
370
747
  return null;
371
748
  // 取消当前活跃会话
372
- this.deactivateAll(channel, channelId);
749
+ this.deactivateAllMetadata(channel, channelId);
373
750
  // 激活目标会话
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() };
751
+ const metadata = target.metadata ? JSON.parse(target.metadata) : {};
752
+ metadata.isActive = true;
753
+ this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
754
+ .run(JSON.stringify(metadata), Date.now(), targetSessionId);
755
+ return { ...this.rowToSession(target), metadata, updatedAt: Date.now() };
379
756
  }
380
757
  async renameSession(sessionId, newName) {
381
758
  const result = this.db.prepare(`
@@ -389,9 +766,14 @@ export class SessionManager {
389
766
  `).run(sessionId);
390
767
  return result.changes > 0;
391
768
  }
392
- async createNewSession(channel, channelId, projectPath, name) {
769
+ async softDeleteSession(channelId) {
770
+ this.db.prepare(`
771
+ UPDATE sessions SET deleted_at = ?, updated_at = ? WHERE channel_id = ? AND deleted_at IS NULL
772
+ `).run(Date.now(), Date.now(), channelId);
773
+ }
774
+ async createNewSession(channel, channelId, projectPath, name, agentId) {
393
775
  // 取消当前活跃会话
394
- this.deactivateAll(channel, channelId);
776
+ this.deactivateAllMetadata(channel, channelId);
395
777
  // 创建新会话
396
778
  const session = {
397
779
  id: `${channel}-${channelId}-${Date.now()}`,
@@ -399,13 +781,24 @@ export class SessionManager {
399
781
  channelId,
400
782
  projectPath,
401
783
  threadId: '',
402
- agentType: 'claude',
784
+ agentId: agentId || 'claude',
785
+ chatType: 'private',
786
+ sessionMode: 'interactive',
787
+ metadata: { isActive: true },
403
788
  name: name || '默认会话',
404
- isActive: true,
405
789
  createdAt: Date.now(),
406
790
  updatedAt: Date.now()
407
791
  };
408
792
  this.insertSession(session);
793
+ this.eventBus.publish({
794
+ type: 'session:created',
795
+ sessionId: session.id,
796
+ channel,
797
+ channelId,
798
+ projectPath,
799
+ name: session.name,
800
+ timestamp: Date.now()
801
+ });
409
802
  return session;
410
803
  }
411
804
  /**
@@ -413,129 +806,80 @@ export class SessionManager {
413
806
  */
414
807
  async createForkedSession(sourceSession, forkedAgentSessionId, name) {
415
808
  // 取消当前活跃会话
416
- this.deactivateAll(sourceSession.channel, sourceSession.channelId);
809
+ this.deactivateAllMetadata(sourceSession.channel, sourceSession.channelId);
417
810
  const session = {
418
811
  id: `${sourceSession.channel}-${sourceSession.channelId}-${Date.now()}`,
419
812
  channel: sourceSession.channel,
420
813
  channelId: sourceSession.channelId,
421
814
  projectPath: sourceSession.projectPath,
422
815
  threadId: sourceSession.threadId || '',
423
- agentType: sourceSession.agentType || 'claude',
816
+ agentId: sourceSession.agentId || 'claude',
817
+ chatType: sourceSession.chatType || 'private',
818
+ sessionMode: sourceSession.sessionMode || 'interactive',
424
819
  agentSessionId: forkedAgentSessionId,
820
+ metadata: { isActive: true },
425
821
  name: name || `${sourceSession.name || '会话'}-分支`,
426
- isActive: true,
427
822
  createdAt: Date.now(),
428
823
  updatedAt: Date.now()
429
824
  };
430
825
  this.insertSession(session);
826
+ this.eventBus.publish({
827
+ type: 'session:created',
828
+ sessionId: session.id,
829
+ channel: sourceSession.channel,
830
+ channelId: sourceSession.channelId,
831
+ projectPath: sourceSession.projectPath,
832
+ name: session.name,
833
+ timestamp: Date.now()
834
+ });
431
835
  return session;
432
836
  }
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))
837
+ async scanCliSessions(projectPath, agentId) {
838
+ const adapter = this.getFileAdapter(agentId);
839
+ if (!adapter)
438
840
  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))
841
+ return adapter.scanCliSessions(projectPath);
842
+ }
843
+ checkSessionFileExists(projectPath, agentSessionId, agentId) {
844
+ const adapter = this.getFileAdapter(agentId);
845
+ if (!adapter)
846
+ return false;
847
+ return adapter.checkExists(projectPath, agentSessionId);
848
+ }
849
+ readSessionFirstMessage(projectPath, agentSessionId, agentId) {
850
+ const adapter = this.getFileAdapter(agentId);
851
+ if (!adapter)
459
852
  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;
853
+ return adapter.readFirstMessage(projectPath, agentSessionId);
476
854
  }
477
- readSessionLastUserMessage(projectPath, agentSessionId) {
478
- const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
479
- if (!fs.existsSync(sessionFile))
855
+ readSessionLastUserMessage(projectPath, agentSessionId, agentId) {
856
+ const adapter = this.getFileAdapter(agentId);
857
+ if (!adapter)
480
858
  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;
859
+ return adapter.readLastUserMessage(projectPath, agentSessionId);
497
860
  }
498
861
  /**
499
862
  * 获取会话文件信息(回合数 + 标题)
500
863
  */
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);
864
+ getSessionFileInfo(projectPath, agentSessionId, agentId) {
865
+ const adapter = this.getFileAdapter(agentId);
866
+ if (!adapter)
532
867
  return { turns: 0 };
533
- }
868
+ return adapter.getFileInfo(projectPath, agentSessionId);
869
+ }
870
+ /**
871
+ * 列出 SDK 侧的会话列表(用于名称同步)
872
+ */
873
+ async listSdkSessions(projectPath, agentId) {
874
+ const adapter = this.getFileAdapter(agentId);
875
+ if (!adapter?.listSdkSessions)
876
+ return [];
877
+ return adapter.listSdkSessions(projectPath);
534
878
  }
535
879
  async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
536
880
  const rows = this.db.prepare(`
537
881
  SELECT * FROM sessions
538
- WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ?
882
+ WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ? AND deleted_at IS NULL
539
883
  `).all(channel, channelId, `${uuidPrefix}%`);
540
884
  if (rows.length === 0)
541
885
  return undefined;
@@ -544,41 +888,38 @@ export class SessionManager {
544
888
  }
545
889
  return this.rowToSession(rows[0]);
546
890
  }
547
- async importCliSession(channel, channelId, projectPath, agentSessionId) {
548
- // 检查是否已存在相同项目路径的会话
549
- const existingByPath = this.db.prepare(`
550
- SELECT * FROM sessions
551
- WHERE channel = ? AND channel_id = ? AND project_path = ?
552
- `).get(channel, channelId, projectPath);
553
- if (existingByPath) {
554
- // 更新 agent_session_id 并激活
555
- this.db.prepare(`
556
- UPDATE sessions SET is_active = 0, updated_at = ?
557
- WHERE channel = ? AND channel_id = ? AND is_active = 1 AND id != ?
558
- `).run(Date.now(), channel, channelId, existingByPath.id);
559
- this.db.prepare(`
560
- UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
561
- WHERE id = ?
562
- `).run(agentSessionId, Date.now(), existingByPath.id);
563
- return { ...this.rowToSession(existingByPath), agentSessionId, isActive: true, updatedAt: Date.now() };
564
- }
891
+ async importCliSession(channel, channelId, projectPath, agentSessionId, agentId = 'claude') {
565
892
  // 取消当前活跃会话
566
- this.deactivateAll(channel, channelId);
567
- // 创建会话记录
893
+ this.deactivateAllMetadata(channel, channelId);
894
+ // 从 CLI 会话文件读取标题
895
+ const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId, agentId);
896
+ const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
897
+ // 创建新会话记录
568
898
  const session = {
569
899
  id: `${channel}-${channelId}-${Date.now()}`,
570
900
  channel,
571
901
  channelId,
572
902
  projectPath,
573
903
  threadId: '',
574
- agentType: 'claude',
904
+ agentId,
905
+ chatType: 'private',
906
+ sessionMode: 'interactive',
575
907
  agentSessionId,
576
- name: `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
577
- isActive: true,
908
+ metadata: { isActive: true },
909
+ name,
578
910
  createdAt: Date.now(),
579
911
  updatedAt: Date.now()
580
912
  };
581
913
  this.insertSession(session);
914
+ this.eventBus.publish({
915
+ type: 'session:created',
916
+ sessionId: session.id,
917
+ channel,
918
+ channelId,
919
+ projectPath,
920
+ name,
921
+ timestamp: Date.now()
922
+ });
582
923
  return session;
583
924
  }
584
925
  // ==================== 健康状态管理 ====================
@@ -610,6 +951,11 @@ export class SessionManager {
610
951
  lastSuccessTime: row.last_success_time
611
952
  };
612
953
  }
954
+ /** 当前处于安全模式的会话数 */
955
+ getSafeModeSessionCount() {
956
+ const row = this.db.prepare(`SELECT COUNT(*) as count FROM session_health WHERE safe_mode = 1`).get();
957
+ return row?.count ?? 0;
958
+ }
613
959
  /**
614
960
  * 记录成功响应(重置错误计数)
615
961
  */
@@ -673,6 +1019,8 @@ export class SessionManager {
673
1019
  `).run(sessionId, now, now, now, now);
674
1020
  }
675
1021
  close() {
1022
+ for (const adapter of this.fileAdapters.values())
1023
+ adapter.close?.();
676
1024
  this.db.close();
677
1025
  }
678
1026
  }