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