evolclaw 3.1.4 → 3.1.6
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/CHANGELOG.md +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
|
@@ -4,6 +4,7 @@ import { logger } from '../../utils/logger.js';
|
|
|
4
4
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
5
5
|
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
6
6
|
import { sessionToFile, fileToSession } from './session-mapper.js';
|
|
7
|
+
import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import fs from 'fs';
|
|
9
10
|
import os from 'os';
|
|
@@ -21,6 +22,7 @@ export class SessionManager {
|
|
|
21
22
|
this.eventBus = eventBus;
|
|
22
23
|
this.ownerResolver = ownerResolver;
|
|
23
24
|
this.adminResolver = adminResolver;
|
|
25
|
+
this.migrateChannelKeyFormat();
|
|
24
26
|
}
|
|
25
27
|
setOwnerResolver(resolver) {
|
|
26
28
|
this.ownerResolver = resolver;
|
|
@@ -87,83 +89,39 @@ export class SessionManager {
|
|
|
87
89
|
// ─── File I/O helpers ───
|
|
88
90
|
/**
|
|
89
91
|
* 解析 chat 目录路径。
|
|
90
|
-
*
|
|
91
|
-
* 2. 找不到则按 fallback:channelType=channel(实例名),selfId=null
|
|
92
|
-
*
|
|
93
|
-
* 这样保持兼容:不知道 channelType 的 caller 仍可以用 (channel, channelId) 调用。
|
|
92
|
+
* 需要明确的 channelType + selfAID 才能确定路径。
|
|
94
93
|
*/
|
|
95
|
-
resolveChatDir(channel, channelId, channelType,
|
|
96
|
-
|
|
97
|
-
if (!channelType) {
|
|
98
|
-
throw new Error(`[SessionManager] resolveChatDir requires channelType. Got channel="${channel}" channelId="${channelId}". Caller must pass channelType (e.g. 'aun', 'feishu').`);
|
|
99
|
-
}
|
|
100
|
-
return chatDirPath(this.sessionsDir, channelType, channelId, selfId);
|
|
94
|
+
resolveChatDir(channel, channelId, channelType, selfAID) {
|
|
95
|
+
return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
|
|
101
96
|
}
|
|
102
97
|
/**
|
|
103
|
-
* 给定明确的 channelType +
|
|
98
|
+
* 给定明确的 channelType + selfAID 时直接计算路径(不扫描)。
|
|
104
99
|
* 用于 caller 已经知道完整路由信息的场景(如 message-bridge 透传)。
|
|
105
100
|
*/
|
|
106
|
-
resolveChatDirExact(channel, channelId, channelType,
|
|
107
|
-
|
|
108
|
-
return chatDirPath(this.sessionsDir, channelType, channelId, selfId);
|
|
109
|
-
}
|
|
110
|
-
return this.resolveChatDirSafe(channel, channelId);
|
|
101
|
+
resolveChatDirExact(channel, channelId, channelType, selfAID) {
|
|
102
|
+
return chatDirPath(this.sessionsDir, channelType, channelId, selfAID);
|
|
111
103
|
}
|
|
112
104
|
resolveChatDirFromSession(session) {
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
if (!session.channelType) {
|
|
106
|
+
throw new Error(`[SessionManager] missing channelType for session ${session.id}`);
|
|
107
|
+
}
|
|
108
|
+
return chatDirPath(this.sessionsDir, session.channelType, session.channelId, session.selfAID);
|
|
115
109
|
}
|
|
116
110
|
/** Public accessor: get the chat directory path for a session (for message log etc.) */
|
|
117
111
|
getChatDir(session) {
|
|
118
112
|
return this.resolveChatDirFromSession(session);
|
|
119
113
|
}
|
|
120
114
|
/** Like resolveChatDir but also ensures the dir + _threads + _trash exist. */
|
|
121
|
-
ensureResolvedChatDir(channel, channelId, channelType,
|
|
122
|
-
const dir = this.resolveChatDir(channel, channelId, channelType,
|
|
115
|
+
ensureResolvedChatDir(channel, channelId, channelType, selfAID) {
|
|
116
|
+
const dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
123
117
|
fs.mkdirSync(dir, { recursive: true });
|
|
124
118
|
fs.mkdirSync(path.join(dir, '_threads'), { recursive: true });
|
|
125
119
|
fs.mkdirSync(path.join(dir, '_trash'), { recursive: true });
|
|
126
120
|
return dir;
|
|
127
121
|
}
|
|
128
|
-
/** 推断给定 chat 的 channelType(优先取 active.json)。无活跃时回落到 channel 实例名。 */
|
|
129
|
-
inferChannelType(channel, channelId, chatDir) {
|
|
130
|
-
if (chatDir) {
|
|
131
|
-
const active = readJsonFile(path.join(chatDir, 'active.json'));
|
|
132
|
-
if (active?.channelType)
|
|
133
|
-
return active.channelType;
|
|
134
|
-
}
|
|
135
|
-
// 扫描已有目录
|
|
136
|
-
const dirs = scanChatDirs(this.sessionsDir);
|
|
137
|
-
for (const d of dirs) {
|
|
138
|
-
if (d.channelId !== channelId)
|
|
139
|
-
continue;
|
|
140
|
-
const active = readJsonFile(path.join(d.dirPath, 'active.json'));
|
|
141
|
-
if (active && active.channel === channel && active.channelType)
|
|
142
|
-
return active.channelType;
|
|
143
|
-
}
|
|
144
|
-
throw new Error(`[SessionManager] Cannot infer channelType for channel="${channel}" channelId="${channelId}". No existing session found.`);
|
|
145
|
-
}
|
|
146
|
-
/** 从 active 推断 selfId(已有 session 的复用) */
|
|
147
|
-
inferSelfId(channel, channelId, chatDir) {
|
|
148
|
-
if (chatDir) {
|
|
149
|
-
const active = readJsonFile(path.join(chatDir, 'active.json'));
|
|
150
|
-
if (active?.selfId)
|
|
151
|
-
return active.selfId;
|
|
152
|
-
}
|
|
153
|
-
// 扫描已有目录
|
|
154
|
-
const dirs = scanChatDirs(this.sessionsDir);
|
|
155
|
-
for (const d of dirs) {
|
|
156
|
-
if (d.channelId !== channelId)
|
|
157
|
-
continue;
|
|
158
|
-
const active = readJsonFile(path.join(d.dirPath, 'active.json'));
|
|
159
|
-
if (active && active.channel === channel)
|
|
160
|
-
return active.selfId || undefined;
|
|
161
|
-
}
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
122
|
/**
|
|
165
123
|
* 扫描已有 chat 目录,找到匹配 channel+channelId 的目录并返回其 chatDir 路径。
|
|
166
|
-
* 用于不知道 channelType/
|
|
124
|
+
* 用于不知道 channelType/selfAID 的 caller 在调用 resolveChatDir 前定位已有目录。
|
|
167
125
|
*/
|
|
168
126
|
findExistingChatDir(channel, channelId) {
|
|
169
127
|
const dirs = scanChatDirs(this.sessionsDir);
|
|
@@ -194,26 +152,26 @@ export class SessionManager {
|
|
|
194
152
|
return undefined;
|
|
195
153
|
}
|
|
196
154
|
/**
|
|
197
|
-
* 安全版 resolveChatDir:先尝试用提供的 channelType/
|
|
155
|
+
* 安全版 resolveChatDir:先尝试用提供的 channelType/selfAID,
|
|
198
156
|
* 如果没有则扫描已有目录推断。用于操作已有 session 的公共方法。
|
|
199
157
|
*/
|
|
200
|
-
resolveChatDirSafe(channel, channelId, channelType,
|
|
201
|
-
if (channelType) {
|
|
202
|
-
return this.resolveChatDir(channel, channelId, channelType,
|
|
158
|
+
resolveChatDirSafe(channel, channelId, channelType, selfAID) {
|
|
159
|
+
if (channelType && (selfAID || channelType !== 'aun')) {
|
|
160
|
+
return this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
203
161
|
}
|
|
204
162
|
// 尝试从已有目录推断
|
|
205
163
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
206
164
|
if (existingDir)
|
|
207
165
|
return existingDir;
|
|
208
|
-
throw new Error(`[SessionManager] Cannot resolve chat dir for channel="${channel}" channelId="${channelId}". No channelType provided and no existing session found.`);
|
|
166
|
+
throw new Error(`[SessionManager] Cannot resolve chat dir for channel="${channel}" channelId="${channelId}". No channelType/selfAID provided and no existing session found.`);
|
|
209
167
|
}
|
|
210
168
|
/**
|
|
211
|
-
* 安全版 ensureResolvedChatDir:先尝试用提供的 channelType/
|
|
169
|
+
* 安全版 ensureResolvedChatDir:先尝试用提供的 channelType/selfAID,
|
|
212
170
|
* 如果没有则扫描已有目录推断。确保目录存在。
|
|
213
171
|
*/
|
|
214
|
-
ensureResolvedChatDirSafe(channel, channelId, channelType,
|
|
215
|
-
if (channelType) {
|
|
216
|
-
return this.ensureResolvedChatDir(channel, channelId, channelType,
|
|
172
|
+
ensureResolvedChatDirSafe(channel, channelId, channelType, selfAID) {
|
|
173
|
+
if (channelType && (selfAID || channelType !== 'aun')) {
|
|
174
|
+
return this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
217
175
|
}
|
|
218
176
|
// 尝试从已有目录推断
|
|
219
177
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
@@ -224,18 +182,14 @@ export class SessionManager {
|
|
|
224
182
|
fs.mkdirSync(path.join(existingDir, '_trash'), { recursive: true });
|
|
225
183
|
return existingDir;
|
|
226
184
|
}
|
|
227
|
-
|
|
228
|
-
const inferredType = this.inferChannelType(channel, channelId);
|
|
229
|
-
const inferredSelfId = this.inferSelfId(channel, channelId);
|
|
230
|
-
return this.ensureResolvedChatDir(channel, channelId, inferredType, inferredSelfId);
|
|
185
|
+
throw new Error(`[SessionManager] Cannot resolve chat dir for channel="${channel}" channelId="${channelId}". No channelType/selfAID provided and no existing session found.`);
|
|
231
186
|
}
|
|
232
|
-
readActive(channel, channelId, channelType,
|
|
187
|
+
readActive(channel, channelId, channelType, selfAID) {
|
|
233
188
|
let dir;
|
|
234
|
-
|
|
235
|
-
dir = this.resolveChatDir(channel, channelId, channelType,
|
|
189
|
+
if (channelType && selfAID) {
|
|
190
|
+
dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
236
191
|
}
|
|
237
|
-
|
|
238
|
-
// channelType not provided — try to find existing dir
|
|
192
|
+
else {
|
|
239
193
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
240
194
|
if (!existingDir)
|
|
241
195
|
return undefined;
|
|
@@ -251,12 +205,12 @@ export class SessionManager {
|
|
|
251
205
|
const file = sessionToFile(session);
|
|
252
206
|
atomicWriteJson(path.join(dir, 'active.json'), file);
|
|
253
207
|
}
|
|
254
|
-
clearActive(channel, channelId, channelType,
|
|
208
|
+
clearActive(channel, channelId, channelType, selfAID) {
|
|
255
209
|
let dir;
|
|
256
|
-
|
|
257
|
-
dir = this.resolveChatDir(channel, channelId, channelType,
|
|
210
|
+
if (channelType && selfAID) {
|
|
211
|
+
dir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
258
212
|
}
|
|
259
|
-
|
|
213
|
+
else {
|
|
260
214
|
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
261
215
|
if (!existingDir)
|
|
262
216
|
return;
|
|
@@ -273,51 +227,72 @@ export class SessionManager {
|
|
|
273
227
|
}
|
|
274
228
|
ensureChatDirForSession(session) {
|
|
275
229
|
const channelType = session.channelType || session.channel;
|
|
276
|
-
return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.
|
|
230
|
+
return ensureChatDir(this.sessionsDir, channelType, session.channelId, session.selfAID);
|
|
277
231
|
}
|
|
278
232
|
metaFilePath(chatDir, sessionId) {
|
|
279
233
|
return path.join(chatDir, `${sessionId}.jsonl`);
|
|
280
234
|
}
|
|
281
|
-
|
|
235
|
+
/** 由 session 自身的 channelType/channelId/selfId/threadId 直接定位其 .jsonl 路径(不扫描目录)。 */
|
|
236
|
+
metaPathForSession(session) {
|
|
282
237
|
const dir = this.ensureChatDirForSession(session);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const file = sessionToFile(session);
|
|
286
|
-
const metaPath = this.metaFilePath(targetDir, session.id);
|
|
287
|
-
appendJsonl(metaPath, file);
|
|
238
|
+
const targetDir = session.threadId ? path.join(dir, '_threads') : dir;
|
|
239
|
+
return this.metaFilePath(targetDir, session.id);
|
|
288
240
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
*/
|
|
293
|
-
sessionFilesEqual(a, b) {
|
|
294
|
-
const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
|
|
295
|
-
return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
|
|
241
|
+
appendMeta(channel, channelId, session) {
|
|
242
|
+
const file = sessionToFile(session);
|
|
243
|
+
appendJsonl(this.metaPathForSession(session), file);
|
|
296
244
|
}
|
|
297
245
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
246
|
+
* Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
|
|
247
|
+
* appendMeta / writeActive。
|
|
248
|
+
*
|
|
249
|
+
* 契约:.jsonl 是权威源,active.json 是热缓存。
|
|
250
|
+
* 1. 内建 diff 去重:与该 session 的 .jsonl 末行比较(忽略 updatedAt),无变化则跳过
|
|
251
|
+
* 2. appendMeta(.jsonl) —— 始终先落历史档案(权威源)
|
|
252
|
+
* 3. 按 intent 决定 active.json 行为:
|
|
253
|
+
* - 'set' : 无条件让该 session 成为 active(创建/切换主会话)
|
|
254
|
+
* - 'sync' : 仅当该 session 已是当前 active 时同步缓存(更新自身字段)
|
|
255
|
+
* - 'none' : 不碰 active.json(thread 会话 / 后台 autonomous 会话)
|
|
256
|
+
*
|
|
257
|
+
* @param session 要持久化的 session(其 updatedAt 会被刷新)
|
|
258
|
+
* @param intent active.json 行为意图
|
|
259
|
+
* @param opts.forceWrite 强制写入一条新记录,跳过去重。用于 markProcessing/clearProcessing
|
|
260
|
+
* 这类"状态转换事件必须留痕"的场景。
|
|
261
|
+
* @returns 是否真的写入了 .jsonl(去重命中时返回 false)
|
|
301
262
|
*/
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
263
|
+
persistSession(session, intent, opts) {
|
|
264
|
+
if (!opts?.forceWrite) {
|
|
265
|
+
const lastMeta = readLastJsonlLine(this.metaPathForSession(session));
|
|
266
|
+
if (lastMeta && this.sessionFilesEqual(lastMeta, sessionToFile(session))) {
|
|
267
|
+
// .jsonl 末行已与目标一致:仍可能需要把缓存对齐('set' 语义)
|
|
268
|
+
if (intent === 'set') {
|
|
269
|
+
session.updatedAt = Date.now();
|
|
270
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
271
|
+
}
|
|
307
272
|
return false;
|
|
273
|
+
}
|
|
308
274
|
}
|
|
309
|
-
|
|
310
|
-
this.appendMeta(channel, channelId,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
275
|
+
session.updatedAt = Date.now();
|
|
276
|
+
this.appendMeta(session.channel, session.channelId, session);
|
|
277
|
+
if (intent === 'set') {
|
|
278
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
279
|
+
}
|
|
280
|
+
else if (intent === 'sync') {
|
|
281
|
+
const active = this.readActive(session.channel, session.channelId);
|
|
282
|
+
if (active && active.id === session.id) {
|
|
283
|
+
this.writeActive(session.channel, session.channelId, session);
|
|
316
284
|
}
|
|
317
|
-
this.writeActive(channel, channelId, next);
|
|
318
285
|
}
|
|
319
286
|
return true;
|
|
320
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* 比较两个 SessionFile 是否在内容上相等(忽略 updatedAt / updatedAtStr)。
|
|
290
|
+
* 用于跳过"没真变化"的写入,避免 jsonl 写放大。
|
|
291
|
+
*/
|
|
292
|
+
sessionFilesEqual(a, b) {
|
|
293
|
+
const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
|
|
294
|
+
return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
|
|
295
|
+
}
|
|
321
296
|
readMetaLatest(metaFilePath) {
|
|
322
297
|
const file = readLastJsonlLine(metaFilePath);
|
|
323
298
|
if (!file)
|
|
@@ -335,9 +310,7 @@ export class SessionManager {
|
|
|
335
310
|
* 2. 优先读 active.json(如果 active.id === sessionId)—— 当前状态
|
|
336
311
|
* 3. 否则 fallback 到 .jsonl 末行 —— 非活跃 session 的更新(如多 session 并存时改非 active 那个)
|
|
337
312
|
*
|
|
338
|
-
* 返回 { current
|
|
339
|
-
* - current 用于 caller 修改后写回
|
|
340
|
-
* - prev 是 current 的初始快照(用于 writeSessionIfChanged 的 diff 检查)
|
|
313
|
+
* 返回 { current }:caller 修改后交给 persistSession 写回(去重由 persistSession 内建)。
|
|
341
314
|
*/
|
|
342
315
|
loadSessionForUpdate(sessionId) {
|
|
343
316
|
const found = this.findSessionFileById(sessionId);
|
|
@@ -350,10 +323,8 @@ export class SessionManager {
|
|
|
350
323
|
// 优先用 active.json 的当前状态(如果它就是这个 sessionId)
|
|
351
324
|
const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
|
|
352
325
|
const base = (active && active.id === sessionId) ? active : fromJsonl;
|
|
353
|
-
// 深拷贝避免 caller 改 current 时污染 prev
|
|
354
326
|
const current = JSON.parse(JSON.stringify(base));
|
|
355
|
-
|
|
356
|
-
return { current, prev };
|
|
327
|
+
return { current };
|
|
357
328
|
}
|
|
358
329
|
validateSessionFile(session) {
|
|
359
330
|
const agentSessionId = session.agentSessionId;
|
|
@@ -366,9 +337,8 @@ export class SessionManager {
|
|
|
366
337
|
if (adapter.checkExists(session.projectPath, agentSessionId))
|
|
367
338
|
return agentSessionId;
|
|
368
339
|
logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
|
|
369
|
-
const prev = JSON.parse(JSON.stringify(session));
|
|
370
340
|
session.agentSessionId = undefined;
|
|
371
|
-
this.
|
|
341
|
+
this.persistSession(session, 'sync');
|
|
372
342
|
return undefined;
|
|
373
343
|
}
|
|
374
344
|
getActiveChatType(channel, channelId) {
|
|
@@ -433,17 +403,14 @@ export class SessionManager {
|
|
|
433
403
|
markProcessing(sessionId, taskId) {
|
|
434
404
|
const now = Date.now();
|
|
435
405
|
const state = taskId ? `${now}:${taskId}` : String(now);
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
406
|
+
const found = this.findSessionFileById(sessionId);
|
|
407
|
+
if (!found)
|
|
408
|
+
return;
|
|
409
|
+
const session = this.readMetaLatest(found.metaPath);
|
|
410
|
+
if (!session)
|
|
411
|
+
return;
|
|
412
|
+
session.processingState = state;
|
|
413
|
+
this.persistSession(session, 'sync', { forceWrite: true });
|
|
447
414
|
}
|
|
448
415
|
getActiveTaskId(sessionId) {
|
|
449
416
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
@@ -459,17 +426,20 @@ export class SessionManager {
|
|
|
459
426
|
return undefined;
|
|
460
427
|
}
|
|
461
428
|
clearProcessing(sessionId) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
429
|
+
const found = this.findSessionFileById(sessionId);
|
|
430
|
+
if (!found) {
|
|
431
|
+
this.sessionEncryptState.delete(sessionId);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const session = this.readMetaLatest(found.metaPath);
|
|
435
|
+
if (!session) {
|
|
436
|
+
this.sessionEncryptState.delete(sessionId);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// 仅当 .jsonl 末行确实有 activeTask 时才写(避免写放大)
|
|
440
|
+
if (session.processingState) {
|
|
441
|
+
session.processingState = undefined;
|
|
442
|
+
this.persistSession(session, 'sync', { forceWrite: true });
|
|
473
443
|
}
|
|
474
444
|
this.sessionEncryptState.delete(sessionId);
|
|
475
445
|
}
|
|
@@ -484,36 +454,41 @@ export class SessionManager {
|
|
|
484
454
|
const result = [];
|
|
485
455
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
486
456
|
for (const { dirPath } of chatDirs) {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
atomicWriteJson(path.join(dirPath, 'active.json'), active);
|
|
457
|
+
for (const metaFile of scanMetaFiles(dirPath)) {
|
|
458
|
+
const session = this.readMetaLatest(path.join(dirPath, metaFile));
|
|
459
|
+
if (!session?.processingState)
|
|
460
|
+
continue;
|
|
461
|
+
const colonIdx = session.processingState.indexOf(':');
|
|
462
|
+
const ts = parseInt(colonIdx > 0 ? session.processingState.slice(0, colonIdx) : session.processingState, 10);
|
|
463
|
+
if (!isNaN(ts) && (now - ts) < maxAgeMs) {
|
|
464
|
+
result.push(session);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
this.clearProcessing(session.id);
|
|
468
|
+
}
|
|
500
469
|
}
|
|
501
470
|
}
|
|
502
471
|
return result;
|
|
503
472
|
}
|
|
504
473
|
// ─── Session lifecycle ───
|
|
505
|
-
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId,
|
|
474
|
+
async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfAID, channelType, peerType) {
|
|
475
|
+
if (!selfAID && channelType === 'aun') {
|
|
476
|
+
throw new Error(`[SessionManager] getOrCreateSession requires selfAID for aun channel. channelId="${channelId}"`);
|
|
477
|
+
}
|
|
478
|
+
if (!channelType) {
|
|
479
|
+
throw new Error(`[SessionManager] getOrCreateSession requires channelType. channel="${channel}" channelId="${channelId}"`);
|
|
480
|
+
}
|
|
506
481
|
if (threadId) {
|
|
507
|
-
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId,
|
|
482
|
+
const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType);
|
|
508
483
|
session.identity = this.resolveIdentity(channel, userId);
|
|
509
484
|
if (session.metadata && !session.metadata.permissionMode) {
|
|
510
485
|
session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
|
|
511
|
-
this.
|
|
486
|
+
this.persistSession(session, 'none');
|
|
512
487
|
}
|
|
513
488
|
return session;
|
|
514
489
|
}
|
|
515
490
|
// 使用精确路径解析(caller 提供了 channelType 时直接定位,避免扫描回落)
|
|
516
|
-
const exactDir = this.resolveChatDirExact(channel, channelId, channelType,
|
|
491
|
+
const exactDir = this.resolveChatDirExact(channel, channelId, channelType, selfAID);
|
|
517
492
|
const activeFile = readJsonFile(path.join(exactDir, 'active.json'));
|
|
518
493
|
const active = activeFile ? fileToSession(activeFile) : undefined;
|
|
519
494
|
if (active && !active.threadId) {
|
|
@@ -526,8 +501,8 @@ export class SessionManager {
|
|
|
526
501
|
session.chatType = chatType;
|
|
527
502
|
mutated = true;
|
|
528
503
|
}
|
|
529
|
-
if (
|
|
530
|
-
session.
|
|
504
|
+
if (session.selfAID !== selfAID) {
|
|
505
|
+
session.selfAID = selfAID;
|
|
531
506
|
mutated = true;
|
|
532
507
|
}
|
|
533
508
|
if (chatType === 'private' && userId) {
|
|
@@ -541,41 +516,38 @@ export class SessionManager {
|
|
|
541
516
|
session.metadata.peerName = metadata.peerName;
|
|
542
517
|
mutated = true;
|
|
543
518
|
}
|
|
544
|
-
if (metadata?.
|
|
545
|
-
session.metadata.
|
|
519
|
+
if (metadata?.channelKey && session.metadata.channelKey !== metadata.channelKey) {
|
|
520
|
+
session.metadata.channelKey = metadata.channelKey;
|
|
546
521
|
mutated = true;
|
|
547
522
|
}
|
|
548
523
|
}
|
|
549
|
-
if (metadata?.
|
|
524
|
+
if (metadata?.channelKey && chatType !== 'private') {
|
|
550
525
|
if (!session.metadata)
|
|
551
526
|
session.metadata = {};
|
|
552
|
-
if (session.metadata.
|
|
553
|
-
session.metadata.
|
|
527
|
+
if (session.metadata.channelKey !== metadata.channelKey) {
|
|
528
|
+
session.metadata.channelKey = metadata.channelKey;
|
|
554
529
|
mutated = true;
|
|
555
530
|
}
|
|
556
531
|
}
|
|
557
532
|
if (mutated) {
|
|
558
|
-
session
|
|
559
|
-
this.appendMeta(channel, channelId, session);
|
|
560
|
-
this.writeActive(channel, channelId, session);
|
|
533
|
+
this.persistSession(session, 'sync');
|
|
561
534
|
}
|
|
562
535
|
return session;
|
|
563
536
|
}
|
|
564
537
|
// Find existing session for default project path
|
|
565
|
-
const chatDir = this.resolveChatDir(channel, channelId, channelType,
|
|
538
|
+
const chatDir = this.resolveChatDir(channel, channelId, channelType, selfAID);
|
|
566
539
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
567
540
|
const existing = allSessions
|
|
568
541
|
.filter(s => s.projectPath === defaultProjectPath && !s.threadId)
|
|
569
542
|
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
570
543
|
if (existing) {
|
|
571
544
|
const validSessionId = this.validateSessionFile(existing);
|
|
572
|
-
const prev = JSON.parse(JSON.stringify({ ...existing, agentSessionId: validSessionId }));
|
|
573
545
|
const session = { ...existing, agentSessionId: validSessionId };
|
|
574
546
|
session.identity = this.resolveIdentity(channel, userId);
|
|
575
547
|
if (!session.metadata)
|
|
576
548
|
session.metadata = {};
|
|
577
|
-
if (
|
|
578
|
-
session.
|
|
549
|
+
if (session.selfAID !== selfAID) {
|
|
550
|
+
session.selfAID = selfAID;
|
|
579
551
|
}
|
|
580
552
|
if (chatType && session.chatType !== chatType) {
|
|
581
553
|
logger.info(`[SessionManager] Updating chatType for session ${session.id}: ${session.chatType} -> ${chatType}`);
|
|
@@ -587,7 +559,7 @@ export class SessionManager {
|
|
|
587
559
|
if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
|
|
588
560
|
session.metadata.peerName = metadata.peerName;
|
|
589
561
|
}
|
|
590
|
-
this.
|
|
562
|
+
this.persistSession(session, 'sync');
|
|
591
563
|
return session;
|
|
592
564
|
}
|
|
593
565
|
// Create new session
|
|
@@ -597,12 +569,13 @@ export class SessionManager {
|
|
|
597
569
|
const session = {
|
|
598
570
|
id: generateSessionId(),
|
|
599
571
|
channel,
|
|
600
|
-
channelType
|
|
572
|
+
channelType,
|
|
601
573
|
channelId,
|
|
602
|
-
|
|
574
|
+
selfAID,
|
|
603
575
|
projectPath: defaultProjectPath,
|
|
604
576
|
threadId: '',
|
|
605
577
|
agentId: agentId || 'claude',
|
|
578
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
606
579
|
chatType: chatType || 'private',
|
|
607
580
|
sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private', peerType),
|
|
608
581
|
metadata: sessionMetadata,
|
|
@@ -611,8 +584,7 @@ export class SessionManager {
|
|
|
611
584
|
updatedAt: Date.now(),
|
|
612
585
|
};
|
|
613
586
|
session.identity = this.resolveIdentity(channel, userId);
|
|
614
|
-
this.
|
|
615
|
-
this.writeActive(channel, channelId, session);
|
|
587
|
+
this.persistSession(session, 'set');
|
|
616
588
|
this.eventBus.publish({
|
|
617
589
|
type: 'session:created',
|
|
618
590
|
sessionId: session.id,
|
|
@@ -629,7 +601,7 @@ export class SessionManager {
|
|
|
629
601
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
630
602
|
if (!loaded)
|
|
631
603
|
return;
|
|
632
|
-
const { current
|
|
604
|
+
const { current } = loaded;
|
|
633
605
|
if (updates.chatType !== undefined)
|
|
634
606
|
current.chatType = updates.chatType;
|
|
635
607
|
if (updates.name !== undefined)
|
|
@@ -640,41 +612,43 @@ export class SessionManager {
|
|
|
640
612
|
current.metadata = updates.metadata;
|
|
641
613
|
if ('agentSessionId' in updates)
|
|
642
614
|
current.agentSessionId = updates.agentSessionId ?? undefined;
|
|
643
|
-
this.
|
|
615
|
+
this.persistSession(current, 'sync');
|
|
644
616
|
}
|
|
645
|
-
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId,
|
|
646
|
-
//
|
|
647
|
-
const chatDir = (channelType &&
|
|
648
|
-
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId,
|
|
649
|
-
: this.ensureResolvedChatDirSafe(channel, channelId, channelType);
|
|
617
|
+
getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfAID, channelType, peerType) {
|
|
618
|
+
// 使用精确路径(channelType + selfAID)
|
|
619
|
+
const chatDir = (channelType && selfAID)
|
|
620
|
+
? (() => { const d = chatDirPath(this.sessionsDir, channelType, channelId, selfAID); fs.mkdirSync(d, { recursive: true }); fs.mkdirSync(path.join(d, '_threads'), { recursive: true }); return d; })()
|
|
621
|
+
: this.ensureResolvedChatDirSafe(channel, channelId, channelType, selfAID);
|
|
650
622
|
const threadIndex = readThreadIndex(chatDir);
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
653
|
-
const metaPath = path.join(chatDir, '_threads', `${
|
|
623
|
+
const existingEntry = threadIndex[threadId];
|
|
624
|
+
if (existingEntry) {
|
|
625
|
+
const metaPath = path.join(chatDir, '_threads', `${existingEntry.sessionId}.jsonl`);
|
|
654
626
|
const existing = this.readMetaLatest(metaPath);
|
|
655
627
|
if (existing) {
|
|
656
628
|
const validSessionId = this.validateSessionFile(existing);
|
|
657
629
|
if (metadata) {
|
|
658
630
|
existing.metadata = { ...(existing.metadata || {}), ...metadata };
|
|
659
|
-
existing
|
|
660
|
-
this.appendMeta(channel, channelId, existing);
|
|
631
|
+
this.persistSession(existing, 'none');
|
|
661
632
|
}
|
|
662
633
|
return { ...existing, agentSessionId: validSessionId };
|
|
663
634
|
}
|
|
664
635
|
}
|
|
665
636
|
// Inherit project path & chatType from active main session
|
|
666
|
-
const activeMain = this.readActive(channel, channelId, channelType,
|
|
637
|
+
const activeMain = this.readActive(channel, channelId, channelType, selfAID);
|
|
667
638
|
const projectPath = (activeMain && !activeMain.threadId ? activeMain.projectPath : undefined) || defaultProjectPath;
|
|
668
639
|
const inheritedChatType = (activeMain && !activeMain.threadId ? activeMain.chatType : undefined) || 'private';
|
|
640
|
+
const effectiveChannelType = channelType || channel;
|
|
641
|
+
const sessionKey = formatSessionKey(effectiveChannelType, channelId, threadId);
|
|
669
642
|
const session = {
|
|
670
643
|
id: generateSessionId(),
|
|
671
644
|
channel,
|
|
672
|
-
channelType:
|
|
645
|
+
channelType: effectiveChannelType,
|
|
673
646
|
channelId,
|
|
674
|
-
|
|
647
|
+
selfAID: selfAID || '',
|
|
675
648
|
projectPath,
|
|
676
649
|
threadId,
|
|
677
650
|
agentId: agentId || 'claude',
|
|
651
|
+
sessionKey,
|
|
678
652
|
chatType: inheritedChatType,
|
|
679
653
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType, peerType),
|
|
680
654
|
metadata,
|
|
@@ -682,8 +656,12 @@ export class SessionManager {
|
|
|
682
656
|
createdAt: Date.now(),
|
|
683
657
|
updatedAt: Date.now(),
|
|
684
658
|
};
|
|
685
|
-
this.
|
|
686
|
-
threadIndex[threadId] =
|
|
659
|
+
this.persistSession(session, 'none');
|
|
660
|
+
threadIndex[threadId] = {
|
|
661
|
+
sessionId: session.id,
|
|
662
|
+
sessionKey,
|
|
663
|
+
metaFile: `${session.id}.jsonl`,
|
|
664
|
+
};
|
|
687
665
|
writeThreadIndex(chatDir, threadIndex);
|
|
688
666
|
this.eventBus.publish({
|
|
689
667
|
type: 'session:created',
|
|
@@ -708,20 +686,23 @@ export class SessionManager {
|
|
|
708
686
|
if (target) {
|
|
709
687
|
const validSessionId = this.validateSessionFile(target);
|
|
710
688
|
target.agentSessionId = validSessionId;
|
|
711
|
-
target
|
|
712
|
-
this.appendMeta(channel, channelId, target);
|
|
713
|
-
this.writeActive(channel, channelId, target);
|
|
689
|
+
this.persistSession(target, 'set');
|
|
714
690
|
return target;
|
|
715
691
|
}
|
|
692
|
+
// Derive selfAID and channelType from existing sessions in this chatDir
|
|
693
|
+
const existingAny = allSessions[0];
|
|
694
|
+
const selfAID = existingAny?.selfAID || '';
|
|
695
|
+
const channelType = existingAny?.channelType || channel;
|
|
716
696
|
const session = {
|
|
717
697
|
id: generateSessionId(),
|
|
718
698
|
channel,
|
|
719
|
-
channelType
|
|
699
|
+
channelType,
|
|
720
700
|
channelId,
|
|
721
|
-
|
|
701
|
+
selfAID,
|
|
722
702
|
projectPath: newProjectPath,
|
|
723
703
|
threadId: '',
|
|
724
704
|
agentId,
|
|
705
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
725
706
|
chatType: inheritedChatType,
|
|
726
707
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
727
708
|
metadata: {},
|
|
@@ -729,8 +710,7 @@ export class SessionManager {
|
|
|
729
710
|
createdAt: Date.now(),
|
|
730
711
|
updatedAt: Date.now(),
|
|
731
712
|
};
|
|
732
|
-
this.
|
|
733
|
-
this.writeActive(channel, channelId, session);
|
|
713
|
+
this.persistSession(session, 'set');
|
|
734
714
|
this.eventBus.publish({
|
|
735
715
|
type: 'session:created',
|
|
736
716
|
sessionId: session.id,
|
|
@@ -746,24 +726,29 @@ export class SessionManager {
|
|
|
746
726
|
const active = this.readActive(channel, channelId);
|
|
747
727
|
if (!active)
|
|
748
728
|
return;
|
|
749
|
-
const prev = JSON.parse(JSON.stringify(active));
|
|
750
729
|
active.agentSessionId = agentSessionId;
|
|
751
|
-
this.
|
|
730
|
+
this.persistSession(active, 'sync');
|
|
752
731
|
}
|
|
753
732
|
async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
|
|
754
733
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
755
734
|
if (!loaded)
|
|
756
735
|
return;
|
|
757
|
-
const { current
|
|
736
|
+
const { current } = loaded;
|
|
758
737
|
current.agentSessionId = agentSessionId;
|
|
759
|
-
const wrote = this.
|
|
738
|
+
const wrote = this.persistSession(current, 'sync');
|
|
760
739
|
if (wrote) {
|
|
761
740
|
logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
762
741
|
}
|
|
763
742
|
}
|
|
764
743
|
async switchAgent(channel, channelId, projectPath, newAgentId) {
|
|
765
744
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
766
|
-
|
|
745
|
+
// Derive channelType/selfAID from existing sessions; fall back to channel name
|
|
746
|
+
const probeChatDir = this.resolveChatDir(channel, channelId, channel, '');
|
|
747
|
+
const probeSessions = fs.existsSync(probeChatDir) ? this.findAllSessionsInChat(probeChatDir, false) : [];
|
|
748
|
+
const existingAny = probeSessions[0];
|
|
749
|
+
const channelType = existingAny?.channelType || channel;
|
|
750
|
+
const selfAID = existingAny?.selfAID || '';
|
|
751
|
+
const chatDir = this.ensureResolvedChatDir(channel, channelId, channelType, selfAID);
|
|
767
752
|
const allSessions = this.findAllSessionsInChat(chatDir, false);
|
|
768
753
|
const target = allSessions
|
|
769
754
|
.filter(s => s.projectPath === projectPath && (s.agentId || 'claude') === newAgentId && !s.threadId)
|
|
@@ -771,20 +756,19 @@ export class SessionManager {
|
|
|
771
756
|
if (target) {
|
|
772
757
|
const validSessionId = this.validateSessionFile(target);
|
|
773
758
|
target.agentSessionId = validSessionId;
|
|
774
|
-
target
|
|
775
|
-
this.appendMeta(channel, channelId, target);
|
|
776
|
-
this.writeActive(channel, channelId, target);
|
|
759
|
+
this.persistSession(target, 'set');
|
|
777
760
|
return target;
|
|
778
761
|
}
|
|
779
762
|
const session = {
|
|
780
763
|
id: generateSessionId(),
|
|
781
764
|
channel,
|
|
782
|
-
channelType
|
|
765
|
+
channelType,
|
|
783
766
|
channelId,
|
|
784
|
-
|
|
767
|
+
selfAID,
|
|
785
768
|
projectPath,
|
|
786
769
|
threadId: '',
|
|
787
770
|
agentId: newAgentId,
|
|
771
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
788
772
|
chatType: inheritedChatType,
|
|
789
773
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
790
774
|
metadata: {},
|
|
@@ -792,8 +776,7 @@ export class SessionManager {
|
|
|
792
776
|
createdAt: Date.now(),
|
|
793
777
|
updatedAt: Date.now(),
|
|
794
778
|
};
|
|
795
|
-
this.
|
|
796
|
-
this.writeActive(channel, channelId, session);
|
|
779
|
+
this.persistSession(session, 'set');
|
|
797
780
|
this.eventBus.publish({
|
|
798
781
|
type: 'session:created',
|
|
799
782
|
sessionId: session.id,
|
|
@@ -809,9 +792,8 @@ export class SessionManager {
|
|
|
809
792
|
const active = this.readActive(channel, channelId);
|
|
810
793
|
if (!active)
|
|
811
794
|
return;
|
|
812
|
-
const prev = JSON.parse(JSON.stringify(active));
|
|
813
795
|
active.agentSessionId = undefined;
|
|
814
|
-
this.
|
|
796
|
+
this.persistSession(active, 'sync');
|
|
815
797
|
}
|
|
816
798
|
getOwnerChatId(targetChannel, ownerPeerId) {
|
|
817
799
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
@@ -851,8 +833,8 @@ export class SessionManager {
|
|
|
851
833
|
return this.readActive(channel, channelId);
|
|
852
834
|
}
|
|
853
835
|
/** 同步版 getActiveSession,支持精确路径定位(避免扫描) */
|
|
854
|
-
getActiveSessionSync(channel, channelId, channelType,
|
|
855
|
-
return this.readActive(channel, channelId, channelType,
|
|
836
|
+
getActiveSessionSync(channel, channelId, channelType, selfAID) {
|
|
837
|
+
return this.readActive(channel, channelId, channelType, selfAID);
|
|
856
838
|
}
|
|
857
839
|
async getThreadSession(channel, channelId, threadId) {
|
|
858
840
|
let chatDir;
|
|
@@ -863,10 +845,10 @@ export class SessionManager {
|
|
|
863
845
|
return undefined;
|
|
864
846
|
}
|
|
865
847
|
const threadIndex = readThreadIndex(chatDir);
|
|
866
|
-
const
|
|
867
|
-
if (!
|
|
848
|
+
const entry = threadIndex[threadId];
|
|
849
|
+
if (!entry)
|
|
868
850
|
return undefined;
|
|
869
|
-
const metaPath = path.join(chatDir, '_threads', `${
|
|
851
|
+
const metaPath = path.join(chatDir, '_threads', `${entry.sessionId}.jsonl`);
|
|
870
852
|
const session = this.readMetaLatest(metaPath);
|
|
871
853
|
if (!session)
|
|
872
854
|
return undefined;
|
|
@@ -928,26 +910,24 @@ export class SessionManager {
|
|
|
928
910
|
const target = sessions.find(s => s.id === targetSessionId);
|
|
929
911
|
if (!target)
|
|
930
912
|
return null;
|
|
931
|
-
target
|
|
932
|
-
this.appendMeta(channel, channelId, target);
|
|
933
|
-
this.writeActive(channel, channelId, target);
|
|
913
|
+
this.persistSession(target, 'set');
|
|
934
914
|
return target;
|
|
935
915
|
}
|
|
936
916
|
updateMetadata(sessionId, metadata) {
|
|
937
917
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
938
918
|
if (!loaded)
|
|
939
919
|
return;
|
|
940
|
-
const { current
|
|
920
|
+
const { current } = loaded;
|
|
941
921
|
current.metadata = metadata;
|
|
942
|
-
this.
|
|
922
|
+
this.persistSession(current, 'sync');
|
|
943
923
|
}
|
|
944
924
|
async renameSession(sessionId, newName) {
|
|
945
925
|
const loaded = this.loadSessionForUpdate(sessionId);
|
|
946
926
|
if (!loaded)
|
|
947
927
|
return false;
|
|
948
|
-
const { current
|
|
928
|
+
const { current } = loaded;
|
|
949
929
|
current.name = newName;
|
|
950
|
-
this.
|
|
930
|
+
this.persistSession(current, 'sync');
|
|
951
931
|
return true;
|
|
952
932
|
}
|
|
953
933
|
async unbindSession(sessionId) {
|
|
@@ -967,8 +947,8 @@ export class SessionManager {
|
|
|
967
947
|
// If thread session, remove from thread-index
|
|
968
948
|
if (found.isThread) {
|
|
969
949
|
const threadIndex = readThreadIndex(found.chatDir);
|
|
970
|
-
for (const [tid,
|
|
971
|
-
if (
|
|
950
|
+
for (const [tid, entry] of Object.entries(threadIndex)) {
|
|
951
|
+
if (entry.sessionId === sessionId) {
|
|
972
952
|
delete threadIndex[tid];
|
|
973
953
|
break;
|
|
974
954
|
}
|
|
@@ -1040,34 +1020,35 @@ export class SessionManager {
|
|
|
1040
1020
|
}
|
|
1041
1021
|
async createNewSession(channel, channelId, projectPath, name, agentId) {
|
|
1042
1022
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1023
|
+
// Derive selfAID and channelType from existing sessions
|
|
1024
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1025
|
+
let channelType = channel;
|
|
1026
|
+
let selfAID = '';
|
|
1027
|
+
if (existingDir) {
|
|
1028
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
1029
|
+
if (active) {
|
|
1030
|
+
channelType = active.channelType || channel;
|
|
1031
|
+
selfAID = active.selfAID || '';
|
|
1032
|
+
}
|
|
1052
1033
|
}
|
|
1053
1034
|
const session = {
|
|
1054
1035
|
id: generateSessionId(),
|
|
1055
1036
|
channel,
|
|
1056
|
-
channelType
|
|
1037
|
+
channelType,
|
|
1057
1038
|
channelId,
|
|
1058
|
-
|
|
1039
|
+
selfAID,
|
|
1059
1040
|
projectPath,
|
|
1060
1041
|
threadId: '',
|
|
1061
1042
|
agentId: agentId || 'claude',
|
|
1043
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1062
1044
|
chatType: inheritedChatType,
|
|
1063
1045
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
1064
|
-
metadata: {},
|
|
1046
|
+
metadata: { permissionMode: DEFAULT_PERMISSION_MODE },
|
|
1065
1047
|
name: name || '默认会话',
|
|
1066
1048
|
createdAt: Date.now(),
|
|
1067
1049
|
updatedAt: Date.now(),
|
|
1068
1050
|
};
|
|
1069
|
-
this.
|
|
1070
|
-
this.writeActive(channel, channelId, session);
|
|
1051
|
+
this.persistSession(session, 'set');
|
|
1071
1052
|
this.eventBus.publish({
|
|
1072
1053
|
type: 'session:created',
|
|
1073
1054
|
sessionId: session.id,
|
|
@@ -1080,25 +1061,27 @@ export class SessionManager {
|
|
|
1080
1061
|
return session;
|
|
1081
1062
|
}
|
|
1082
1063
|
async createForkedSession(sourceSession, forkedAgentSessionId, name) {
|
|
1064
|
+
const channelType = sourceSession.channelType || sourceSession.channel;
|
|
1065
|
+
const threadId = sourceSession.threadId || '';
|
|
1083
1066
|
const session = {
|
|
1084
1067
|
id: generateSessionId(),
|
|
1085
1068
|
channel: sourceSession.channel,
|
|
1086
|
-
channelType
|
|
1069
|
+
channelType,
|
|
1087
1070
|
channelId: sourceSession.channelId,
|
|
1088
|
-
|
|
1071
|
+
selfAID: sourceSession.selfAID,
|
|
1089
1072
|
projectPath: sourceSession.projectPath,
|
|
1090
|
-
threadId
|
|
1073
|
+
threadId,
|
|
1091
1074
|
agentId: sourceSession.agentId || 'claude',
|
|
1075
|
+
sessionKey: formatSessionKey(channelType, sourceSession.channelId, threadId || DEFAULT_THREAD_ID),
|
|
1092
1076
|
chatType: sourceSession.chatType || 'private',
|
|
1093
1077
|
sessionMode: sourceSession.sessionMode || 'interactive',
|
|
1094
1078
|
agentSessionId: forkedAgentSessionId,
|
|
1095
|
-
metadata: {},
|
|
1079
|
+
metadata: { permissionMode: sourceSession.metadata?.permissionMode || DEFAULT_PERMISSION_MODE },
|
|
1096
1080
|
name: name || `${sourceSession.name || '会话'}-分支`,
|
|
1097
1081
|
createdAt: Date.now(),
|
|
1098
1082
|
updatedAt: Date.now(),
|
|
1099
1083
|
};
|
|
1100
|
-
this.
|
|
1101
|
-
this.writeActive(sourceSession.channel, sourceSession.channelId, session);
|
|
1084
|
+
this.persistSession(session, 'set');
|
|
1102
1085
|
this.eventBus.publish({
|
|
1103
1086
|
type: 'session:created',
|
|
1104
1087
|
sessionId: session.id,
|
|
@@ -1161,15 +1144,27 @@ export class SessionManager {
|
|
|
1161
1144
|
const inheritedChatType = this.getActiveChatType(channel, channelId);
|
|
1162
1145
|
const fileInfo = this.getSessionFileInfo(projectPath, agentSessionId, agentId);
|
|
1163
1146
|
const name = fileInfo.title || `CLI会话-${new Date().toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}`;
|
|
1147
|
+
// Derive selfAID and channelType from existing sessions
|
|
1148
|
+
const existingDir = this.findExistingChatDir(channel, channelId);
|
|
1149
|
+
let channelType = channel;
|
|
1150
|
+
let selfAID = '';
|
|
1151
|
+
if (existingDir) {
|
|
1152
|
+
const active = readJsonFile(path.join(existingDir, 'active.json'));
|
|
1153
|
+
if (active) {
|
|
1154
|
+
channelType = active.channelType || channel;
|
|
1155
|
+
selfAID = active.selfAID || '';
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1164
1158
|
const session = {
|
|
1165
1159
|
id: generateSessionId(),
|
|
1166
1160
|
channel,
|
|
1167
|
-
channelType
|
|
1161
|
+
channelType,
|
|
1168
1162
|
channelId,
|
|
1169
|
-
|
|
1163
|
+
selfAID,
|
|
1170
1164
|
projectPath,
|
|
1171
1165
|
threadId: '',
|
|
1172
1166
|
agentId,
|
|
1167
|
+
sessionKey: formatSessionKey(channelType, channelId, DEFAULT_THREAD_ID),
|
|
1173
1168
|
chatType: inheritedChatType,
|
|
1174
1169
|
sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
|
|
1175
1170
|
agentSessionId,
|
|
@@ -1178,8 +1173,7 @@ export class SessionManager {
|
|
|
1178
1173
|
createdAt: Date.now(),
|
|
1179
1174
|
updatedAt: Date.now(),
|
|
1180
1175
|
};
|
|
1181
|
-
this.
|
|
1182
|
-
this.writeActive(channel, channelId, session);
|
|
1176
|
+
this.persistSession(session, 'set');
|
|
1183
1177
|
this.eventBus.publish({
|
|
1184
1178
|
type: 'session:created',
|
|
1185
1179
|
sessionId: session.id,
|
|
@@ -1300,4 +1294,67 @@ export class SessionManager {
|
|
|
1300
1294
|
for (const adapter of this.fileAdapters.values())
|
|
1301
1295
|
adapter.close?.();
|
|
1302
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Migrate session channel key from old format "{selfPeerId}#{type}#{name}"
|
|
1299
|
+
* to new format "{type}#{selfPeerId}#{name}".
|
|
1300
|
+
* Runs once at startup, rewrites active.json and meta_*.jsonl in-place.
|
|
1301
|
+
*/
|
|
1302
|
+
migrateChannelKeyFormat() {
|
|
1303
|
+
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
1304
|
+
let migrated = 0;
|
|
1305
|
+
for (const { dirPath } of chatDirs) {
|
|
1306
|
+
const activePath = path.join(dirPath, 'active.json');
|
|
1307
|
+
const active = readJsonFile(activePath);
|
|
1308
|
+
if (!active?.channel)
|
|
1309
|
+
continue;
|
|
1310
|
+
const newKey = this.convertOldChannelKey(active.channel);
|
|
1311
|
+
if (!newKey)
|
|
1312
|
+
continue;
|
|
1313
|
+
// Rewrite active.json
|
|
1314
|
+
active.channel = newKey;
|
|
1315
|
+
atomicWriteJson(activePath, active);
|
|
1316
|
+
// Rewrite meta_*.jsonl files
|
|
1317
|
+
const metaFiles = scanMetaFiles(dirPath);
|
|
1318
|
+
for (const metaFile of metaFiles) {
|
|
1319
|
+
const metaPath = path.join(dirPath, metaFile);
|
|
1320
|
+
const lines = readAllJsonlLines(metaPath);
|
|
1321
|
+
if (!lines.length)
|
|
1322
|
+
continue;
|
|
1323
|
+
let changed = false;
|
|
1324
|
+
for (const line of lines) {
|
|
1325
|
+
if (line.channel && this.convertOldChannelKey(line.channel)) {
|
|
1326
|
+
line.channel = this.convertOldChannelKey(line.channel);
|
|
1327
|
+
changed = true;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (changed) {
|
|
1331
|
+
const content = lines.map(l => JSON.stringify(l)).join('\n') + '\n';
|
|
1332
|
+
fs.writeFileSync(metaPath, content, 'utf-8');
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
migrated++;
|
|
1336
|
+
}
|
|
1337
|
+
if (migrated > 0) {
|
|
1338
|
+
logger.info(`[SessionManager] Migrated ${migrated} session(s) to new channelKey format`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Detect old format "selfPeerId#type#name" and convert to "type#selfPeerId#name".
|
|
1343
|
+
* Returns null if already in new format or not a valid key.
|
|
1344
|
+
*/
|
|
1345
|
+
convertOldChannelKey(key) {
|
|
1346
|
+
const parts = key.split('#');
|
|
1347
|
+
if (parts.length !== 3)
|
|
1348
|
+
return null;
|
|
1349
|
+
const [first, second, third] = parts;
|
|
1350
|
+
// New format: type#selfPeerId#name — type is short (aun, feishu, wechat, etc.)
|
|
1351
|
+
// Old format: selfPeerId#type#name — selfPeerId contains '.' (e.g., evolai.agentid.pub)
|
|
1352
|
+
const knownTypes = ['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom'];
|
|
1353
|
+
if (knownTypes.includes(first))
|
|
1354
|
+
return null; // already new format
|
|
1355
|
+
if (knownTypes.includes(second) && first.includes('.')) {
|
|
1356
|
+
return `${second}#${first}#${third}`;
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1303
1360
|
}
|