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.
- package/README.md +9 -4
- package/data/evolclaw.sample.json +3 -2
- package/dist/channels/feishu.js +39 -15
- package/dist/cli.js +33 -15
- package/dist/config.js +4 -1
- package/dist/core/agent-runner.js +56 -21
- package/dist/core/command-handler.js +271 -66
- package/dist/core/message-processor.js +117 -59
- package/dist/core/session-manager.js +156 -130
- package/dist/index.js +18 -14
- package/dist/index.js.bak +340 -0
- package/dist/utils/session-file-health.js +4 -3
- package/dist/utils/stream-flusher.js +42 -32
- 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,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
|
-
//
|
|
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),
|
|
184
|
+
return { ...this.rowToSession(active), agentSessionId: validSessionId };
|
|
200
185
|
}
|
|
201
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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),
|
|
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
|
|
290
|
-
// 只更新当前活跃会话的
|
|
303
|
+
async updateAgentSessionId(channel, channelId, agentSessionId) {
|
|
304
|
+
// 只更新当前活跃会话的 Agent Session ID
|
|
291
305
|
this.db.prepare(`
|
|
292
306
|
UPDATE sessions
|
|
293
|
-
SET
|
|
307
|
+
SET agent_session_id = ?, updated_at = ?
|
|
294
308
|
WHERE channel = ? AND channel_id = ? AND is_active = 1
|
|
295
|
-
`).run(
|
|
309
|
+
`).run(agentSessionId, Date.now(), channel, channelId);
|
|
296
310
|
}
|
|
297
|
-
async
|
|
311
|
+
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
298
312
|
// 根据 sessionId 直接更新
|
|
299
|
-
logger.info(`[SessionManager] Updating
|
|
313
|
+
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
300
314
|
this.db.prepare(`
|
|
301
315
|
UPDATE sessions
|
|
302
|
-
SET
|
|
316
|
+
SET agent_session_id = ?, updated_at = ?
|
|
303
317
|
WHERE id = ?
|
|
304
|
-
`).run(
|
|
318
|
+
`).run(agentSessionId, Date.now(), sessionId);
|
|
305
319
|
}
|
|
306
320
|
async clearActiveSession(channel, channelId) {
|
|
307
|
-
// 清除当前活跃会话的
|
|
321
|
+
// 清除当前活跃会话的 Agent Session ID
|
|
308
322
|
this.db.prepare(`
|
|
309
323
|
UPDATE sessions
|
|
310
|
-
SET
|
|
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,
|
|
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
|
-
|
|
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,
|
|
429
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
452
|
+
checkSessionFileExists(projectPath, agentSessionId) {
|
|
453
|
+
const sessionFile = this.getSessionFilePath(projectPath, agentSessionId);
|
|
430
454
|
return fs.existsSync(sessionFile);
|
|
431
455
|
}
|
|
432
|
-
readSessionFirstMessage(projectPath,
|
|
433
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
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,
|
|
454
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
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,
|
|
478
|
-
const sessionFile = this.getSessionFilePath(projectPath,
|
|
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
|
|
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,
|
|
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
|
-
// 更新
|
|
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
|
|
560
|
+
UPDATE sessions SET agent_session_id = ?, is_active = 1, updated_at = ?
|
|
537
561
|
WHERE id = ?
|
|
538
|
-
`).run(
|
|
539
|
-
return { ...this.rowToSession(existingByPath),
|
|
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
|
-
|
|
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,
|
|
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
|
+
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 =
|
|
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
|
|
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(
|
|
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 消息处理
|