evolclaw 2.0.7 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/evolclaw.sample.json +3 -2
- package/dist/channels/feishu.js +32 -14
- package/dist/cli.js +20 -8
- package/dist/core/agent-runner.js +24 -21
- package/dist/core/command-handler.js +81 -39
- package/dist/core/message-processor.js +82 -48
- package/dist/core/session-manager.js +161 -113
- package/dist/index.js +13 -12
- package/dist/utils/session-file-health.js +4 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
48
|
-
if (!
|
|
50
|
+
const agentSessionId = row.agent_session_id;
|
|
51
|
+
if (!agentSessionId)
|
|
49
52
|
return undefined;
|
|
50
|
-
const sessionFile = this.getSessionFilePath(row.project_path,
|
|
53
|
+
const sessionFile = this.getSessionFilePath(row.project_path, agentSessionId);
|
|
51
54
|
if (fs.existsSync(sessionFile))
|
|
52
|
-
return
|
|
55
|
+
return agentSessionId;
|
|
53
56
|
logger.warn(`Session file not found: ${sessionFile}, clearing session ID`);
|
|
54
|
-
this.db.prepare(`UPDATE sessions SET
|
|
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,
|
|
60
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
61
|
-
`).run(session.id, session.channel, session.channelId, session.projectPath, session.
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
115
|
+
// 话题会话唯一约束(thread_id 非空时才生效)
|
|
151
116
|
this.db.exec(`
|
|
152
|
-
CREATE
|
|
153
|
-
|
|
154
|
-
|
|
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,22 +169,28 @@ 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) {
|
|
172
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name) {
|
|
173
|
+
const normalizedThreadId = threadId || '';
|
|
174
|
+
// 话题会话:直接按 thread_id 查找/创建
|
|
175
|
+
if (normalizedThreadId) {
|
|
176
|
+
return this.getOrCreateThreadSession(channel, channelId, normalizedThreadId, defaultProjectPath, metadata, name);
|
|
177
|
+
}
|
|
178
|
+
// 主会话:原有逻辑
|
|
192
179
|
// 1. 查找该聊天的活跃会话
|
|
193
180
|
const active = this.db.prepare(`
|
|
194
181
|
SELECT * FROM sessions
|
|
195
|
-
WHERE channel = ? AND channel_id = ? AND is_active = 1
|
|
182
|
+
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
|
|
196
183
|
`).get(channel, channelId);
|
|
197
184
|
if (active) {
|
|
198
185
|
const validSessionId = this.validateSessionFile(active);
|
|
199
|
-
return { ...this.rowToSession(active),
|
|
186
|
+
return { ...this.rowToSession(active), agentSessionId: validSessionId };
|
|
200
187
|
}
|
|
201
|
-
// 2.
|
|
188
|
+
// 2. 没有活跃会话,查找该聊天在默认项目的会话(匹配 thread_id)
|
|
202
189
|
const existing = this.db.prepare(`
|
|
203
190
|
SELECT * FROM sessions
|
|
204
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ?
|
|
191
|
+
WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
|
|
205
192
|
ORDER BY updated_at DESC LIMIT 1
|
|
206
|
-
`).get(channel, channelId, defaultProjectPath);
|
|
193
|
+
`).get(channel, channelId, defaultProjectPath, normalizedThreadId);
|
|
207
194
|
if (existing) {
|
|
208
195
|
const validSessionId = this.validateSessionFile(existing);
|
|
209
196
|
// 激活该会话
|
|
@@ -211,7 +198,7 @@ export class SessionManager {
|
|
|
211
198
|
UPDATE sessions SET is_active = 1, updated_at = ?
|
|
212
199
|
WHERE id = ?
|
|
213
200
|
`).run(Date.now(), existing.id);
|
|
214
|
-
return { ...this.rowToSession(existing),
|
|
201
|
+
return { ...this.rowToSession(existing), agentSessionId: validSessionId, isActive: true };
|
|
215
202
|
}
|
|
216
203
|
// 3. 创建新会话(默认为活跃)
|
|
217
204
|
const session = {
|
|
@@ -219,6 +206,9 @@ export class SessionManager {
|
|
|
219
206
|
channel,
|
|
220
207
|
channelId,
|
|
221
208
|
projectPath: defaultProjectPath,
|
|
209
|
+
threadId: normalizedThreadId,
|
|
210
|
+
agentType: 'claude',
|
|
211
|
+
metadata,
|
|
222
212
|
name: name || '默认会话',
|
|
223
213
|
isActive: true,
|
|
224
214
|
createdAt: Date.now(),
|
|
@@ -226,15 +216,15 @@ export class SessionManager {
|
|
|
226
216
|
};
|
|
227
217
|
// 使用 INSERT OR IGNORE 避免并发时的 UNIQUE 约束冲突
|
|
228
218
|
const result = this.db.prepare(`
|
|
229
|
-
INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path,
|
|
230
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
231
|
-
`).run(session.id, session.channel, session.channelId, session.projectPath, session.
|
|
219
|
+
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)
|
|
220
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
221
|
+
`).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId, session.agentType, session.agentSessionId ?? null, session.name ?? null, 1, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
|
|
232
222
|
// 如果插入被忽略(已存在),重新查询
|
|
233
223
|
if (result.changes === 0) {
|
|
234
224
|
const recheck = this.db.prepare(`
|
|
235
225
|
SELECT * FROM sessions
|
|
236
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ?
|
|
237
|
-
`).get(channel, channelId, defaultProjectPath);
|
|
226
|
+
WHERE channel = ? AND channel_id = ? AND project_path = ? AND thread_id = ?
|
|
227
|
+
`).get(channel, channelId, defaultProjectPath, normalizedThreadId);
|
|
238
228
|
if (recheck) {
|
|
239
229
|
this.db.prepare(`UPDATE sessions SET is_active = 1, updated_at = ? WHERE id = ?`).run(Date.now(), recheck.id);
|
|
240
230
|
return { ...this.rowToSession(recheck), isActive: true, updatedAt: Date.now() };
|
|
@@ -250,10 +240,60 @@ export class SessionManager {
|
|
|
250
240
|
return null;
|
|
251
241
|
if (typeof v === 'boolean')
|
|
252
242
|
return v ? 1 : 0;
|
|
243
|
+
if (typeof v === 'object' && v !== null)
|
|
244
|
+
return JSON.stringify(v);
|
|
253
245
|
return v;
|
|
254
246
|
});
|
|
255
247
|
this.db.prepare(`UPDATE sessions SET ${fields}, updated_at = ? WHERE id = ?`).run(...values, Date.now(), sessionId);
|
|
256
248
|
}
|
|
249
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name) {
|
|
250
|
+
const existing = this.db.prepare(`
|
|
251
|
+
SELECT * FROM sessions
|
|
252
|
+
WHERE channel = ? AND channel_id = ? AND thread_id = ?
|
|
253
|
+
LIMIT 1
|
|
254
|
+
`).get(channel, channelId, threadId);
|
|
255
|
+
if (existing) {
|
|
256
|
+
const validSessionId = this.validateSessionFile(existing);
|
|
257
|
+
const existingMeta = this.rowToSession(existing).metadata;
|
|
258
|
+
if (metadata) {
|
|
259
|
+
const merged = existingMeta ? { ...existingMeta, ...metadata } : metadata;
|
|
260
|
+
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
261
|
+
.run(JSON.stringify(merged), Date.now(), existing.id);
|
|
262
|
+
return { ...this.rowToSession(existing), agentSessionId: validSessionId, metadata: merged };
|
|
263
|
+
}
|
|
264
|
+
return { ...this.rowToSession(existing), agentSessionId: validSessionId };
|
|
265
|
+
}
|
|
266
|
+
const activeSession = this.db.prepare(`
|
|
267
|
+
SELECT project_path FROM sessions
|
|
268
|
+
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND thread_id = ''
|
|
269
|
+
LIMIT 1
|
|
270
|
+
`).get(channel, channelId);
|
|
271
|
+
const projectPath = activeSession?.project_path || defaultProjectPath;
|
|
272
|
+
const session = {
|
|
273
|
+
id: `${channel}-${channelId}-${Date.now()}`,
|
|
274
|
+
channel,
|
|
275
|
+
channelId,
|
|
276
|
+
projectPath,
|
|
277
|
+
threadId,
|
|
278
|
+
agentType: 'claude',
|
|
279
|
+
metadata,
|
|
280
|
+
name: name || '话题会话',
|
|
281
|
+
isActive: false,
|
|
282
|
+
createdAt: Date.now(),
|
|
283
|
+
updatedAt: Date.now()
|
|
284
|
+
};
|
|
285
|
+
this.insertSession(session);
|
|
286
|
+
// Race condition 保护
|
|
287
|
+
const recheck = this.db.prepare(`
|
|
288
|
+
SELECT * FROM sessions
|
|
289
|
+
WHERE channel = ? AND channel_id = ? AND thread_id = ?
|
|
290
|
+
LIMIT 1
|
|
291
|
+
`).get(channel, channelId, threadId);
|
|
292
|
+
if (recheck && recheck.id !== session.id) {
|
|
293
|
+
return this.rowToSession(recheck);
|
|
294
|
+
}
|
|
295
|
+
return session;
|
|
296
|
+
}
|
|
257
297
|
async switchProject(channel, channelId, newProjectPath) {
|
|
258
298
|
// 1. 取消当前活跃会话
|
|
259
299
|
this.deactivateAll(channel, channelId);
|
|
@@ -270,7 +310,7 @@ export class SessionManager {
|
|
|
270
310
|
UPDATE sessions SET is_active = 1, updated_at = ?
|
|
271
311
|
WHERE id = ?
|
|
272
312
|
`).run(Date.now(), target.id);
|
|
273
|
-
return { ...this.rowToSession(target),
|
|
313
|
+
return { ...this.rowToSession(target), agentSessionId: validSessionId, isActive: true };
|
|
274
314
|
}
|
|
275
315
|
// 3. 创建新会话
|
|
276
316
|
const session = {
|
|
@@ -278,6 +318,8 @@ export class SessionManager {
|
|
|
278
318
|
channel,
|
|
279
319
|
channelId,
|
|
280
320
|
projectPath: newProjectPath,
|
|
321
|
+
threadId: '',
|
|
322
|
+
agentType: 'claude',
|
|
281
323
|
name: '默认会话',
|
|
282
324
|
isActive: true,
|
|
283
325
|
createdAt: Date.now(),
|
|
@@ -286,28 +328,28 @@ export class SessionManager {
|
|
|
286
328
|
this.insertSession(session);
|
|
287
329
|
return session;
|
|
288
330
|
}
|
|
289
|
-
async
|
|
290
|
-
// 只更新当前活跃会话的
|
|
331
|
+
async updateAgentSessionId(channel, channelId, agentSessionId) {
|
|
332
|
+
// 只更新当前活跃会话的 Agent Session ID
|
|
291
333
|
this.db.prepare(`
|
|
292
334
|
UPDATE sessions
|
|
293
|
-
SET
|
|
335
|
+
SET agent_session_id = ?, updated_at = ?
|
|
294
336
|
WHERE channel = ? AND channel_id = ? AND is_active = 1
|
|
295
|
-
`).run(
|
|
337
|
+
`).run(agentSessionId, Date.now(), channel, channelId);
|
|
296
338
|
}
|
|
297
|
-
async
|
|
339
|
+
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
298
340
|
// 根据 sessionId 直接更新
|
|
299
|
-
logger.info(`[SessionManager] Updating
|
|
341
|
+
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
300
342
|
this.db.prepare(`
|
|
301
343
|
UPDATE sessions
|
|
302
|
-
SET
|
|
344
|
+
SET agent_session_id = ?, updated_at = ?
|
|
303
345
|
WHERE id = ?
|
|
304
|
-
`).run(
|
|
346
|
+
`).run(agentSessionId, Date.now(), sessionId);
|
|
305
347
|
}
|
|
306
348
|
async clearActiveSession(channel, channelId) {
|
|
307
|
-
// 清除当前活跃会话的
|
|
349
|
+
// 清除当前活跃会话的 Agent Session ID
|
|
308
350
|
this.db.prepare(`
|
|
309
351
|
UPDATE sessions
|
|
310
|
-
SET
|
|
352
|
+
SET agent_session_id = NULL, updated_at = ?
|
|
311
353
|
WHERE channel = ? AND channel_id = ? AND is_active = 1
|
|
312
354
|
`).run(Date.now(), channel, channelId);
|
|
313
355
|
}
|
|
@@ -378,6 +420,8 @@ export class SessionManager {
|
|
|
378
420
|
channel,
|
|
379
421
|
channelId,
|
|
380
422
|
projectPath,
|
|
423
|
+
threadId: '',
|
|
424
|
+
agentType: 'claude',
|
|
381
425
|
name: name || '默认会话',
|
|
382
426
|
isActive: true,
|
|
383
427
|
createdAt: Date.now(),
|
|
@@ -389,7 +433,7 @@ export class SessionManager {
|
|
|
389
433
|
/**
|
|
390
434
|
* 基于现有会话创建分支会话
|
|
391
435
|
*/
|
|
392
|
-
async createForkedSession(sourceSession,
|
|
436
|
+
async createForkedSession(sourceSession, forkedAgentSessionId, name) {
|
|
393
437
|
// 取消当前活跃会话
|
|
394
438
|
this.deactivateAll(sourceSession.channel, sourceSession.channelId);
|
|
395
439
|
const session = {
|
|
@@ -397,7 +441,9 @@ export class SessionManager {
|
|
|
397
441
|
channel: sourceSession.channel,
|
|
398
442
|
channelId: sourceSession.channelId,
|
|
399
443
|
projectPath: sourceSession.projectPath,
|
|
400
|
-
|
|
444
|
+
threadId: sourceSession.threadId || '',
|
|
445
|
+
agentType: sourceSession.agentType || 'claude',
|
|
446
|
+
agentSessionId: forkedAgentSessionId,
|
|
401
447
|
name: name || `${sourceSession.name || '会话'}-分支`,
|
|
402
448
|
isActive: true,
|
|
403
449
|
createdAt: Date.now(),
|
|
@@ -425,12 +471,12 @@ export class SessionManager {
|
|
|
425
471
|
.slice(0, 10);
|
|
426
472
|
return files.map(f => ({ uuid: f.uuid, mtime: f.mtime }));
|
|
427
473
|
}
|
|
428
|
-
checkSessionFileExists(projectPath,
|
|
429
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
474
|
+
checkSessionFileExists(projectPath, agentSessionId) {
|
|
475
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
430
476
|
return fs.existsSync(sessionFile);
|
|
431
477
|
}
|
|
432
|
-
readSessionFirstMessage(projectPath,
|
|
433
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
478
|
+
readSessionFirstMessage(projectPath, agentSessionId) {
|
|
479
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
434
480
|
if (!fs.existsSync(sessionFile))
|
|
435
481
|
return null;
|
|
436
482
|
try {
|
|
@@ -450,8 +496,8 @@ export class SessionManager {
|
|
|
450
496
|
}
|
|
451
497
|
return null;
|
|
452
498
|
}
|
|
453
|
-
readSessionLastUserMessage(projectPath,
|
|
454
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
499
|
+
readSessionLastUserMessage(projectPath, agentSessionId) {
|
|
500
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
455
501
|
if (!fs.existsSync(sessionFile))
|
|
456
502
|
return null;
|
|
457
503
|
try {
|
|
@@ -474,8 +520,8 @@ export class SessionManager {
|
|
|
474
520
|
/**
|
|
475
521
|
* 获取会话文件信息(回合数 + 标题)
|
|
476
522
|
*/
|
|
477
|
-
getSessionFileInfo(projectPath,
|
|
478
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
523
|
+
getSessionFileInfo(projectPath, agentSessionId) {
|
|
524
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
479
525
|
if (!fs.existsSync(sessionFile))
|
|
480
526
|
return { turns: 0 };
|
|
481
527
|
try {
|
|
@@ -511,7 +557,7 @@ export class SessionManager {
|
|
|
511
557
|
async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
|
|
512
558
|
const rows = this.db.prepare(`
|
|
513
559
|
SELECT * FROM sessions
|
|
514
|
-
WHERE channel = ? AND channel_id = ? AND
|
|
560
|
+
WHERE channel = ? AND channel_id = ? AND agent_session_id LIKE ?
|
|
515
561
|
`).all(channel, channelId, `${uuidPrefix}%`);
|
|
516
562
|
if (rows.length === 0)
|
|
517
563
|
return undefined;
|
|
@@ -520,23 +566,23 @@ export class SessionManager {
|
|
|
520
566
|
}
|
|
521
567
|
return this.rowToSession(rows[0]);
|
|
522
568
|
}
|
|
523
|
-
async importCliSession(channel, channelId, projectPath,
|
|
569
|
+
async importCliSession(channel, channelId, projectPath, agentSessionId) {
|
|
524
570
|
// 检查是否已存在相同项目路径的会话
|
|
525
571
|
const existingByPath = this.db.prepare(`
|
|
526
572
|
SELECT * FROM sessions
|
|
527
573
|
WHERE channel = ? AND channel_id = ? AND project_path = ?
|
|
528
574
|
`).get(channel, channelId, projectPath);
|
|
529
575
|
if (existingByPath) {
|
|
530
|
-
// 更新
|
|
576
|
+
// 更新 agent_session_id 并激活
|
|
531
577
|
this.db.prepare(`
|
|
532
578
|
UPDATE sessions SET is_active = 0, updated_at = ?
|
|
533
579
|
WHERE channel = ? AND channel_id = ? AND is_active = 1 AND id != ?
|
|
534
580
|
`).run(Date.now(), channel, channelId, existingByPath.id);
|
|
535
581
|
this.db.prepare(`
|
|
536
|
-
UPDATE sessions SET
|
|
582
|
+
UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
|
|
537
583
|
WHERE id = ?
|
|
538
|
-
`).run(
|
|
539
|
-
return { ...this.rowToSession(existingByPath),
|
|
584
|
+
`).run(agentSessionId, Date.now(), existingByPath.id);
|
|
585
|
+
return { ...this.rowToSession(existingByPath), agentSessionId, isActive: true, updatedAt: Date.now() };
|
|
540
586
|
}
|
|
541
587
|
// 取消当前活跃会话
|
|
542
588
|
this.deactivateAll(channel, channelId);
|
|
@@ -546,7 +592,9 @@ export class SessionManager {
|
|
|
546
592
|
channel,
|
|
547
593
|
channelId,
|
|
548
594
|
projectPath,
|
|
549
|
-
|
|
595
|
+
threadId: '',
|
|
596
|
+
agentType: 'claude',
|
|
597
|
+
agentSessionId,
|
|
550
598
|
name: `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`,
|
|
551
599
|
isActive: true,
|
|
552
600
|
createdAt: Date.now(),
|
package/dist/index.js
CHANGED
|
@@ -43,8 +43,8 @@ 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,
|
|
47
|
-
await sessionManager.
|
|
46
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, async (sessionId, agentSessionId) => {
|
|
47
|
+
await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
|
|
48
48
|
}, anthropic.baseUrl, config);
|
|
49
49
|
logger.info('✓ Agent runner ready');
|
|
50
50
|
// 创建消息缓存
|
|
@@ -78,7 +78,7 @@ async function main() {
|
|
|
78
78
|
// 创建命令处理器
|
|
79
79
|
const cmdHandler = new CommandHandler(sessionManager, agentRunner, config, messageCache);
|
|
80
80
|
// 创建消息处理器
|
|
81
|
-
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId) => {
|
|
81
|
+
const processor = new MessageProcessor(agentRunner, sessionManager, config, messageCache, (content, channel, channelId, userId, threadId) => {
|
|
82
82
|
const sendFn = async (id, text) => {
|
|
83
83
|
const adapter = cmdHandler.getAdapter(channel);
|
|
84
84
|
if (!adapter)
|
|
@@ -108,7 +108,7 @@ async function main() {
|
|
|
108
108
|
await adapter.sendText(id, text);
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
|
-
return cmdHandler.handle(content, channel, channelId, sendFn, userId);
|
|
111
|
+
return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
|
|
112
112
|
});
|
|
113
113
|
// 回填 processor 和 messageQueue 的引用
|
|
114
114
|
cmdHandler.setProcessor(processor);
|
|
@@ -223,8 +223,8 @@ async function main() {
|
|
|
223
223
|
}
|
|
224
224
|
// Feishu 消息处理
|
|
225
225
|
if (feishu) {
|
|
226
|
-
feishu.onMessage(async (chatId, content, images, userId, userName, messageId, mentions) => {
|
|
227
|
-
content =
|
|
226
|
+
feishu.onMessage(async ({ channelId: chatId, content: rawContent, images, userId, userName, messageId, mentions, threadId, rootId }) => {
|
|
227
|
+
let content = rawContent.trim();
|
|
228
228
|
// 首次交互自动绑定主人
|
|
229
229
|
if (userId && !config.channels?.feishu?.owner) {
|
|
230
230
|
const { setOwner } = await import('./config.js');
|
|
@@ -233,11 +233,11 @@ async function main() {
|
|
|
233
233
|
}
|
|
234
234
|
// 命令立即处理,不进入队列
|
|
235
235
|
if (cmdHandler.isCommand(content)) {
|
|
236
|
-
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId);
|
|
236
|
+
const cmdResult = await cmdHandler.handle(content, 'feishu', chatId, undefined, userId, threadId);
|
|
237
237
|
if (cmdResult !== null) {
|
|
238
238
|
if (cmdResult) {
|
|
239
239
|
try {
|
|
240
|
-
await feishu.sendMessage(chatId, cmdResult, { forceText: true });
|
|
240
|
+
await feishu.sendMessage(chatId, cmdResult, { forceText: true, replyToMessageId: rootId, replyInThread: true });
|
|
241
241
|
}
|
|
242
242
|
catch (error) {
|
|
243
243
|
logger.error('[Feishu] Failed to send command response:', error);
|
|
@@ -246,15 +246,16 @@ async function main() {
|
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
//
|
|
250
|
-
const
|
|
249
|
+
// 获取当前项目路径(话题会话自动创建,携带 metadata)
|
|
250
|
+
const metadata = rootId ? { feishu: { rootId } } : undefined;
|
|
251
|
+
const session = await sessionManager.getOrCreateSession('feishu', chatId, config.projects?.defaultPath || process.cwd(), threadId, metadata);
|
|
251
252
|
// 群聊消息添加用户名前缀
|
|
252
253
|
const chatMode = await feishu.getChatMode(chatId);
|
|
253
254
|
if (chatMode === 'group' && userName) {
|
|
254
255
|
content = `[${userName}] ${content}`;
|
|
255
256
|
}
|
|
256
|
-
//
|
|
257
|
-
await messageQueue.enqueue(
|
|
257
|
+
// 普通消息进入队列(使用 session.id 作为 key,话题间可并行)
|
|
258
|
+
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
259
|
});
|
|
259
260
|
}
|
|
260
261
|
// AUN 消息处理
|
|
@@ -16,9 +16,9 @@ async function fileExists(filePath) {
|
|
|
16
16
|
/**
|
|
17
17
|
* 检查会话文件健康度
|
|
18
18
|
*/
|
|
19
|
-
export async function checkSessionFileHealth(projectPath,
|
|
19
|
+
export async function checkSessionFileHealth(projectPath, agentSessionId) {
|
|
20
20
|
const issues = [];
|
|
21
|
-
const sessionFile = path.join(projectPath, '.claude', `${
|
|
21
|
+
const sessionFile = path.join(projectPath, '.claude', `${agentSessionId}.jsonl`);
|
|
22
22
|
// 检查文件是否存在
|
|
23
23
|
if (!(await fileExists(sessionFile))) {
|
|
24
24
|
// 新会话没有文件是正常的
|
|
@@ -60,7 +60,8 @@ export async function checkSessionFileHealth(projectPath, claudeSessionId) {
|
|
|
60
60
|
*/
|
|
61
61
|
export async function backupClaudeDir(projectPath) {
|
|
62
62
|
const claudeDir = path.join(projectPath, '.claude');
|
|
63
|
-
const
|
|
63
|
+
const dirName = path.basename(claudeDir);
|
|
64
|
+
const backupDir = path.join(path.dirname(claudeDir), `${dirName}-backup-${Date.now()}`);
|
|
64
65
|
await fs.cp(claudeDir, backupDir, { recursive: true });
|
|
65
66
|
logger.info(`[SessionFileHealth] Backup created: ${backupDir}`);
|
|
66
67
|
return backupDir;
|
package/package.json
CHANGED