evolclaw 2.8.3 → 3.0.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 +21 -12
- package/dist/agents/claude-runner.js +102 -38
- package/dist/agents/codex-runner.js +11 -14
- package/dist/agents/gemini-runner.js +10 -12
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1051 -288
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +858 -847
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +309 -142
- package/dist/core/message/message-queue.js +3 -3
- package/dist/core/permission.js +21 -8
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +704 -775
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/{templates → data}/prompts.md +34 -1
- package/dist/index.js +431 -275
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
2
1
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
3
|
-
import { ensureDir } from '../../
|
|
4
|
-
import { resolvePaths } from '../../paths.js';
|
|
2
|
+
import { ensureDir } from '../../utils/atomic-write.js';
|
|
5
3
|
import { logger } from '../../utils/logger.js';
|
|
6
4
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
5
|
+
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
6
|
+
import { sessionToFile, fileToSession } from './session-mapper.js';
|
|
7
7
|
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
8
9
|
import os from 'os';
|
|
9
10
|
export class SessionManager {
|
|
10
|
-
|
|
11
|
+
sessionsDir;
|
|
11
12
|
eventBus;
|
|
12
13
|
ownerResolver;
|
|
13
14
|
adminResolver;
|
|
14
15
|
sessionModeResolver;
|
|
15
16
|
fileAdapters = new Map();
|
|
16
17
|
sessionEncryptState = new Map();
|
|
17
|
-
constructor(
|
|
18
|
-
ensureDir(
|
|
19
|
-
this.
|
|
18
|
+
constructor(sessionsDir, eventBus, ownerResolver, adminResolver) {
|
|
19
|
+
ensureDir(sessionsDir);
|
|
20
|
+
this.sessionsDir = sessionsDir;
|
|
20
21
|
this.eventBus = eventBus;
|
|
21
22
|
this.ownerResolver = ownerResolver;
|
|
22
23
|
this.adminResolver = adminResolver;
|
|
23
|
-
this.initDatabase();
|
|
24
24
|
}
|
|
25
25
|
setOwnerResolver(resolver) {
|
|
26
26
|
this.ownerResolver = resolver;
|
|
@@ -31,7 +31,6 @@ export class SessionManager {
|
|
|
31
31
|
setSessionModeResolver(resolver) {
|
|
32
32
|
this.sessionModeResolver = resolver;
|
|
33
33
|
}
|
|
34
|
-
/** 解析默认 sessionMode:通道配置锁定 > chatType 默认 > 'interactive' */
|
|
35
34
|
resolveDefaultSessionMode(channel, chatType) {
|
|
36
35
|
const ct = chatType || 'private';
|
|
37
36
|
const resolved = this.sessionModeResolver?.(channel, ct);
|
|
@@ -44,9 +43,6 @@ export class SessionManager {
|
|
|
44
43
|
getFileAdapter(agentId) {
|
|
45
44
|
return this.fileAdapters.get(agentId);
|
|
46
45
|
}
|
|
47
|
-
getDatabase() {
|
|
48
|
-
return this.db;
|
|
49
|
-
}
|
|
50
46
|
getProjectDirName(projectPath) {
|
|
51
47
|
return encodePath(projectPath);
|
|
52
48
|
}
|
|
@@ -55,27 +51,6 @@ export class SessionManager {
|
|
|
55
51
|
const encodedPath = this.getProjectDirName(projectPath);
|
|
56
52
|
return path.join(homeDir, '.claude', 'projects', encodedPath, `${sessionId}.jsonl`);
|
|
57
53
|
}
|
|
58
|
-
rowToSession(row) {
|
|
59
|
-
const metadata = row.metadata ? JSON.parse(row.metadata) : undefined;
|
|
60
|
-
return {
|
|
61
|
-
id: row.id,
|
|
62
|
-
channel: row.channel,
|
|
63
|
-
channelId: row.channel_id,
|
|
64
|
-
projectPath: row.project_path,
|
|
65
|
-
threadId: row.thread_id || '',
|
|
66
|
-
agentId: row.agent_id || 'claude',
|
|
67
|
-
chatType: row.chat_type || 'private',
|
|
68
|
-
sessionMode: row.session_mode || 'interactive',
|
|
69
|
-
agentSessionId: row.agent_session_id,
|
|
70
|
-
metadata,
|
|
71
|
-
name: row.name,
|
|
72
|
-
processingState: row.processing_state || undefined,
|
|
73
|
-
createdAt: row.created_at,
|
|
74
|
-
updatedAt: row.updated_at,
|
|
75
|
-
deletedAt: row.deleted_at ?? undefined,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/** 根据 userId 计算身份 */
|
|
79
54
|
resolveIdentity(channel, userId) {
|
|
80
55
|
if (!userId)
|
|
81
56
|
return { role: 'anonymous', mode: 'interactive' };
|
|
@@ -85,58 +60,9 @@ export class SessionManager {
|
|
|
85
60
|
return { role: 'admin', mode: 'interactive' };
|
|
86
61
|
return { role: 'guest', mode: 'interactive' };
|
|
87
62
|
}
|
|
88
|
-
/** 更新 session 的 identity(owner 绑定后调用) */
|
|
89
63
|
async updateIdentity(sessionId, identity) {
|
|
90
|
-
// identity 不持久化到 DB,仅更新内存中的返回值
|
|
91
|
-
// 调用方应直接修改持有的 session 对象
|
|
92
64
|
logger.debug(`[SessionManager] updateIdentity: sessionId=${sessionId}, role=${identity.role}`);
|
|
93
65
|
}
|
|
94
|
-
/** 取消所有活跃会话(通过 metadata.isActive) */
|
|
95
|
-
deactivateAllMetadata(channel, channelId) {
|
|
96
|
-
const rows = this.db.prepare(`
|
|
97
|
-
SELECT id, metadata FROM sessions
|
|
98
|
-
WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true
|
|
99
|
-
`).all(channel, channelId);
|
|
100
|
-
for (const row of rows) {
|
|
101
|
-
const metadata = row.metadata ? JSON.parse(row.metadata) : {};
|
|
102
|
-
metadata.isActive = false;
|
|
103
|
-
this.db.prepare(`
|
|
104
|
-
UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?
|
|
105
|
-
`).run(JSON.stringify(metadata), Date.now(), row.id);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** 获取当前活跃 session 的 chatType(用于新建 session 时继承) */
|
|
109
|
-
getActiveChatType(channel, channelId) {
|
|
110
|
-
const row = this.db.prepare(`
|
|
111
|
-
SELECT chat_type FROM sessions
|
|
112
|
-
WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND thread_id = '' AND deleted_at IS NULL
|
|
113
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
114
|
-
`).get(channel, channelId);
|
|
115
|
-
return row?.chat_type || 'private';
|
|
116
|
-
}
|
|
117
|
-
validateSessionFile(row) {
|
|
118
|
-
const agentSessionId = row.agent_session_id;
|
|
119
|
-
if (!agentSessionId)
|
|
120
|
-
return undefined;
|
|
121
|
-
const agentId = row.agent_id || 'claude';
|
|
122
|
-
const adapter = this.getFileAdapter(agentId);
|
|
123
|
-
if (!adapter) {
|
|
124
|
-
// 无适配器:无法验证文件,信任 DB 记录
|
|
125
|
-
return agentSessionId;
|
|
126
|
-
}
|
|
127
|
-
if (adapter.checkExists(row.project_path, agentSessionId)) {
|
|
128
|
-
return agentSessionId;
|
|
129
|
-
}
|
|
130
|
-
logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
|
|
131
|
-
this.db.prepare(`UPDATE sessions SET agent_session_id = NULL WHERE id = ?`).run(row.id);
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
insertSession(session) {
|
|
135
|
-
this.db.prepare(`
|
|
136
|
-
INSERT OR IGNORE INTO sessions (id, channel, channel_id, project_path, thread_id, agent_id, chat_type, session_mode, agent_session_id, name, created_at, updated_at, metadata)
|
|
137
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
138
|
-
`).run(session.id, session.channel, session.channelId, session.projectPath, session.threadId || '', session.agentId || 'claude', session.chatType || 'private', session.sessionMode || 'interactive', session.agentSessionId ?? null, session.name ?? null, session.createdAt, session.updatedAt, session.metadata ? JSON.stringify(session.metadata) : null);
|
|
139
|
-
}
|
|
140
66
|
extractUserMessageText(messageContent) {
|
|
141
67
|
if (typeof messageContent === 'string') {
|
|
142
68
|
const text = messageContent.trim().replace(/\s+/g, ' ');
|
|
@@ -151,306 +77,230 @@ export class SessionManager {
|
|
|
151
77
|
}
|
|
152
78
|
return null;
|
|
153
79
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
id TEXT PRIMARY KEY,
|
|
176
|
-
channel TEXT NOT NULL,
|
|
177
|
-
channel_id TEXT NOT NULL,
|
|
178
|
-
agent_id TEXT NOT NULL DEFAULT 'claude',
|
|
179
|
-
thread_id TEXT NOT NULL DEFAULT '',
|
|
180
|
-
chat_type TEXT NOT NULL DEFAULT 'private',
|
|
181
|
-
session_mode TEXT NOT NULL DEFAULT 'interactive',
|
|
182
|
-
project_path TEXT NOT NULL,
|
|
183
|
-
agent_session_id TEXT,
|
|
184
|
-
name TEXT,
|
|
185
|
-
created_at INTEGER NOT NULL,
|
|
186
|
-
updated_at INTEGER NOT NULL,
|
|
187
|
-
metadata TEXT,
|
|
188
|
-
deleted_at INTEGER
|
|
189
|
-
)
|
|
190
|
-
`);
|
|
191
|
-
// 迁移数据:is_group → chat_type, agent_type → agent_id
|
|
192
|
-
this.db.exec(`
|
|
193
|
-
INSERT INTO sessions_new (id, channel, channel_id, agent_id, thread_id, chat_type, session_mode, project_path, agent_session_id, name, created_at, updated_at, metadata, deleted_at)
|
|
194
|
-
SELECT
|
|
195
|
-
id,
|
|
196
|
-
channel,
|
|
197
|
-
channel_id,
|
|
198
|
-
COALESCE(agent_type, 'claude'),
|
|
199
|
-
COALESCE(thread_id, ''),
|
|
200
|
-
CASE WHEN is_group = 1 THEN 'group' ELSE 'private' END,
|
|
201
|
-
'interactive',
|
|
202
|
-
project_path,
|
|
203
|
-
agent_session_id,
|
|
204
|
-
name,
|
|
205
|
-
created_at,
|
|
206
|
-
updated_at,
|
|
207
|
-
metadata,
|
|
208
|
-
deleted_at
|
|
209
|
-
FROM sessions
|
|
210
|
-
`);
|
|
211
|
-
this.db.exec(`DROP TABLE sessions`);
|
|
212
|
-
this.db.exec(`ALTER TABLE sessions_new RENAME TO sessions`);
|
|
213
|
-
// 创建新索引
|
|
214
|
-
this.db.exec(`
|
|
215
|
-
CREATE INDEX IF NOT EXISTS idx_session_space
|
|
216
|
-
ON sessions(channel, channel_id, agent_id, thread_id)
|
|
217
|
-
WHERE deleted_at IS NULL
|
|
218
|
-
`);
|
|
219
|
-
this.db.exec(`
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_session_active
|
|
221
|
-
ON sessions(channel, channel_id)
|
|
222
|
-
WHERE deleted_at IS NULL
|
|
223
|
-
`);
|
|
224
|
-
logger.info('✓ Database migration completed (session model refactored)');
|
|
80
|
+
// ─── File I/O helpers ───
|
|
81
|
+
/**
|
|
82
|
+
* 解析 chat 目录路径。
|
|
83
|
+
* 1. 先扫描所有 chat 目录,按 channelId 查找匹配项(同时 channelType==channel 或缺失时直接 channelId 匹配)
|
|
84
|
+
* 2. 找不到则按 fallback:channelType=channel(实例名),selfId=null
|
|
85
|
+
*
|
|
86
|
+
* 这样保持兼容:不知道 channelType 的 caller 仍可以用 (channel, channelId) 调用。
|
|
87
|
+
*/
|
|
88
|
+
resolveChatDir(channel, channelId) {
|
|
89
|
+
// 优先尝试从已有目录里找
|
|
90
|
+
const dirs = scanChatDirs(this.sessionsDir);
|
|
91
|
+
for (const d of dirs) {
|
|
92
|
+
if (d.channelId !== channelId)
|
|
93
|
+
continue;
|
|
94
|
+
// 验证 active.json 或 meta 文件里 channel(实例名)匹配
|
|
95
|
+
const active = readJsonFile(path.join(d.dirPath, 'active.json'));
|
|
96
|
+
if (active && active.channel === channel)
|
|
97
|
+
return d.dirPath;
|
|
98
|
+
// 没 active.json 时,看 channelType 是否能匹配 channel
|
|
99
|
+
if (!active && d.channelType === channel)
|
|
100
|
+
return d.dirPath;
|
|
225
101
|
}
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
this.db.exec(`DROP TABLE IF EXISTS sessions_new`);
|
|
237
|
-
this.db.exec(`
|
|
238
|
-
CREATE TABLE sessions_new (
|
|
239
|
-
id TEXT PRIMARY KEY,
|
|
240
|
-
channel TEXT NOT NULL,
|
|
241
|
-
channel_id TEXT NOT NULL,
|
|
242
|
-
project_path TEXT NOT NULL,
|
|
243
|
-
thread_id TEXT NOT NULL DEFAULT '',
|
|
244
|
-
agent_type TEXT NOT NULL DEFAULT 'claude',
|
|
245
|
-
agent_session_id TEXT,
|
|
246
|
-
name TEXT,
|
|
247
|
-
is_active INTEGER NOT NULL DEFAULT 0,
|
|
248
|
-
created_at INTEGER NOT NULL,
|
|
249
|
-
updated_at INTEGER NOT NULL,
|
|
250
|
-
metadata TEXT
|
|
251
|
-
);
|
|
252
|
-
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)
|
|
253
|
-
SELECT id, channel, channel_id, project_path, '', 'claude', claude_session_id, name, is_active, created_at, updated_at, NULL FROM sessions;
|
|
254
|
-
DROP TABLE sessions;
|
|
255
|
-
ALTER TABLE sessions_new RENAME TO sessions;
|
|
256
|
-
`);
|
|
257
|
-
// 话题会话唯一约束(thread_id 非空时才生效)
|
|
258
|
-
this.db.exec(`
|
|
259
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_thread
|
|
260
|
-
ON sessions(channel, channel_id, project_path, thread_id)
|
|
261
|
-
WHERE thread_id != ''
|
|
262
|
-
`);
|
|
263
|
-
logger.info('✓ Database migration completed (thread support added)');
|
|
264
|
-
}
|
|
265
|
-
// Migration: add is_group column
|
|
266
|
-
if (!hasIsGroup) {
|
|
267
|
-
logger.info('Migrating database schema (adding is_group)...');
|
|
268
|
-
const addIsGroupCol = 'ALTER TABLE sessions ADD COLUMN is_group INTEGER NOT NULL DEFAULT 0';
|
|
269
|
-
this.db.exec(addIsGroupCol);
|
|
270
|
-
logger.info('✓ Database migration completed (is_group added)');
|
|
271
|
-
}
|
|
272
|
-
// Reset incorrect is_group values (oc_ prefix doesn't reliably indicate group chat)
|
|
273
|
-
if (hasIsGroup) {
|
|
274
|
-
this.db.exec("UPDATE sessions SET is_group = 0 WHERE channel = 'feishu'");
|
|
275
|
-
}
|
|
276
|
-
// Migration: add deleted_at column
|
|
277
|
-
if (!hasDeletedAt) {
|
|
278
|
-
logger.info('Migrating database schema (adding deleted_at)...');
|
|
279
|
-
this.db.exec(`ALTER TABLE sessions ADD COLUMN deleted_at INTEGER`);
|
|
280
|
-
logger.info('✓ Database migration completed (deleted_at added)');
|
|
281
|
-
}
|
|
102
|
+
// Fallback:按 channel 当 channelType 创建(旧路径布局兼容)
|
|
103
|
+
return chatDirPath(this.sessionsDir, channel, channelId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 给定明确的 channelType + selfId 时直接计算路径(不扫描)。
|
|
107
|
+
* 用于 caller 已经知道完整路由信息的场景(如 message-bridge 透传)。
|
|
108
|
+
*/
|
|
109
|
+
resolveChatDirExact(channel, channelId, channelType, selfId) {
|
|
110
|
+
if (channelType) {
|
|
111
|
+
return chatDirPath(this.sessionsDir, channelType, channelId, selfId);
|
|
282
112
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
113
|
+
return this.resolveChatDir(channel, channelId);
|
|
114
|
+
}
|
|
115
|
+
resolveChatDirFromSession(session) {
|
|
116
|
+
const channelType = session.channelType || session.channel;
|
|
117
|
+
return chatDirPath(this.sessionsDir, channelType, session.channelId, session.selfId);
|
|
118
|
+
}
|
|
119
|
+
/** Public accessor: get the chat directory path for a session (for message log etc.) */
|
|
120
|
+
getChatDir(session) {
|
|
121
|
+
return this.resolveChatDirFromSession(session);
|
|
122
|
+
}
|
|
123
|
+
/** Like resolveChatDir but also ensures the dir + _threads + _trash exist. */
|
|
124
|
+
ensureResolvedChatDir(channel, channelId) {
|
|
125
|
+
const dir = this.resolveChatDir(channel, channelId);
|
|
126
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
127
|
+
fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
|
|
128
|
+
fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
|
|
129
|
+
return dir;
|
|
130
|
+
}
|
|
131
|
+
/** 推断给定 chat 的 channelType(优先取 active.json)。无活跃时回落到 channel 实例名。 */
|
|
132
|
+
inferChannelType(channel, channelId) {
|
|
133
|
+
const active = this.readActive(channel, channelId);
|
|
134
|
+
return active?.channelType || channel;
|
|
135
|
+
}
|
|
136
|
+
/** 从 active 推断 selfId(已有 session 的复用) */
|
|
137
|
+
inferSelfId(channel, channelId) {
|
|
138
|
+
const active = this.readActive(channel, channelId);
|
|
139
|
+
return active?.selfId;
|
|
140
|
+
}
|
|
141
|
+
readActive(channel, channelId) {
|
|
142
|
+
const dir = this.resolveChatDir(channel, channelId);
|
|
143
|
+
const file = readJsonFile(path.join(dir, 'active.json'));
|
|
144
|
+
if (!file)
|
|
145
|
+
return undefined;
|
|
146
|
+
return fileToSession(file);
|
|
147
|
+
}
|
|
148
|
+
writeActive(channel, channelId, session) {
|
|
149
|
+
const dir = this.ensureChatDirForSession(session);
|
|
150
|
+
const file = sessionToFile(session);
|
|
151
|
+
atomicWriteJson(path.join(dir, 'active.json'), file);
|
|
152
|
+
}
|
|
153
|
+
clearActive(channel, channelId) {
|
|
154
|
+
const dir = this.resolveChatDir(channel, channelId);
|
|
155
|
+
const activePath = path.join(dir, 'active.json');
|
|
156
|
+
try {
|
|
157
|
+
fs.unlinkSync(activePath);
|
|
291
158
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let migrated = 0;
|
|
296
|
-
for (const row of rows) {
|
|
297
|
-
try {
|
|
298
|
-
const meta = JSON.parse(row.metadata);
|
|
299
|
-
const rootId = meta.feishu?.rootId ?? meta.threadRootId ?? meta.replyOpts?.rootId;
|
|
300
|
-
if (!rootId && !meta.feishu && !meta.threadRootId && !meta.replyOpts)
|
|
301
|
-
continue;
|
|
302
|
-
// Generate replyContext from rootId if missing
|
|
303
|
-
if (rootId && !meta.replyContext) {
|
|
304
|
-
meta.replyContext = { replyToMessageId: rootId, replyInThread: true };
|
|
305
|
-
}
|
|
306
|
-
// Clean up all legacy fields
|
|
307
|
-
delete meta.feishu;
|
|
308
|
-
delete meta.threadRootId;
|
|
309
|
-
delete meta.replyOpts;
|
|
310
|
-
this.db.prepare('UPDATE sessions SET metadata = ? WHERE id = ?')
|
|
311
|
-
.run(JSON.stringify(meta), row.id);
|
|
312
|
-
migrated++;
|
|
313
|
-
}
|
|
314
|
-
catch { /* skip malformed JSON */ }
|
|
315
|
-
}
|
|
316
|
-
if (migrated > 0) {
|
|
317
|
-
logger.info(`✓ Migrated ${migrated} session(s): rootId normalized to replyContext`);
|
|
318
|
-
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
if (e.code !== 'ENOENT')
|
|
161
|
+
throw e;
|
|
319
162
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
163
|
+
}
|
|
164
|
+
ensureChatDirForSession(session) {
|
|
165
|
+
const channelType = session.channelType || session.channel;
|
|
166
|
+
return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfId);
|
|
167
|
+
}
|
|
168
|
+
metaFilePath(chatDir, sessionId) {
|
|
169
|
+
return path.join(chatDir, `${sessionId}.jsonl`);
|
|
170
|
+
}
|
|
171
|
+
appendMeta(channel, channelId, session) {
|
|
172
|
+
const dir = this.ensureChatDirForSession(session);
|
|
173
|
+
const isThread = !!session.threadId;
|
|
174
|
+
const targetDir = isThread ? path.join(dir, '_threads') : dir;
|
|
175
|
+
const file = sessionToFile(session);
|
|
176
|
+
const metaPath = this.metaFilePath(targetDir, session.id);
|
|
177
|
+
appendJsonl(metaPath, file);
|
|
178
|
+
}
|
|
179
|
+
readMetaLatest(metaFilePath) {
|
|
180
|
+
const file = readLastJsonlLine(metaFilePath);
|
|
181
|
+
if (!file)
|
|
182
|
+
return undefined;
|
|
183
|
+
return fileToSession(file);
|
|
184
|
+
}
|
|
185
|
+
validateSessionFile(session) {
|
|
186
|
+
const agentSessionId = session.agentSessionId;
|
|
187
|
+
if (!agentSessionId)
|
|
188
|
+
return undefined;
|
|
189
|
+
const agentId = session.agentId || 'claude';
|
|
190
|
+
const adapter = this.getFileAdapter(agentId);
|
|
191
|
+
if (!adapter)
|
|
192
|
+
return agentSessionId;
|
|
193
|
+
if (adapter.checkExists(session.projectPath, agentSessionId))
|
|
194
|
+
return agentSessionId;
|
|
195
|
+
logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
|
|
196
|
+
session.agentSessionId = undefined;
|
|
197
|
+
this.appendMeta(session.channel, session.channelId, session);
|
|
198
|
+
const active = this.readActive(session.channel, session.channelId);
|
|
199
|
+
if (active && active.id === session.id) {
|
|
200
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
getActiveChatType(channel, channelId) {
|
|
205
|
+
const active = this.readActive(channel, channelId);
|
|
206
|
+
if (active && !active.threadId)
|
|
207
|
+
return active.chatType || 'private';
|
|
208
|
+
return 'private';
|
|
209
|
+
}
|
|
210
|
+
findAllSessionsInChat(chatDir, includeThreads = true) {
|
|
211
|
+
const metaFiles = scanMetaFiles(chatDir);
|
|
212
|
+
const results = [];
|
|
213
|
+
for (const metaFile of metaFiles) {
|
|
214
|
+
const session = this.readMetaLatest(path.join(chatDir, metaFile));
|
|
215
|
+
if (session)
|
|
216
|
+
results.push(session);
|
|
217
|
+
}
|
|
218
|
+
if (includeThreads) {
|
|
219
|
+
const threadsDir = path.join(chatDir, '_threads');
|
|
220
|
+
const threadMetas = scanMetaFiles(threadsDir);
|
|
221
|
+
for (const metaFile of threadMetas) {
|
|
222
|
+
const session = this.readMetaLatest(path.join(threadsDir, metaFile));
|
|
223
|
+
if (session)
|
|
224
|
+
results.push(session);
|
|
338
225
|
}
|
|
339
226
|
}
|
|
340
|
-
|
|
341
|
-
this.db.exec(`
|
|
342
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
343
|
-
id TEXT PRIMARY KEY,
|
|
344
|
-
channel TEXT NOT NULL,
|
|
345
|
-
channel_id TEXT NOT NULL,
|
|
346
|
-
agent_id TEXT NOT NULL DEFAULT 'claude',
|
|
347
|
-
thread_id TEXT NOT NULL DEFAULT '',
|
|
348
|
-
chat_type TEXT NOT NULL DEFAULT 'private',
|
|
349
|
-
session_mode TEXT NOT NULL DEFAULT 'interactive',
|
|
350
|
-
project_path TEXT NOT NULL,
|
|
351
|
-
agent_session_id TEXT,
|
|
352
|
-
name TEXT,
|
|
353
|
-
processing_state TEXT,
|
|
354
|
-
created_at INTEGER NOT NULL,
|
|
355
|
-
updated_at INTEGER NOT NULL,
|
|
356
|
-
metadata TEXT,
|
|
357
|
-
deleted_at INTEGER
|
|
358
|
-
)
|
|
359
|
-
`);
|
|
360
|
-
// 会话空间索引(查询优化,无唯一约束)
|
|
361
|
-
this.db.exec(`
|
|
362
|
-
CREATE INDEX IF NOT EXISTS idx_session_space
|
|
363
|
-
ON sessions(channel, channel_id, agent_id, thread_id)
|
|
364
|
-
WHERE deleted_at IS NULL
|
|
365
|
-
`);
|
|
366
|
-
this.db.exec(`
|
|
367
|
-
CREATE INDEX IF NOT EXISTS idx_session_active
|
|
368
|
-
ON sessions(channel, channel_id)
|
|
369
|
-
WHERE deleted_at IS NULL
|
|
370
|
-
`);
|
|
371
|
-
// 创建消息去重表
|
|
372
|
-
this.db.exec(`
|
|
373
|
-
CREATE TABLE IF NOT EXISTS processed_messages (
|
|
374
|
-
message_id TEXT PRIMARY KEY,
|
|
375
|
-
channel TEXT NOT NULL,
|
|
376
|
-
channel_id TEXT NOT NULL,
|
|
377
|
-
processed_at INTEGER NOT NULL
|
|
378
|
-
);
|
|
379
|
-
CREATE INDEX IF NOT EXISTS idx_processed_at ON processed_messages(processed_at);
|
|
380
|
-
`);
|
|
381
|
-
// 创建会话健康状态表
|
|
382
|
-
this.db.exec(`
|
|
383
|
-
CREATE TABLE IF NOT EXISTS session_health (
|
|
384
|
-
session_id TEXT PRIMARY KEY,
|
|
385
|
-
consecutive_errors INTEGER NOT NULL DEFAULT 0,
|
|
386
|
-
last_error TEXT,
|
|
387
|
-
last_error_type TEXT,
|
|
388
|
-
safe_mode INTEGER NOT NULL DEFAULT 0,
|
|
389
|
-
last_success_time INTEGER NOT NULL,
|
|
390
|
-
created_at INTEGER NOT NULL,
|
|
391
|
-
updated_at INTEGER NOT NULL,
|
|
392
|
-
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
393
|
-
);
|
|
394
|
-
CREATE INDEX IF NOT EXISTS idx_session_health_safe_mode ON session_health(safe_mode);
|
|
395
|
-
`);
|
|
227
|
+
return results;
|
|
396
228
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
migrateChannelToInstanceName() {
|
|
402
|
-
const rows = this.db.prepare(`SELECT id, channel, metadata FROM sessions WHERE metadata IS NOT NULL AND metadata != ''`).all();
|
|
403
|
-
let migrated = 0;
|
|
404
|
-
for (const row of rows) {
|
|
229
|
+
findSessionFileById(sessionId) {
|
|
230
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
231
|
+
for (const { dirPath } of chatDirs) {
|
|
232
|
+
const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
405
233
|
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
this.db.prepare(`UPDATE sessions SET channel = ?, updated_at = ? WHERE id = ?`)
|
|
409
|
-
.run(meta.channelName, Date.now(), row.id);
|
|
410
|
-
migrated++;
|
|
411
|
-
logger.info(`[Migration] Restored channel '${row.channel}' -> '${meta.channelName}' (session ${row.id})`);
|
|
412
|
-
}
|
|
234
|
+
fs.statSync(mainPath);
|
|
235
|
+
return { chatDir: dirPath, metaPath: mainPath, isThread: false };
|
|
413
236
|
}
|
|
414
|
-
catch {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
237
|
+
catch { }
|
|
238
|
+
const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
|
|
239
|
+
try {
|
|
240
|
+
fs.statSync(threadPath);
|
|
241
|
+
return { chatDir: dirPath, metaPath: threadPath, isThread: true };
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
418
244
|
}
|
|
245
|
+
return undefined;
|
|
419
246
|
}
|
|
420
|
-
|
|
421
|
-
* 获取指定渠道所有已知的 thread_id(用于重启后预填充 seenThreads)
|
|
422
|
-
*/
|
|
247
|
+
// ─── Public API ───
|
|
423
248
|
getKnownThreadIds(channel) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
249
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
250
|
+
const threadIds = [];
|
|
251
|
+
for (const { dirPath } of chatDirs) {
|
|
252
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
253
|
+
const matchInstance = active?.channel === channel;
|
|
254
|
+
// 也兼容没 active.json 时按目录顶层 channelType 匹配(fallback 路径布局)
|
|
255
|
+
if (!matchInstance)
|
|
256
|
+
continue;
|
|
257
|
+
const index = readThreadIndex(dirPath);
|
|
258
|
+
for (const tid of Object.keys(index))
|
|
259
|
+
threadIds.push(tid);
|
|
260
|
+
}
|
|
261
|
+
return threadIds;
|
|
429
262
|
}
|
|
430
|
-
/**
|
|
431
|
-
* 标记会话为处理中(实时写 DB,crash 也能恢复)
|
|
432
|
-
* processing_state 格式: "timestamp:taskId"
|
|
433
|
-
*/
|
|
434
263
|
markProcessing(sessionId, taskId) {
|
|
435
264
|
const now = Date.now();
|
|
436
265
|
const state = taskId ? `${now}:${taskId}` : String(now);
|
|
437
|
-
|
|
438
|
-
|
|
266
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
267
|
+
for (const { dirPath } of chatDirs) {
|
|
268
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
269
|
+
if (active && active.id === sessionId) {
|
|
270
|
+
active.activeTask = state;
|
|
271
|
+
active.updatedAt = now;
|
|
272
|
+
active.updatedAtStr = formatTimestamp(now);
|
|
273
|
+
atomicWriteJson(path.join(dirPath, 'active.json'), active);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
439
277
|
}
|
|
440
|
-
/** 从 processing_state 解析当前活跃 taskId */
|
|
441
278
|
getActiveTaskId(sessionId) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
279
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
280
|
+
for (const { dirPath } of chatDirs) {
|
|
281
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
282
|
+
if (active && active.id === sessionId) {
|
|
283
|
+
if (!active.activeTask)
|
|
284
|
+
return undefined;
|
|
285
|
+
const colonIdx = active.activeTask.indexOf(':');
|
|
286
|
+
return colonIdx > 0 ? active.activeTask.slice(colonIdx + 1) : undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
447
290
|
}
|
|
448
|
-
/**
|
|
449
|
-
* 清除会话处理中状态
|
|
450
|
-
*/
|
|
451
291
|
clearProcessing(sessionId) {
|
|
452
|
-
|
|
453
|
-
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
294
|
+
for (const { dirPath } of chatDirs) {
|
|
295
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
296
|
+
if (active && active.id === sessionId) {
|
|
297
|
+
active.activeTask = null;
|
|
298
|
+
active.updatedAt = now;
|
|
299
|
+
active.updatedAtStr = formatTimestamp(now);
|
|
300
|
+
atomicWriteJson(path.join(dirPath, 'active.json'), active);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
454
304
|
this.sessionEncryptState.delete(sessionId);
|
|
455
305
|
}
|
|
456
306
|
setSessionEncrypt(sessionId, encrypted) {
|
|
@@ -459,136 +309,128 @@ export class SessionManager {
|
|
|
459
309
|
getSessionEncrypt(sessionId) {
|
|
460
310
|
return this.sessionEncryptState.get(sessionId);
|
|
461
311
|
}
|
|
462
|
-
/**
|
|
463
|
-
* 获取所有处于 processing 状态的会话(用于重启后恢复)
|
|
464
|
-
* @param maxAgeMs 最大存活时间(超过则视为超时,清除状态)默认 1 小时
|
|
465
|
-
*/
|
|
466
312
|
getPendingProcessingSessions(maxAgeMs = 60 * 60 * 1000) {
|
|
467
|
-
const rows = this.db.prepare(`
|
|
468
|
-
SELECT * FROM sessions
|
|
469
|
-
WHERE processing_state IS NOT NULL AND deleted_at IS NULL
|
|
470
|
-
`).all();
|
|
471
313
|
const now = Date.now();
|
|
472
314
|
const result = [];
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
315
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
316
|
+
for (const { dirPath } of chatDirs) {
|
|
317
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
318
|
+
if (!active || !active.activeTask)
|
|
319
|
+
continue;
|
|
320
|
+
const colonIdx = active.activeTask.indexOf(':');
|
|
321
|
+
const ts = parseInt(colonIdx > 0 ? active.activeTask.slice(0, colonIdx) : active.activeTask, 10);
|
|
476
322
|
if (!isNaN(ts) && (now - ts) < maxAgeMs) {
|
|
477
|
-
result.push(
|
|
323
|
+
result.push(fileToSession(active));
|
|
478
324
|
}
|
|
479
325
|
else {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
326
|
+
active.activeTask = null;
|
|
327
|
+
active.updatedAt = now;
|
|
328
|
+
active.updatedAtStr = formatTimestamp(now);
|
|
329
|
+
atomicWriteJson(path.join(dirPath, 'active.json'), active);
|
|
483
330
|
}
|
|
484
331
|
}
|
|
485
332
|
return result;
|
|
486
333
|
}
|
|
487
|
-
|
|
488
|
-
|
|
334
|
+
// ─── Session lifecycle ───
|
|
335
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType) {
|
|
489
336
|
if (threadId) {
|
|
490
|
-
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId);
|
|
337
|
+
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType);
|
|
491
338
|
session.identity = this.resolveIdentity(channel, userId);
|
|
492
|
-
// 新话题会话补写默认权限模式
|
|
493
339
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
494
340
|
session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
495
|
-
this.
|
|
496
|
-
.run(JSON.stringify(session.metadata), Date.now(), session.id);
|
|
341
|
+
this.appendMeta(channel, channelId, session);
|
|
497
342
|
}
|
|
498
343
|
return session;
|
|
499
344
|
}
|
|
500
|
-
//
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (active) {
|
|
345
|
+
// 使用精确路径解析(caller 提供了 channelType 时直接定位,避免扫描回落)
|
|
346
|
+
const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfId);
|
|
347
|
+
const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
|
|
348
|
+
const active = activeFile ? fileToSession(activeFile) : undefined;
|
|
349
|
+
if (active && !active.threadId) {
|
|
506
350
|
const validSessionId = this.validateSessionFile(active);
|
|
507
|
-
const session = { ...
|
|
351
|
+
const session = { ...active, agentSessionId: validSessionId };
|
|
508
352
|
session.identity = this.resolveIdentity(channel, userId);
|
|
509
|
-
|
|
353
|
+
let mutated = false;
|
|
510
354
|
if (chatType && session.chatType !== chatType) {
|
|
511
355
|
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
512
|
-
this.db.prepare(`UPDATE sessions SET chat_type = ?, updated_at = ? WHERE id = ?`)
|
|
513
|
-
.run(chatType, Date.now(), active.id);
|
|
514
356
|
session.chatType = chatType;
|
|
357
|
+
mutated = true;
|
|
358
|
+
}
|
|
359
|
+
if (selfId && session.selfId !== selfId) {
|
|
360
|
+
session.selfId = selfId;
|
|
361
|
+
mutated = true;
|
|
515
362
|
}
|
|
516
|
-
// 补写 peerId/peerName/channelName(旧 session 可能在这些字段引入前创建)
|
|
517
363
|
if (chatType === 'private' && userId) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
if (!activeMeta.peerName && metadata?.peerName) {
|
|
525
|
-
activeMeta.peerName = metadata.peerName;
|
|
526
|
-
updated = true;
|
|
364
|
+
if (!session.metadata)
|
|
365
|
+
session.metadata = {};
|
|
366
|
+
if (!session.metadata.peerId) {
|
|
367
|
+
session.metadata.peerId = userId;
|
|
368
|
+
mutated = true;
|
|
527
369
|
}
|
|
528
|
-
if (metadata
|
|
529
|
-
|
|
530
|
-
|
|
370
|
+
if (!session.metadata.peerName && metadata?.peerName) {
|
|
371
|
+
session.metadata.peerName = metadata.peerName;
|
|
372
|
+
mutated = true;
|
|
531
373
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
session.metadata = activeMeta;
|
|
374
|
+
if (metadata?.channelName && session.metadata.channelName !== metadata.channelName) {
|
|
375
|
+
session.metadata.channelName = metadata.channelName;
|
|
376
|
+
mutated = true;
|
|
536
377
|
}
|
|
537
378
|
}
|
|
538
|
-
// 补写 channelName(非私聊时也需要)
|
|
539
379
|
if (metadata?.channelName && chatType !== 'private') {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
session.metadata = activeMeta;
|
|
380
|
+
if (!session.metadata)
|
|
381
|
+
session.metadata = {};
|
|
382
|
+
if (session.metadata.channelName !== metadata.channelName) {
|
|
383
|
+
session.metadata.channelName = metadata.channelName;
|
|
384
|
+
mutated = true;
|
|
546
385
|
}
|
|
547
386
|
}
|
|
387
|
+
if (mutated) {
|
|
388
|
+
session.updatedAt = Date.now();
|
|
389
|
+
this.appendMeta(channel, channelId, session);
|
|
390
|
+
this.writeActive(channel, channelId, session);
|
|
391
|
+
}
|
|
548
392
|
return session;
|
|
549
393
|
}
|
|
550
|
-
//
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
394
|
+
// Find existing session for default project path
|
|
395
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
396
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
397
|
+
const existing = allSessions
|
|
398
|
+
.filter(s => s.projectPath === defaultProjectPath && !s.threadId)
|
|
399
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
556
400
|
if (existing) {
|
|
557
401
|
const validSessionId = this.validateSessionFile(existing);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
logger.info(`[SessionManager] Updating chatType for session ${existing.id}: ${existing.chat_type} -> ${chatType}`);
|
|
565
|
-
existing.chat_type = chatType;
|
|
566
|
-
}
|
|
567
|
-
// 补写 peerId/peerName
|
|
568
|
-
if (chatType === 'private' && userId && !existingMeta.peerId) {
|
|
569
|
-
existingMeta.peerId = userId;
|
|
402
|
+
const session = { ...existing, agentSessionId: validSessionId };
|
|
403
|
+
session.identity = this.resolveIdentity(channel, userId);
|
|
404
|
+
if (!session.metadata)
|
|
405
|
+
session.metadata = {};
|
|
406
|
+
if (selfId && session.selfId !== selfId) {
|
|
407
|
+
session.selfId = selfId;
|
|
570
408
|
}
|
|
571
|
-
if (chatType
|
|
572
|
-
|
|
409
|
+
if (chatType && session.chatType !== chatType) {
|
|
410
|
+
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
411
|
+
session.chatType = chatType;
|
|
573
412
|
}
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
.run(JSON.stringify(existingMeta), Date.now(), chatType, existing.id);
|
|
413
|
+
if (chatType === 'private' && userId && !session.metadata.peerId) {
|
|
414
|
+
session.metadata.peerId = userId;
|
|
577
415
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
.run(JSON.stringify(existingMeta), Date.now(), existing.id);
|
|
416
|
+
if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
|
|
417
|
+
session.metadata.peerName = metadata.peerName;
|
|
581
418
|
}
|
|
582
|
-
|
|
583
|
-
|
|
419
|
+
session.updatedAt = Date.now();
|
|
420
|
+
this.appendMeta(channel, channelId, session);
|
|
421
|
+
this.writeActive(channel, channelId, session);
|
|
584
422
|
return session;
|
|
585
423
|
}
|
|
586
|
-
//
|
|
587
|
-
const sessionMetadata = { ...metadata
|
|
424
|
+
// Create new session
|
|
425
|
+
const sessionMetadata = { ...(metadata || {}) };
|
|
426
|
+
if (!sessionMetadata.permissionMode)
|
|
427
|
+
sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
588
428
|
const session = {
|
|
589
|
-
id:
|
|
429
|
+
id: generateSessionId(),
|
|
590
430
|
channel,
|
|
431
|
+
channelType: channelType || channel,
|
|
591
432
|
channelId,
|
|
433
|
+
selfId,
|
|
592
434
|
projectPath: defaultProjectPath,
|
|
593
435
|
threadId: '',
|
|
594
436
|
agentId: agentId || 'claude',
|
|
@@ -597,14 +439,11 @@ export class SessionManager {
|
|
|
597
439
|
metadata: sessionMetadata,
|
|
598
440
|
name: name || '默认会话',
|
|
599
441
|
createdAt: Date.now(),
|
|
600
|
-
updatedAt: Date.now()
|
|
442
|
+
updatedAt: Date.now(),
|
|
601
443
|
};
|
|
602
444
|
session.identity = this.resolveIdentity(channel, userId);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
sessionMetadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
606
|
-
}
|
|
607
|
-
this.insertSession(session);
|
|
445
|
+
this.appendMeta(channel, channelId, session);
|
|
446
|
+
this.writeActive(channel, channelId, session);
|
|
608
447
|
this.eventBus.publish({
|
|
609
448
|
type: 'session:created',
|
|
610
449
|
sessionId: session.id,
|
|
@@ -613,70 +452,61 @@ export class SessionManager {
|
|
|
613
452
|
projectPath: defaultProjectPath,
|
|
614
453
|
name: session.name,
|
|
615
454
|
chatType: session.chatType,
|
|
616
|
-
timestamp: Date.now()
|
|
455
|
+
timestamp: Date.now(),
|
|
617
456
|
});
|
|
618
457
|
return session;
|
|
619
458
|
}
|
|
620
459
|
async updateSession(sessionId, updates) {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
if (updates.chatType !== undefined) {
|
|
624
|
-
sets.push('chat_type = ?');
|
|
625
|
-
values.push(updates.chatType);
|
|
626
|
-
}
|
|
627
|
-
if (updates.name !== undefined) {
|
|
628
|
-
sets.push('name = ?');
|
|
629
|
-
values.push(updates.name);
|
|
630
|
-
}
|
|
631
|
-
if (updates.sessionMode !== undefined) {
|
|
632
|
-
sets.push('session_mode = ?');
|
|
633
|
-
values.push(updates.sessionMode);
|
|
634
|
-
}
|
|
635
|
-
if (updates.metadata !== undefined) {
|
|
636
|
-
sets.push('metadata = ?');
|
|
637
|
-
values.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
|
|
638
|
-
}
|
|
639
|
-
if ('agentSessionId' in updates) {
|
|
640
|
-
sets.push('claude_session_id = ?');
|
|
641
|
-
values.push(updates.agentSessionId ?? null);
|
|
642
|
-
}
|
|
643
|
-
if (sets.length === 0)
|
|
460
|
+
const found = this.findSessionFileById(sessionId);
|
|
461
|
+
if (!found)
|
|
644
462
|
return;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
463
|
+
const current = this.readMetaLatest(found.metaPath);
|
|
464
|
+
if (!current)
|
|
465
|
+
return;
|
|
466
|
+
if (updates.chatType !== undefined)
|
|
467
|
+
current.chatType = updates.chatType;
|
|
468
|
+
if (updates.name !== undefined)
|
|
469
|
+
current.name = updates.name;
|
|
470
|
+
if (updates.sessionMode !== undefined)
|
|
471
|
+
current.sessionMode = updates.sessionMode;
|
|
472
|
+
if (updates.metadata !== undefined)
|
|
473
|
+
current.metadata = updates.metadata;
|
|
474
|
+
if ('agentSessionId' in updates)
|
|
475
|
+
current.agentSessionId = updates.agentSessionId ?? undefined;
|
|
476
|
+
current.updatedAt = Date.now();
|
|
477
|
+
this.appendMeta(current.channel, current.channelId, current);
|
|
478
|
+
const active = this.readActive(current.channel, current.channelId);
|
|
479
|
+
if (active && active.id === sessionId) {
|
|
480
|
+
this.writeActive(current.channel, current.channelId, current);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType) {
|
|
484
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId);
|
|
485
|
+
const threadIndex = readThreadIndex(chatDir);
|
|
486
|
+
const existingMetaId = threadIndex[threadId];
|
|
487
|
+
if (existingMetaId) {
|
|
488
|
+
const metaPath = path.join(chatDir, '_threads', `${existingMetaId}.jsonl`);
|
|
489
|
+
const existing = this.readMetaLatest(metaPath);
|
|
490
|
+
if (existing) {
|
|
491
|
+
const validSessionId = this.validateSessionFile(existing);
|
|
492
|
+
if (metadata) {
|
|
493
|
+
existing.metadata = { ...(existing.metadata || {}), ...metadata };
|
|
494
|
+
existing.updatedAt = Date.now();
|
|
495
|
+
this.appendMeta(channel, channelId, existing);
|
|
496
|
+
}
|
|
497
|
+
return { ...existing, agentSessionId: validSessionId };
|
|
665
498
|
}
|
|
666
|
-
return { ...this.rowToSession(existing), agentSessionId: validSessionId };
|
|
667
499
|
}
|
|
668
|
-
//
|
|
669
|
-
const activeMain = this.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
`).get(channel, channelId);
|
|
673
|
-
const projectPath = activeMain?.project_path || defaultProjectPath;
|
|
674
|
-
// 创建新话题会话
|
|
675
|
-
const inheritedChatType = activeMain?.chat_type || 'private';
|
|
500
|
+
// Inherit project path & chatType from active main session
|
|
501
|
+
const activeMain = this.readActive(channel, channelId);
|
|
502
|
+
const projectPath = (activeMain && !activeMain.threadId ? activeMain.projectPath : undefined) || defaultProjectPath;
|
|
503
|
+
const inheritedChatType = (activeMain && !activeMain.threadId ? activeMain.chatType : undefined) || 'private';
|
|
676
504
|
const session = {
|
|
677
|
-
id:
|
|
505
|
+
id: generateSessionId(),
|
|
678
506
|
channel,
|
|
507
|
+
channelType: channelType || channel,
|
|
679
508
|
channelId,
|
|
509
|
+
selfId,
|
|
680
510
|
projectPath,
|
|
681
511
|
threadId,
|
|
682
512
|
agentId: agentId || 'claude',
|
|
@@ -685,9 +515,11 @@ export class SessionManager {
|
|
|
685
515
|
metadata,
|
|
686
516
|
name: name || '话题会话',
|
|
687
517
|
createdAt: Date.now(),
|
|
688
|
-
updatedAt: Date.now()
|
|
518
|
+
updatedAt: Date.now(),
|
|
689
519
|
};
|
|
690
|
-
this.
|
|
520
|
+
this.appendMeta(channel, channelId, session);
|
|
521
|
+
threadIndex[threadId] = session.id;
|
|
522
|
+
writeThreadIndex(chatDir, threadIndex);
|
|
691
523
|
this.eventBus.publish({
|
|
692
524
|
type: 'session:created',
|
|
693
525
|
sessionId: session.id,
|
|
@@ -695,48 +527,45 @@ export class SessionManager {
|
|
|
695
527
|
channelId,
|
|
696
528
|
projectPath,
|
|
697
529
|
name: session.name,
|
|
698
|
-
timestamp: Date.now()
|
|
530
|
+
timestamp: Date.now(),
|
|
699
531
|
});
|
|
700
532
|
return session;
|
|
701
533
|
}
|
|
702
534
|
async switchProject(channel, channelId, newProjectPath, currentAgentId) {
|
|
703
535
|
const agentId = currentAgentId || 'claude';
|
|
704
536
|
logger.info(`[SessionManager] switchProject: channel=${channel} channelId=${channelId} newPath=${newProjectPath} agent=${agentId}`);
|
|
705
|
-
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
706
537
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
707
|
-
|
|
708
|
-
this.
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
|
|
713
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
714
|
-
`).get(channel, channelId, newProjectPath, agentId);
|
|
538
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId);
|
|
539
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
540
|
+
const target = allSessions
|
|
541
|
+
.filter(s => s.projectPath === newProjectPath && (s.agentId || 'claude') === agentId && !s.threadId)
|
|
542
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
715
543
|
if (target) {
|
|
716
544
|
const validSessionId = this.validateSessionFile(target);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
this.
|
|
721
|
-
|
|
722
|
-
return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
|
|
545
|
+
target.agentSessionId = validSessionId;
|
|
546
|
+
target.updatedAt = Date.now();
|
|
547
|
+
this.appendMeta(channel, channelId, target);
|
|
548
|
+
this.writeActive(channel, channelId, target);
|
|
549
|
+
return target;
|
|
723
550
|
}
|
|
724
|
-
// 4. 创建新会话
|
|
725
551
|
const session = {
|
|
726
|
-
id:
|
|
552
|
+
id: generateSessionId(),
|
|
727
553
|
channel,
|
|
554
|
+
channelType: this.inferChannelType(channel, channelId),
|
|
728
555
|
channelId,
|
|
556
|
+
selfId: this.inferSelfId(channel, channelId),
|
|
729
557
|
projectPath: newProjectPath,
|
|
730
558
|
threadId: '',
|
|
731
559
|
agentId,
|
|
732
560
|
chatType: inheritedChatType,
|
|
733
561
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
734
|
-
metadata: {
|
|
562
|
+
metadata: {},
|
|
735
563
|
name: '默认会话',
|
|
736
564
|
createdAt: Date.now(),
|
|
737
|
-
updatedAt: Date.now()
|
|
565
|
+
updatedAt: Date.now(),
|
|
738
566
|
};
|
|
739
|
-
this.
|
|
567
|
+
this.appendMeta(channel, channelId, session);
|
|
568
|
+
this.writeActive(channel, channelId, session);
|
|
740
569
|
this.eventBus.publish({
|
|
741
570
|
type: 'session:created',
|
|
742
571
|
sessionId: session.id,
|
|
@@ -744,63 +573,68 @@ export class SessionManager {
|
|
|
744
573
|
channelId,
|
|
745
574
|
projectPath: newProjectPath,
|
|
746
575
|
name: session.name,
|
|
747
|
-
timestamp: Date.now()
|
|
576
|
+
timestamp: Date.now(),
|
|
748
577
|
});
|
|
749
578
|
return session;
|
|
750
579
|
}
|
|
751
580
|
async updateAgentSessionId(channel, channelId, agentSessionId) {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
581
|
+
const active = this.readActive(channel, channelId);
|
|
582
|
+
if (!active)
|
|
583
|
+
return;
|
|
584
|
+
active.agentSessionId = agentSessionId;
|
|
585
|
+
active.updatedAt = Date.now();
|
|
586
|
+
this.appendMeta(channel, channelId, active);
|
|
587
|
+
this.writeActive(channel, channelId, active);
|
|
758
588
|
}
|
|
759
589
|
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
760
|
-
// 根据 sessionId 直接更新
|
|
761
590
|
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
762
|
-
this.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
591
|
+
const found = this.findSessionFileById(sessionId);
|
|
592
|
+
if (!found)
|
|
593
|
+
return;
|
|
594
|
+
const current = this.readMetaLatest(found.metaPath);
|
|
595
|
+
if (!current)
|
|
596
|
+
return;
|
|
597
|
+
current.agentSessionId = agentSessionId;
|
|
598
|
+
current.updatedAt = Date.now();
|
|
599
|
+
this.appendMeta(current.channel, current.channelId, current);
|
|
600
|
+
const active = this.readActive(current.channel, current.channelId);
|
|
601
|
+
if (active && active.id === sessionId) {
|
|
602
|
+
this.writeActive(current.channel, current.channelId, current);
|
|
603
|
+
}
|
|
767
604
|
}
|
|
768
605
|
async switchAgent(channel, channelId, projectPath, newAgentId) {
|
|
769
|
-
// 1. 继承当前 chatType(在 deactivate 之前读取)
|
|
770
606
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
771
|
-
|
|
772
|
-
this.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
WHERE channel = ? AND channel_id = ? AND project_path = ? AND agent_id = ? AND thread_id = '' AND deleted_at IS NULL
|
|
777
|
-
ORDER BY updated_at DESC LIMIT 1
|
|
778
|
-
`).get(channel, channelId, projectPath, newAgentId);
|
|
607
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId);
|
|
608
|
+
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
609
|
+
const target = allSessions
|
|
610
|
+
.filter(s => s.projectPath === projectPath && (s.agentId || 'claude') === newAgentId && !s.threadId)
|
|
611
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
779
612
|
if (target) {
|
|
780
613
|
const validSessionId = this.validateSessionFile(target);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
this.
|
|
785
|
-
|
|
786
|
-
return { ...this.rowToSession(target), agentSessionId: validSessionId, metadata };
|
|
614
|
+
target.agentSessionId = validSessionId;
|
|
615
|
+
target.updatedAt = Date.now();
|
|
616
|
+
this.appendMeta(channel, channelId, target);
|
|
617
|
+
this.writeActive(channel, channelId, target);
|
|
618
|
+
return target;
|
|
787
619
|
}
|
|
788
|
-
// 4. 创建新会话(与 switchProject 保持一致)
|
|
789
620
|
const session = {
|
|
790
|
-
id:
|
|
621
|
+
id: generateSessionId(),
|
|
791
622
|
channel,
|
|
623
|
+
channelType: this.inferChannelType(channel, channelId),
|
|
792
624
|
channelId,
|
|
625
|
+
selfId: this.inferSelfId(channel, channelId),
|
|
793
626
|
projectPath,
|
|
794
627
|
threadId: '',
|
|
795
628
|
agentId: newAgentId,
|
|
796
629
|
chatType: inheritedChatType,
|
|
797
630
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
798
|
-
metadata: {
|
|
631
|
+
metadata: {},
|
|
799
632
|
name: '默认会话',
|
|
800
633
|
createdAt: Date.now(),
|
|
801
|
-
updatedAt: Date.now()
|
|
634
|
+
updatedAt: Date.now(),
|
|
802
635
|
};
|
|
803
|
-
this.
|
|
636
|
+
this.appendMeta(channel, channelId, session);
|
|
637
|
+
this.writeActive(channel, channelId, session);
|
|
804
638
|
this.eventBus.publish({
|
|
805
639
|
type: 'session:created',
|
|
806
640
|
sessionId: session.id,
|
|
@@ -808,144 +642,244 @@ export class SessionManager {
|
|
|
808
642
|
channelId,
|
|
809
643
|
projectPath,
|
|
810
644
|
name: session.name,
|
|
811
|
-
timestamp: Date.now()
|
|
645
|
+
timestamp: Date.now(),
|
|
812
646
|
});
|
|
813
647
|
return session;
|
|
814
648
|
}
|
|
815
649
|
async clearActiveSession(channel, channelId) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
650
|
+
const active = this.readActive(channel, channelId);
|
|
651
|
+
if (!active)
|
|
652
|
+
return;
|
|
653
|
+
active.agentSessionId = undefined;
|
|
654
|
+
active.updatedAt = Date.now();
|
|
655
|
+
this.appendMeta(channel, channelId, active);
|
|
656
|
+
this.writeActive(channel, channelId, active);
|
|
657
|
+
}
|
|
824
658
|
getOwnerChatId(targetChannel, ownerPeerId) {
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
659
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
660
|
+
let bestMatch;
|
|
661
|
+
for (const { channelId, dirPath } of chatDirs) {
|
|
662
|
+
// Check active.json first
|
|
663
|
+
const active = readJsonFile(path.join(dirPath, 'active.json'));
|
|
664
|
+
// 按 channel 实例名匹配(active.json 里有;缺失就跳过这个 chat)
|
|
665
|
+
if (!active || active.channel !== targetChannel)
|
|
666
|
+
continue;
|
|
667
|
+
const candidates = [active];
|
|
668
|
+
// Also scan meta files
|
|
669
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
670
|
+
const file = readLastJsonlLine(path.join(dirPath, metaFile));
|
|
671
|
+
if (file)
|
|
672
|
+
candidates.push(file);
|
|
673
|
+
}
|
|
674
|
+
for (const cand of candidates) {
|
|
675
|
+
if (cand.chatType !== 'private')
|
|
676
|
+
continue;
|
|
677
|
+
if (cand.metadata?.peerId !== ownerPeerId)
|
|
678
|
+
continue;
|
|
679
|
+
if (!bestMatch || cand.updatedAt > bestMatch.updatedAt) {
|
|
680
|
+
bestMatch = { channelId, updatedAt: cand.updatedAt };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return bestMatch?.channelId;
|
|
833
685
|
}
|
|
834
686
|
async getSessionById(sessionId) {
|
|
835
|
-
const
|
|
836
|
-
if (!
|
|
687
|
+
const found = this.findSessionFileById(sessionId);
|
|
688
|
+
if (!found)
|
|
837
689
|
return undefined;
|
|
838
|
-
return this.
|
|
690
|
+
return this.readMetaLatest(found.metaPath);
|
|
839
691
|
}
|
|
840
692
|
async getActiveSession(channel, channelId) {
|
|
841
|
-
|
|
842
|
-
SELECT * FROM sessions
|
|
843
|
-
WHERE channel = ? AND channel_id = ? AND json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL
|
|
844
|
-
`).get(channel, channelId);
|
|
845
|
-
if (!row)
|
|
846
|
-
return undefined;
|
|
847
|
-
return this.rowToSession(row);
|
|
693
|
+
return this.readActive(channel, channelId);
|
|
848
694
|
}
|
|
849
|
-
/**
|
|
850
|
-
* 查询话题会话(不创建)
|
|
851
|
-
*/
|
|
852
695
|
async getThreadSession(channel, channelId, threadId) {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
696
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
697
|
+
const threadIndex = readThreadIndex(chatDir);
|
|
698
|
+
const metaId = threadIndex[threadId];
|
|
699
|
+
if (!metaId)
|
|
700
|
+
return undefined;
|
|
701
|
+
const metaPath = path.join(chatDir, '_threads', `${metaId}.jsonl`);
|
|
702
|
+
const session = this.readMetaLatest(metaPath);
|
|
703
|
+
if (!session)
|
|
858
704
|
return undefined;
|
|
859
|
-
const validSessionId = this.validateSessionFile(
|
|
860
|
-
return { ...
|
|
705
|
+
const validSessionId = this.validateSessionFile(session);
|
|
706
|
+
return { ...session, agentSessionId: validSessionId };
|
|
861
707
|
}
|
|
862
708
|
async listSessions(channel, channelId) {
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
WHERE channel = ? AND channel_id = ? AND deleted_at IS NULL
|
|
867
|
-
ORDER BY updated_at DESC
|
|
868
|
-
`).all(channel, channelId);
|
|
869
|
-
return rows.map(row => this.rowToSession(row));
|
|
709
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
710
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
711
|
+
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
870
712
|
}
|
|
871
713
|
async getSessionByProjectPath(channel, channelId, projectPath) {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
LIMIT 1
|
|
877
|
-
`).get(channel, channelId, projectPath);
|
|
878
|
-
if (!row)
|
|
714
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
715
|
+
const sessions = this.findAllSessionsInChat(chatDir, false);
|
|
716
|
+
const matched = sessions.filter(s => s.projectPath === projectPath);
|
|
717
|
+
if (matched.length === 0)
|
|
879
718
|
return undefined;
|
|
880
|
-
|
|
719
|
+
matched.sort((a, b) => {
|
|
720
|
+
const aProc = a.processingState ? 1 : 0;
|
|
721
|
+
const bProc = b.processingState ? 1 : 0;
|
|
722
|
+
if (aProc !== bProc)
|
|
723
|
+
return bProc - aProc;
|
|
724
|
+
return b.updatedAt - a.updatedAt;
|
|
725
|
+
});
|
|
726
|
+
return matched[0];
|
|
881
727
|
}
|
|
882
728
|
async getSessionByName(channel, channelId, name) {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
`).get(channel, channelId, name);
|
|
887
|
-
if (!row)
|
|
888
|
-
return undefined;
|
|
889
|
-
return this.rowToSession(row);
|
|
729
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
730
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
731
|
+
return sessions.find(s => s.name === name);
|
|
890
732
|
}
|
|
891
733
|
async switchToSession(channel, channelId, targetSessionId) {
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
`).get(targetSessionId, channel, channelId);
|
|
734
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
735
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
736
|
+
const target = sessions.find(s => s.id === targetSessionId);
|
|
896
737
|
if (!target)
|
|
897
738
|
return null;
|
|
898
|
-
|
|
899
|
-
this.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
metadata.isActive = true;
|
|
903
|
-
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
904
|
-
.run(JSON.stringify(metadata), Date.now(), targetSessionId);
|
|
905
|
-
return { ...this.rowToSession(target), metadata, updatedAt: Date.now() };
|
|
739
|
+
target.updatedAt = Date.now();
|
|
740
|
+
this.appendMeta(channel, channelId, target);
|
|
741
|
+
this.writeActive(channel, channelId, target);
|
|
742
|
+
return target;
|
|
906
743
|
}
|
|
907
744
|
updateMetadata(sessionId, metadata) {
|
|
908
|
-
|
|
909
|
-
|
|
745
|
+
const found = this.findSessionFileById(sessionId);
|
|
746
|
+
if (!found)
|
|
747
|
+
return;
|
|
748
|
+
const current = this.readMetaLatest(found.metaPath);
|
|
749
|
+
if (!current)
|
|
750
|
+
return;
|
|
751
|
+
current.metadata = metadata;
|
|
752
|
+
current.updatedAt = Date.now();
|
|
753
|
+
this.appendMeta(current.channel, current.channelId, current);
|
|
754
|
+
const active = this.readActive(current.channel, current.channelId);
|
|
755
|
+
if (active && active.id === sessionId) {
|
|
756
|
+
this.writeActive(current.channel, current.channelId, current);
|
|
757
|
+
}
|
|
910
758
|
}
|
|
911
759
|
async renameSession(sessionId, newName) {
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
760
|
+
const found = this.findSessionFileById(sessionId);
|
|
761
|
+
if (!found)
|
|
762
|
+
return false;
|
|
763
|
+
const current = this.readMetaLatest(found.metaPath);
|
|
764
|
+
if (!current)
|
|
765
|
+
return false;
|
|
766
|
+
current.name = newName;
|
|
767
|
+
current.updatedAt = Date.now();
|
|
768
|
+
this.appendMeta(current.channel, current.channelId, current);
|
|
769
|
+
const active = this.readActive(current.channel, current.channelId);
|
|
770
|
+
if (active && active.id === sessionId) {
|
|
771
|
+
this.writeActive(current.channel, current.channelId, current);
|
|
772
|
+
}
|
|
773
|
+
return true;
|
|
916
774
|
}
|
|
917
775
|
async unbindSession(sessionId) {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
776
|
+
const found = this.findSessionFileById(sessionId);
|
|
777
|
+
if (!found)
|
|
778
|
+
return false;
|
|
779
|
+
const trashDir = path.join(found.chatDir, '_trash');
|
|
780
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
781
|
+
const trashPath = path.join(trashDir, path.basename(found.metaPath));
|
|
782
|
+
try {
|
|
783
|
+
fs.renameSync(found.metaPath, trashPath);
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
if (e.code !== 'ENOENT')
|
|
787
|
+
throw e;
|
|
788
|
+
}
|
|
789
|
+
// If thread session, remove from thread-index
|
|
790
|
+
if (found.isThread) {
|
|
791
|
+
const threadIndex = readThreadIndex(found.chatDir);
|
|
792
|
+
for (const [tid, mid] of Object.entries(threadIndex)) {
|
|
793
|
+
if (mid === sessionId) {
|
|
794
|
+
delete threadIndex[tid];
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
writeThreadIndex(found.chatDir, threadIndex);
|
|
799
|
+
}
|
|
800
|
+
// Clear active.json if it pointed to this session
|
|
801
|
+
const activePath = path.join(found.chatDir, 'active.json');
|
|
802
|
+
const active = readJsonFile(activePath);
|
|
803
|
+
if (active && active.id === sessionId) {
|
|
804
|
+
try {
|
|
805
|
+
fs.unlinkSync(activePath);
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
if (e.code !== 'ENOENT')
|
|
809
|
+
throw e;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return true;
|
|
922
813
|
}
|
|
923
814
|
async softDeleteSession(channelId) {
|
|
924
|
-
this.
|
|
925
|
-
|
|
926
|
-
|
|
815
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
816
|
+
for (const { channelId: cid, dirPath } of chatDirs) {
|
|
817
|
+
if (cid !== channelId)
|
|
818
|
+
continue;
|
|
819
|
+
const trashDir = path.join(dirPath, '_trash');
|
|
820
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
821
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
822
|
+
const src = path.join(dirPath, metaFile);
|
|
823
|
+
const dst = path.join(trashDir, metaFile);
|
|
824
|
+
try {
|
|
825
|
+
fs.renameSync(src, dst);
|
|
826
|
+
}
|
|
827
|
+
catch (e) {
|
|
828
|
+
if (e.code !== 'ENOENT')
|
|
829
|
+
throw e;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
833
|
+
for (const metaFile of scanMetaFiles(threadsDir)) {
|
|
834
|
+
const src = path.join(threadsDir, metaFile);
|
|
835
|
+
const dst = path.join(trashDir, metaFile);
|
|
836
|
+
try {
|
|
837
|
+
fs.renameSync(src, dst);
|
|
838
|
+
}
|
|
839
|
+
catch (e) {
|
|
840
|
+
if (e.code !== 'ENOENT')
|
|
841
|
+
throw e;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Clear active.json
|
|
845
|
+
const activePath = path.join(dirPath, 'active.json');
|
|
846
|
+
try {
|
|
847
|
+
fs.unlinkSync(activePath);
|
|
848
|
+
}
|
|
849
|
+
catch (e) {
|
|
850
|
+
if (e.code !== 'ENOENT')
|
|
851
|
+
throw e;
|
|
852
|
+
}
|
|
853
|
+
// Clear thread index
|
|
854
|
+
try {
|
|
855
|
+
fs.unlinkSync(path.join(threadsDir, 'thread-index.json'));
|
|
856
|
+
}
|
|
857
|
+
catch (e) {
|
|
858
|
+
if (e.code !== 'ENOENT')
|
|
859
|
+
throw e;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
927
862
|
}
|
|
928
863
|
async createNewSession(channel, channelId, projectPath, name, agentId) {
|
|
929
|
-
// 继承当前 chatType(在 deactivate 之前读取)
|
|
930
864
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
931
|
-
// 取消当前活跃会话
|
|
932
|
-
this.deactivateAllMetadata(channel, channelId);
|
|
933
|
-
// 创建新会话
|
|
934
865
|
const session = {
|
|
935
|
-
id:
|
|
866
|
+
id: generateSessionId(),
|
|
936
867
|
channel,
|
|
868
|
+
channelType: this.inferChannelType(channel, channelId),
|
|
937
869
|
channelId,
|
|
870
|
+
selfId: this.inferSelfId(channel, channelId),
|
|
938
871
|
projectPath,
|
|
939
872
|
threadId: '',
|
|
940
873
|
agentId: agentId || 'claude',
|
|
941
874
|
chatType: inheritedChatType,
|
|
942
875
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
943
|
-
metadata: {
|
|
876
|
+
metadata: {},
|
|
944
877
|
name: name || '默认会话',
|
|
945
878
|
createdAt: Date.now(),
|
|
946
|
-
updatedAt: Date.now()
|
|
879
|
+
updatedAt: Date.now(),
|
|
947
880
|
};
|
|
948
|
-
this.
|
|
881
|
+
this.appendMeta(channel, channelId, session);
|
|
882
|
+
this.writeActive(channel, channelId, session);
|
|
949
883
|
this.eventBus.publish({
|
|
950
884
|
type: 'session:created',
|
|
951
885
|
sessionId: session.id,
|
|
@@ -953,32 +887,30 @@ export class SessionManager {
|
|
|
953
887
|
channelId,
|
|
954
888
|
projectPath,
|
|
955
889
|
name: session.name,
|
|
956
|
-
timestamp: Date.now()
|
|
890
|
+
timestamp: Date.now(),
|
|
957
891
|
});
|
|
958
892
|
return session;
|
|
959
893
|
}
|
|
960
|
-
/**
|
|
961
|
-
* 基于现有会话创建分支会话
|
|
962
|
-
*/
|
|
963
894
|
async createForkedSession(sourceSession, forkedAgentSessionId, name) {
|
|
964
|
-
// 取消当前活跃会话
|
|
965
|
-
this.deactivateAllMetadata(sourceSession.channel, sourceSession.channelId);
|
|
966
895
|
const session = {
|
|
967
|
-
id:
|
|
896
|
+
id: generateSessionId(),
|
|
968
897
|
channel: sourceSession.channel,
|
|
898
|
+
channelType: sourceSession.channelType || sourceSession.channel,
|
|
969
899
|
channelId: sourceSession.channelId,
|
|
900
|
+
selfId: sourceSession.selfId,
|
|
970
901
|
projectPath: sourceSession.projectPath,
|
|
971
902
|
threadId: sourceSession.threadId || '',
|
|
972
903
|
agentId: sourceSession.agentId || 'claude',
|
|
973
904
|
chatType: sourceSession.chatType || 'private',
|
|
974
905
|
sessionMode: sourceSession.sessionMode || 'interactive',
|
|
975
906
|
agentSessionId: forkedAgentSessionId,
|
|
976
|
-
metadata: {
|
|
907
|
+
metadata: {},
|
|
977
908
|
name: name || `${sourceSession.name || '会话'}-分支`,
|
|
978
909
|
createdAt: Date.now(),
|
|
979
|
-
updatedAt: Date.now()
|
|
910
|
+
updatedAt: Date.now(),
|
|
980
911
|
};
|
|
981
|
-
this.
|
|
912
|
+
this.appendMeta(sourceSession.channel, sourceSession.channelId, session);
|
|
913
|
+
this.writeActive(sourceSession.channel, sourceSession.channelId, session);
|
|
982
914
|
this.eventBus.publish({
|
|
983
915
|
type: 'session:created',
|
|
984
916
|
sessionId: session.id,
|
|
@@ -986,7 +918,7 @@ export class SessionManager {
|
|
|
986
918
|
channelId: sourceSession.channelId,
|
|
987
919
|
projectPath: sourceSession.projectPath,
|
|
988
920
|
name: session.name,
|
|
989
|
-
timestamp: Date.now()
|
|
921
|
+
timestamp: Date.now(),
|
|
990
922
|
});
|
|
991
923
|
return session;
|
|
992
924
|
}
|
|
@@ -1014,18 +946,12 @@ export class SessionManager {
|
|
|
1014
946
|
return null;
|
|
1015
947
|
return adapter.readLastUserMessage(projectPath, agentSessionId);
|
|
1016
948
|
}
|
|
1017
|
-
/**
|
|
1018
|
-
* 获取会话文件信息(回合数 + 标题)
|
|
1019
|
-
*/
|
|
1020
949
|
getSessionFileInfo(projectPath, agentSessionId, agentId) {
|
|
1021
950
|
const adapter = this.getFileAdapter(agentId);
|
|
1022
951
|
if (!adapter)
|
|
1023
952
|
return { turns: 0 };
|
|
1024
953
|
return adapter.getFileInfo(projectPath, agentSessionId);
|
|
1025
954
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* 列出 SDK 侧的会话列表(用于名称同步)
|
|
1028
|
-
*/
|
|
1029
955
|
async listSdkSessions(projectPath, agentId) {
|
|
1030
956
|
const adapter = this.getFileAdapter(agentId);
|
|
1031
957
|
if (!adapter?.listSdkSessions)
|
|
@@ -1033,42 +959,39 @@ export class SessionManager {
|
|
|
1033
959
|
return adapter.listSdkSessions(projectPath);
|
|
1034
960
|
}
|
|
1035
961
|
async getSessionByUuidPrefix(channel, channelId, uuidPrefix) {
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (rows.length === 0)
|
|
962
|
+
const chatDir = this.resolveChatDir(channel, channelId);
|
|
963
|
+
const sessions = this.findAllSessionsInChat(chatDir, true);
|
|
964
|
+
const matched = sessions.filter(s => s.agentSessionId && s.agentSessionId.startsWith(uuidPrefix));
|
|
965
|
+
if (matched.length === 0)
|
|
1041
966
|
return undefined;
|
|
1042
|
-
if (
|
|
967
|
+
if (matched.length > 1) {
|
|
1043
968
|
logger.warn(`Multiple sessions found with UUID prefix: ${uuidPrefix}`);
|
|
1044
969
|
}
|
|
1045
|
-
return
|
|
970
|
+
return matched[0];
|
|
1046
971
|
}
|
|
1047
972
|
async importCliSession(channel, channelId, projectPath, agentSessionId, agentId = 'claude') {
|
|
1048
|
-
// 继承当前 chatType(在 deactivate 之前读取)
|
|
1049
973
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1050
|
-
// 取消当前活跃会话
|
|
1051
|
-
this.deactivateAllMetadata(channel, channelId);
|
|
1052
|
-
// 从 CLI 会话文件读取标题
|
|
1053
974
|
const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId, agentId);
|
|
1054
975
|
const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
|
1055
|
-
// 创建新会话记录
|
|
1056
976
|
const session = {
|
|
1057
|
-
id:
|
|
977
|
+
id: generateSessionId(),
|
|
1058
978
|
channel,
|
|
979
|
+
channelType: this.inferChannelType(channel, channelId),
|
|
1059
980
|
channelId,
|
|
981
|
+
selfId: this.inferSelfId(channel, channelId),
|
|
1060
982
|
projectPath,
|
|
1061
983
|
threadId: '',
|
|
1062
984
|
agentId,
|
|
1063
985
|
chatType: inheritedChatType,
|
|
1064
986
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
1065
987
|
agentSessionId,
|
|
1066
|
-
metadata: {
|
|
988
|
+
metadata: {},
|
|
1067
989
|
name,
|
|
1068
990
|
createdAt: Date.now(),
|
|
1069
|
-
updatedAt: Date.now()
|
|
991
|
+
updatedAt: Date.now(),
|
|
1070
992
|
};
|
|
1071
|
-
this.
|
|
993
|
+
this.appendMeta(channel, channelId, session);
|
|
994
|
+
this.writeActive(channel, channelId, session);
|
|
1072
995
|
this.eventBus.publish({
|
|
1073
996
|
type: 'session:created',
|
|
1074
997
|
sessionId: session.id,
|
|
@@ -1076,111 +999,117 @@ export class SessionManager {
|
|
|
1076
999
|
channelId,
|
|
1077
1000
|
projectPath,
|
|
1078
1001
|
name,
|
|
1079
|
-
timestamp: Date.now()
|
|
1002
|
+
timestamp: Date.now(),
|
|
1080
1003
|
});
|
|
1081
1004
|
return session;
|
|
1082
1005
|
}
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1006
|
+
// ─── Health status ───
|
|
1007
|
+
healthFilePath(channel, channelId) {
|
|
1008
|
+
return path.join(this.ensureResolvedChatDir(channel, channelId), 'health.jsonl');
|
|
1009
|
+
}
|
|
1010
|
+
/** Find the chat dir containing a given session id */
|
|
1011
|
+
chatDirForSession(sessionId) {
|
|
1012
|
+
const found = this.findSessionFileById(sessionId);
|
|
1013
|
+
if (!found)
|
|
1014
|
+
return undefined;
|
|
1015
|
+
// 从 meta jsonl 最后一行读 channel 实例名 + channelId
|
|
1016
|
+
const meta = readLastJsonlLine(found.metaPath);
|
|
1017
|
+
if (!meta)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
return { channel: meta.channel, channelId: meta.channelId, dirPath: found.chatDir };
|
|
1020
|
+
}
|
|
1087
1021
|
async getHealthStatus(sessionId) {
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1022
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1023
|
+
if (!chatInfo) {
|
|
1024
|
+
return { consecutiveErrors: 0, safeMode: false, lastSuccessTime: Date.now() };
|
|
1025
|
+
}
|
|
1026
|
+
const healthPath = path.join(chatInfo.dirPath, 'health.jsonl');
|
|
1027
|
+
const records = readAllJsonlLines(healthPath).filter(r => r.sessionId === sessionId);
|
|
1028
|
+
let consecutiveErrors = 0;
|
|
1029
|
+
let lastError;
|
|
1030
|
+
let lastErrorType;
|
|
1031
|
+
let lastSuccessTime = 0;
|
|
1032
|
+
// Walk from tail to count consecutive errors
|
|
1033
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
1034
|
+
const rec = records[i];
|
|
1035
|
+
if (rec.type === 'error') {
|
|
1036
|
+
consecutiveErrors++;
|
|
1037
|
+
if (lastError === undefined) {
|
|
1038
|
+
lastError = rec.error;
|
|
1039
|
+
lastErrorType = rec.errorType;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
// success or reset breaks the streak
|
|
1044
|
+
if (rec.type === 'success' && rec.at > lastSuccessTime)
|
|
1045
|
+
lastSuccessTime = rec.at;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Find most recent success time across all records
|
|
1050
|
+
if (lastSuccessTime === 0) {
|
|
1051
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
1052
|
+
if (records[i].type === 'success' && records[i].at > lastSuccessTime) {
|
|
1053
|
+
lastSuccessTime = records[i].at;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1103
1057
|
}
|
|
1058
|
+
if (lastSuccessTime === 0)
|
|
1059
|
+
lastSuccessTime = Date.now();
|
|
1104
1060
|
return {
|
|
1105
|
-
consecutiveErrors
|
|
1106
|
-
lastError
|
|
1107
|
-
lastErrorType
|
|
1108
|
-
safeMode:
|
|
1109
|
-
lastSuccessTime
|
|
1061
|
+
consecutiveErrors,
|
|
1062
|
+
lastError,
|
|
1063
|
+
lastErrorType,
|
|
1064
|
+
safeMode: false,
|
|
1065
|
+
lastSuccessTime,
|
|
1110
1066
|
};
|
|
1111
1067
|
}
|
|
1112
|
-
/** 当前处于安全模式的会话数 */
|
|
1113
|
-
getSafeModeSessionCount() {
|
|
1114
|
-
const row = this.db.prepare(`SELECT COUNT(*) as count FROM session_health WHERE safe_mode = 1`).get();
|
|
1115
|
-
return row?.count ?? 0;
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* 记录成功响应(重置错误计数)
|
|
1119
|
-
*/
|
|
1120
1068
|
async recordSuccess(sessionId) {
|
|
1069
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1070
|
+
if (!chatInfo)
|
|
1071
|
+
return;
|
|
1121
1072
|
const now = Date.now();
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
last_success_time = ?,
|
|
1130
|
-
updated_at = ?
|
|
1131
|
-
`).run(sessionId, now, now, now, now, now);
|
|
1073
|
+
const record = {
|
|
1074
|
+
type: 'success',
|
|
1075
|
+
sessionId,
|
|
1076
|
+
at: now,
|
|
1077
|
+
atStr: formatTimestamp(now),
|
|
1078
|
+
};
|
|
1079
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1132
1080
|
}
|
|
1133
|
-
/**
|
|
1134
|
-
* 记录错误(增加计数)
|
|
1135
|
-
*/
|
|
1136
1081
|
async recordError(sessionId, errorType, errorMessage) {
|
|
1082
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1083
|
+
if (!chatInfo)
|
|
1084
|
+
return 0;
|
|
1137
1085
|
const now = Date.now();
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
return newCount;
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* 设置安全模式
|
|
1153
|
-
*/
|
|
1154
|
-
async setSafeMode(sessionId, enabled) {
|
|
1155
|
-
const now = Date.now();
|
|
1156
|
-
const health = await this.getHealthStatus(sessionId);
|
|
1157
|
-
this.db.prepare(`
|
|
1158
|
-
INSERT INTO session_health (session_id, consecutive_errors, safe_mode, last_success_time, created_at, updated_at)
|
|
1159
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
1160
|
-
ON CONFLICT(session_id) DO UPDATE SET
|
|
1161
|
-
safe_mode = ?,
|
|
1162
|
-
updated_at = ?
|
|
1163
|
-
`).run(sessionId, health.consecutiveErrors, enabled ? 1 : 0, health.lastSuccessTime, now, now, enabled ? 1 : 0, now);
|
|
1086
|
+
const record = {
|
|
1087
|
+
type: 'error',
|
|
1088
|
+
sessionId,
|
|
1089
|
+
errorType,
|
|
1090
|
+
error: errorMessage,
|
|
1091
|
+
at: now,
|
|
1092
|
+
atStr: formatTimestamp(now),
|
|
1093
|
+
};
|
|
1094
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1095
|
+
const status = await this.getHealthStatus(sessionId);
|
|
1096
|
+
return status.consecutiveErrors;
|
|
1164
1097
|
}
|
|
1165
|
-
/**
|
|
1166
|
-
* 重置健康状态(用于修复后)
|
|
1167
|
-
*/
|
|
1168
1098
|
async resetHealthStatus(sessionId) {
|
|
1099
|
+
const chatInfo = this.chatDirForSession(sessionId);
|
|
1100
|
+
if (!chatInfo)
|
|
1101
|
+
return;
|
|
1169
1102
|
const now = Date.now();
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
safe_mode = 0,
|
|
1178
|
-
updated_at = ?
|
|
1179
|
-
`).run(sessionId, now, now, now, now);
|
|
1103
|
+
const record = {
|
|
1104
|
+
type: 'reset',
|
|
1105
|
+
sessionId,
|
|
1106
|
+
at: now,
|
|
1107
|
+
atStr: formatTimestamp(now),
|
|
1108
|
+
};
|
|
1109
|
+
appendJsonl(path.join(chatInfo.dirPath, 'health.jsonl'), record);
|
|
1180
1110
|
}
|
|
1181
1111
|
close() {
|
|
1182
1112
|
for (const adapter of this.fileAdapters.values())
|
|
1183
1113
|
adapter.close?.();
|
|
1184
|
-
this.db.close();
|
|
1185
1114
|
}
|
|
1186
1115
|
}
|