evolclaw 2.0.7 → 2.1.1

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.
@@ -30,7 +30,10 @@ export class SessionManager {
30
30
  channel: row.channel,
31
31
  channelId: row.channel_id,
32
32
  projectPath: row.project_path,
33
- claudeSessionId: row.claude_session_id,
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,
34
37
  name: row.name,
35
38
  isActive: row.is_active === 1,
36
39
  createdAt: row.created_at,
@@ -44,21 +47,21 @@ export class SessionManager {
44
47
  `).run(Date.now(), channel, channelId);
45
48
  }
46
49
  validateSessionFile(row) {
47
- const claudeSessionId = row.claude_session_id;
48
- if (!claudeSessionId)
50
+ const agentSessionId = row.agent_session_id;
51
+ if (!agentSessionId)
49
52
  return undefined;
50
- const sessionFile = this.getSessionFilePath(row.project_path, claudeSessionId);
53
+ const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
51
54
  if (fs.existsSync(sessionFile))
52
- return claudeSessionId;
55
+ return agentSessionId;
53
56
  logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
54
- this.db.prepare(`UPDATE sessions SET claude_session_id = NULL WHERE id = ?`).run(row.id);
57
+ this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
55
58
  return undefined;
56
59
  }
57
60
  insertSession(session) {
58
61
  this.db.prepare(`
59
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
60
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
61
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, session.isActive ? 1 : 0, session.createdAt, session.updatedAt);
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);
62
65
  }
63
66
  extractUserMessageText(messageContent) {
64
67
  if (typeof messageContent === 'string') {
@@ -78,55 +81,16 @@ export class SessionManager {
78
81
  const tableInfo = this.db.prepare('PRAGMA table_info(sessions)').all();
79
82
  const hasIsActive = tableInfo.some((col) => col.name === 'is_active');
80
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');
81
88
  // 检查是否有唯一约束
82
89
  const indexes = this.db.prepare('PRAGMA index_list(sessions)').all();
83
90
  const hasUniqueConstraint = indexes.some((idx) => idx.origin === 'u');
84
- if (!hasIsActive && tableInfo.length > 0) {
85
- logger.info('Migrating database schema (removing unique constraint)...');
86
- this.db.exec(`
87
- CREATE TABLE sessions_new (
88
- id TEXT PRIMARY KEY,
89
- channel TEXT NOT NULL,
90
- channel_id TEXT NOT NULL,
91
- project_path TEXT NOT NULL,
92
- claude_session_id TEXT,
93
- name TEXT,
94
- is_active INTEGER NOT NULL DEFAULT 0,
95
- created_at INTEGER NOT NULL,
96
- updated_at INTEGER NOT NULL
97
- );
98
- INSERT INTO sessions_new SELECT id, channel, channel_id, project_path, claude_session_id, NULL, 1, created_at, updated_at FROM sessions;
99
- DROP TABLE sessions;
100
- ALTER TABLE sessions_new RENAME TO sessions;
101
- `);
102
- logger.info('✓ Database migration completed');
103
- }
104
- else if (!hasName && tableInfo.length > 0) {
105
- logger.info('Adding name column...');
106
- this.db.exec(`ALTER TABLE sessions ADD COLUMN name TEXT`);
107
- if (hasUniqueConstraint) {
108
- logger.info('Removing unique constraint...');
109
- this.db.exec(`
110
- CREATE TABLE sessions_new (
111
- id TEXT PRIMARY KEY,
112
- channel TEXT NOT NULL,
113
- channel_id TEXT NOT NULL,
114
- project_path TEXT NOT NULL,
115
- claude_session_id TEXT,
116
- name TEXT,
117
- is_active INTEGER NOT NULL DEFAULT 0,
118
- created_at INTEGER NOT NULL,
119
- updated_at INTEGER NOT NULL
120
- );
121
- INSERT INTO sessions_new SELECT * FROM sessions;
122
- DROP TABLE sessions;
123
- ALTER TABLE sessions_new RENAME TO sessions;
124
- `);
125
- }
126
- logger.info('✓ Schema updated');
127
- }
128
- else if (hasUniqueConstraint) {
129
- logger.info('Removing stale unique constraint...');
91
+ // 迁移到新表结构(添加 thread_id, agent_type, agent_session_id, metadata)
92
+ if (!hasThreadId && tableInfo.length > 0) {
93
+ logger.info('Migrating database schema (adding thread support)...');
130
94
  this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
131
95
  this.db.exec(`
132
96
  CREATE TABLE sessions_new (
@@ -134,34 +98,51 @@ export class SessionManager {
134
98
  channel TEXT NOT NULL,
135
99
  channel_id TEXT NOT NULL,
136
100
  project_path TEXT NOT NULL,
137
- claude_session_id TEXT,
101
+ thread_id TEXT NOT NULL DEFAULT '',
102
+ agent_type TEXT NOT NULL DEFAULT 'claude',
103
+ agent_session_id TEXT,
138
104
  name TEXT,
139
105
  is_active INTEGER NOT NULL DEFAULT 0,
140
106
  created_at INTEGER NOT NULL,
141
- updated_at INTEGER NOT NULL
107
+ updated_at INTEGER NOT NULL,
108
+ metadata TEXT
142
109
  );
143
- INSERT INTO sessions_new (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
144
- SELECT id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at FROM sessions;
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;
145
112
  DROP TABLE sessions;
146
113
  ALTER TABLE sessions_new RENAME TO sessions;
147
114
  `);
148
- logger.info('✓ Unique constraint removed');
149
- }
150
- else {
115
+ // 话题会话唯一约束(thread_id 非空时才生效)
151
116
  this.db.exec(`
152
- CREATE TABLE IF NOT EXISTS sessions (
153
- id TEXT PRIMARY KEY,
154
- channel TEXT NOT NULL,
155
- channel_id TEXT NOT NULL,
156
- project_path TEXT NOT NULL,
157
- claude_session_id TEXT,
158
- name TEXT,
159
- is_active INTEGER NOT NULL DEFAULT 0,
160
- created_at INTEGER NOT NULL,
161
- updated_at INTEGER NOT NULL
162
- )
117
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
118
+ ON sessions(channel, channel_id, project_path, thread_id)
119
+ WHERE thread_id != ''
163
120
  `);
121
+ logger.info('✓ Database migration completed (thread support added)');
164
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
+ `);
165
146
  // 创建消息去重表
166
147
  this.db.exec(`
167
148
  CREATE TABLE IF NOT EXISTS processed_messages (
@@ -188,58 +169,46 @@ export class SessionManager {
188
169
  CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
189
170
  `);
190
171
  }
191
- async getOrCreateSession(channel, channelId, defaultProjectPath, name) {
192
- // 1. 查找该聊天的活跃会话
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
+ // 主会话:查找活跃会话
193
178
  const active = this.db.prepare(`
194
179
  SELECT * FROM sessions
195
- WHERE channel = ? AND channel_id = ? AND is_active = 1
180
+ WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
196
181
  `).get(channel, channelId);
197
182
  if (active) {
198
183
  const validSessionId = this.validateSessionFile(active);
199
- return { ...this.rowToSession(active), claudeSessionId: validSessionId };
184
+ return { ...this.rowToSession(active), agentSessionId: validSessionId };
200
185
  }
201
- // 2. 没有活跃会话,查找该聊天在默认项目的会话
186
+ // 查找默认项目的主会话
202
187
  const existing = this.db.prepare(`
203
188
  SELECT * FROM sessions
204
- WHERE channel = ? AND channel_id = ? AND project_path = ?
189
+ WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ''
205
190
  ORDER BY updated_at DESC LIMIT 1
206
191
  `).get(channel, channelId, defaultProjectPath);
207
192
  if (existing) {
208
193
  const validSessionId = this.validateSessionFile(existing);
209
- // 激活该会话
210
- this.db.prepare(`
211
- UPDATE sessions SET is_active = 1, updated_at = ?
212
- WHERE id = ?
213
- `).run(Date.now(), existing.id);
214
- return { ...this.rowToSession(existing), claudeSessionId: validSessionId, isActive: true };
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 };
215
196
  }
216
- // 3. 创建新会话(默认为活跃)
197
+ // 创建新主会话
217
198
  const session = {
218
199
  id: `${channel}-${channelId}-${Date.now()}`,
219
200
  channel,
220
201
  channelId,
221
202
  projectPath: defaultProjectPath,
203
+ threadId: '',
204
+ agentType: 'claude',
205
+ metadata,
222
206
  name: name || '默认会话',
223
207
  isActive: true,
224
208
  createdAt: Date.now(),
225
209
  updatedAt: Date.now()
226
210
  };
227
- // 使用 INSERT OR IGNORE 避免并发时的 UNIQUE 约束冲突
228
- const result = this.db.prepare(`
229
- INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, claude_session_id, name, is_active, created_at, updated_at)
230
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
231
- `).run(session.id, session.channel, session.channelId, session.projectPath, session.claudeSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt);
232
- // 如果插入被忽略(已存在),重新查询
233
- if (result.changes === 0) {
234
- const recheck = this.db.prepare(`
235
- SELECT * FROM sessions
236
- WHERE channel = ? AND channel_id = ? AND project_path = ?
237
- `).get(channel, channelId, defaultProjectPath);
238
- if (recheck) {
239
- this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
240
- return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
241
- }
242
- }
211
+ this.insertSession(session);
243
212
  return session;
244
213
  }
245
214
  async updateSession(sessionId, updates) {
@@ -250,10 +219,53 @@ export class SessionManager {
250
219
  return null;
251
220
  if (typeof v === 'boolean')
252
221
  return v ? 1 : 0;
222
+ if (typeof v === 'object' && v !== null)
223
+ return JSON.stringify(v);
253
224
  return v;
254
225
  });
255
226
  this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
256
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
+ }
257
269
  async switchProject(channel, channelId, newProjectPath) {
258
270
  // 1. 取消当前活跃会话
259
271
  this.deactivateAll(channel, channelId);
@@ -270,7 +282,7 @@ export class SessionManager {
270
282
  UPDATE sessions SET is_active = 1, updated_at = ?
271
283
  WHERE id = ?
272
284
  `).run(Date.now(), target.id);
273
- return { ...this.rowToSession(target), claudeSessionId: validSessionId, isActive: true };
285
+ return { ...this.rowToSession(target), agentSessionId: validSessionId, isActive: true };
274
286
  }
275
287
  // 3. 创建新会话
276
288
  const session = {
@@ -278,6 +290,8 @@ export class SessionManager {
278
290
  channel,
279
291
  channelId,
280
292
  projectPath: newProjectPath,
293
+ threadId: '',
294
+ agentType: 'claude',
281
295
  name: '默认会话',
282
296
  isActive: true,
283
297
  createdAt: Date.now(),
@@ -286,28 +300,28 @@ export class SessionManager {
286
300
  this.insertSession(session);
287
301
  return session;
288
302
  }
289
- async updateClaudeSessionId(channel, channelId, claudeSessionId) {
290
- // 只更新当前活跃会话的 Claude Session ID
303
+ async updateAgentSessionId(channel, channelId, agentSessionId) {
304
+ // 只更新当前活跃会话的 Agent Session ID
291
305
  this.db.prepare(`
292
306
  UPDATE sessions
293
- SET claude_session_id = ?, updated_at = ?
307
+ SET agent_session_id = ?, updated_at = ?
294
308
  WHERE channel = ? AND channel_id = ? AND is_active = 1
295
- `).run(claudeSessionId, Date.now(), channel, channelId);
309
+ `).run(agentSessionId, Date.now(), channel, channelId);
296
310
  }
297
- async updateClaudeSessionIdBySessionId(sessionId, claudeSessionId) {
311
+ async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
298
312
  // 根据 sessionId 直接更新
299
- logger.info(`[SessionManager] Updating claude_session_id: sessionId=${sessionId}, claudeSessionId=${claudeSessionId}`);
313
+ logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
300
314
  this.db.prepare(`
301
315
  UPDATE sessions
302
- SET claude_session_id = ?, updated_at = ?
316
+ SET agent_session_id = ?, updated_at = ?
303
317
  WHERE id = ?
304
- `).run(claudeSessionId, Date.now(), sessionId);
318
+ `).run(agentSessionId, Date.now(), sessionId);
305
319
  }
306
320
  async clearActiveSession(channel, channelId) {
307
- // 清除当前活跃会话的 Claude Session ID
321
+ // 清除当前活跃会话的 Agent Session ID
308
322
  this.db.prepare(`
309
323
  UPDATE sessions
310
- SET claude_session_id = NULL, updated_at = ?
324
+ SET agent_session_id = NULL, updated_at = ?
311
325
  WHERE channel = ? AND channel_id = ? AND is_active = 1
312
326
  `).run(Date.now(), channel, channelId);
313
327
  }
@@ -369,6 +383,12 @@ export class SessionManager {
369
383
  `).run(newName, Date.now(), sessionId);
370
384
  return result.changes > 0;
371
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
+ }
372
392
  async createNewSession(channel, channelId, projectPath, name) {
373
393
  // 取消当前活跃会话
374
394
  this.deactivateAll(channel, channelId);
@@ -378,6 +398,8 @@ export class SessionManager {
378
398
  channel,
379
399
  channelId,
380
400
  projectPath,
401
+ threadId: '',
402
+ agentType: 'claude',
381
403
  name: name || '默认会话',
382
404
  isActive: true,
383
405
  createdAt: Date.now(),
@@ -389,7 +411,7 @@ export class SessionManager {
389
411
  /**
390
412
  * 基于现有会话创建分支会话
391
413
  */
392
- async createForkedSession(sourceSession, forkedClaudeSessionId, name) {
414
+ async createForkedSession(sourceSession, forkedAgentSessionId, name) {
393
415
  // 取消当前活跃会话
394
416
  this.deactivateAll(sourceSession.channel, sourceSession.channelId);
395
417
  const session = {
@@ -397,7 +419,9 @@ export class SessionManager {
397
419
  channel: sourceSession.channel,
398
420
  channelId: sourceSession.channelId,
399
421
  projectPath: sourceSession.projectPath,
400
- claudeSessionId: forkedClaudeSessionId,
422
+ threadId: sourceSession.threadId || '',
423
+ agentType: sourceSession.agentType || 'claude',
424
+ agentSessionId: forkedAgentSessionId,
401
425
  name: name || `${sourceSession.name || '会话'}-分支`,
402
426
  isActive: true,
403
427
  createdAt: Date.now(),
@@ -425,12 +449,12 @@ export class SessionManager {
425
449
  .slice(0, 10);
426
450
  return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
427
451
  }
428
- checkSessionFileExists(projectPath, claudeSessionId) {
429
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
452
+ checkSessionFileExists(projectPath, agentSessionId) {
453
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
430
454
  return fs.existsSync(sessionFile);
431
455
  }
432
- readSessionFirstMessage(projectPath, claudeSessionId) {
433
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
456
+ readSessionFirstMessage(projectPath, agentSessionId) {
457
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
434
458
  if (!fs.existsSync(sessionFile))
435
459
  return null;
436
460
  try {
@@ -450,8 +474,8 @@ export class SessionManager {
450
474
  }
451
475
  return null;
452
476
  }
453
- readSessionLastUserMessage(projectPath, claudeSessionId) {
454
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
477
+ readSessionLastUserMessage(projectPath, agentSessionId) {
478
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
455
479
  if (!fs.existsSync(sessionFile))
456
480
  return null;
457
481
  try {
@@ -474,8 +498,8 @@ export class SessionManager {
474
498
  /**
475
499
  * 获取会话文件信息(回合数 + 标题)
476
500
  */
477
- getSessionFileInfo(projectPath, claudeSessionId) {
478
- const sessionFile = this.getSessionFilePath(projectPath, claudeSessionId);
501
+ getSessionFileInfo(projectPath, agentSessionId) {
502
+ const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
479
503
  if (!fs.existsSync(sessionFile))
480
504
  return { turns: 0 };
481
505
  try {
@@ -511,7 +535,7 @@ export class SessionManager {
511
535
  async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
512
536
  const rows = this.db.prepare(`
513
537
  SELECT * FROM sessions
514
- WHERE channel = ? AND channel_id = ? AND claude_session_id LIKE ?
538
+ WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ?
515
539
  `).all(channel, channelId, `${uuidPrefix}%`);
516
540
  if (rows.length === 0)
517
541
  return undefined;
@@ -520,23 +544,23 @@ export class SessionManager {
520
544
  }
521
545
  return this.rowToSession(rows[0]);
522
546
  }
523
- async importCliSession(channel, channelId, projectPath, claudeSessionId) {
547
+ async importCliSession(channel, channelId, projectPath, agentSessionId) {
524
548
  // 检查是否已存在相同项目路径的会话
525
549
  const existingByPath = this.db.prepare(`
526
550
  SELECT * FROM sessions
527
551
  WHERE channel = ? AND channel_id = ? AND project_path = ?
528
552
  `).get(channel, channelId, projectPath);
529
553
  if (existingByPath) {
530
- // 更新 claude_session_id 并激活
554
+ // 更新 agent_session_id 并激活
531
555
  this.db.prepare(`
532
556
  UPDATE sessions SET is_active = 0, updated_at = ?
533
557
  WHERE channel = ? AND channel_id = ? AND is_active = 1 AND id != ?
534
558
  `).run(Date.now(), channel, channelId, existingByPath.id);
535
559
  this.db.prepare(`
536
- UPDATE sessions SET claude_session_id = ?, is_active = 1, updated_at = ?
560
+ UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
537
561
  WHERE id = ?
538
- `).run(claudeSessionId, Date.now(), existingByPath.id);
539
- return { ...this.rowToSession(existingByPath), claudeSessionId, isActive: true, updatedAt: Date.now() };
562
+ `).run(agentSessionId, Date.now(), existingByPath.id);
563
+ return { ...this.rowToSession(existingByPath), agentSessionId, isActive: true, updatedAt: Date.now() };
540
564
  }
541
565
  // 取消当前活跃会话
542
566
  this.deactivateAll(channel, channelId);
@@ -546,7 +570,9 @@ export class SessionManager {
546
570
  channel,
547
571
  channelId,
548
572
  projectPath,
549
- claudeSessionId,
573
+ threadId: '',
574
+ agentType: 'claude',
575
+ agentSessionId,
550
576
  name: `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
551
577
  isActive: true,
552
578
  createdAt: Date.now(),
package/dist/index.js CHANGED
@@ -43,9 +43,12 @@ async function main() {
43
43
  const sessionManager = new SessionManager();
44
44
  logger.info('✓ Database initialized');
45
45
  // 初始化 Agent Runner(带持久化回调)
46
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, claudeSessionId) => {
47
- await sessionManager.updateClaudeSessionIdBySessionId(sessionId, claudeSessionId);
46
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
47
+ await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
48
48
  }, anthropic.baseUrl, config);
49
+ if (anthropic.effort) {
50
+ agentRunner.setEffort(anthropic.effort);
51
+ }
49
52
  logger.info('✓ Agent runner ready');
50
53
  // 创建消息缓存
51
54
  const messageCache = new MessageCache();
@@ -78,8 +81,8 @@ async function main() {
78
81
  // 创建命令处理器
79
82
  const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
80
83
  // 创建消息处理器
81
- const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId) => {
82
- const sendFn = async (id, text) => {
84
+ const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
85
+ const sendFn = async (id, text, opts) => {
83
86
  const adapter = cmdHandler.getAdapter(channel);
84
87
  if (!adapter)
85
88
  return;
@@ -105,10 +108,10 @@ async function main() {
105
108
  text = text.replace(fileMarkerPattern, '').trim();
106
109
  }
107
110
  if (text) {
108
- await adapter.sendText(id, text);
111
+ await adapter.sendText(id, text, opts);
109
112
  }
110
113
  };
111
- return cmdHandler.handle(content, channel, channelId, sendFn, userId);
114
+ return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
112
115
  });
113
116
  // 回填 processor 和 messageQueue 的引用
114
117
  cmdHandler.setProcessor(processor);
@@ -223,8 +226,8 @@ async function main() {
223
226
  }
224
227
  // Feishu 消息处理
225
228
  if (feishu) {
226
- feishu.onMessage(async (chatId, content, images, userId, userName, messageId, mentions) => {
227
- content = content.trim();
229
+ feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
230
+ let content = rawContent.trim();
228
231
  // 首次交互自动绑定主人
229
232
  if (userId && !config.channels?.feishu?.owner) {
230
233
  const { setOwner } = await import('./config.js');
@@ -233,11 +236,11 @@ async function main() {
233
236
  }
234
237
  // 命令立即处理,不进入队列
235
238
  if (cmdHandler.isCommand(content)) {
236
- const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
239
+ const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
237
240
  if (cmdResult !== null) {
238
241
  if (cmdResult) {
239
242
  try {
240
- await feishu.sendMessage(chatId, cmdResult, { forceText: true });
243
+ await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
241
244
  }
242
245
  catch (error) {
243
246
  logger.error('[Feishu] Failed to send command response:', error);
@@ -246,15 +249,16 @@ async function main() {
246
249
  return;
247
250
  }
248
251
  }
249
- // 获取当前项目路径
250
- const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd());
252
+ // 获取当前项目路径(话题会话自动创建,携带 metadata)
253
+ const metadata = rootId ? { feishu: { rootId } } : undefined;
254
+ const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
251
255
  // 群聊消息添加用户名前缀
252
256
  const chatMode = await feishu.getChatMode(chatId);
253
257
  if (chatMode === 'group' && userName) {
254
258
  content = `[${userName}] ${content}`;
255
259
  }
256
- // 普通消息进入队列
257
- await messageQueue.enqueue(`feishu-${chatId}`, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions }, session.projectPath);
260
+ // 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
261
+ await messageQueue.enqueue(session.id, { channel: 'feishu', channelId: chatId, content, images, timestamp: Date.now(), userId, userName, messageId, isGroup: chatMode === 'group', mentions, threadId }, session.projectPath);
258
262
  });
259
263
  }
260
264
  // AUN 消息处理