xiaozuoassistant 0.1.69 → 0.1.71
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/dist/server/core/memories/short-term.js +143 -114
- package/dist/server/index.js +2 -0
- package/package.json +1 -1
|
@@ -14,8 +14,13 @@ function normalizeWorkspace(input) {
|
|
|
14
14
|
}
|
|
15
15
|
export class ShortTermMemory {
|
|
16
16
|
constructor() {
|
|
17
|
-
this.
|
|
17
|
+
this.instanceId = Math.random().toString(36).substring(7);
|
|
18
|
+
// 主存:所有 Session Meta 数据
|
|
19
|
+
this.sessions = new Map();
|
|
18
20
|
this.initPromise = null;
|
|
21
|
+
// 持久化队列,确保串行写入 index.json
|
|
22
|
+
this.saveQueue = Promise.resolve();
|
|
23
|
+
console.log(`[ShortTermMemory] Instance created: ${this.instanceId}`);
|
|
19
24
|
fs.ensureDirSync(SESSIONS_DIR);
|
|
20
25
|
}
|
|
21
26
|
static getInstance() {
|
|
@@ -31,18 +36,61 @@ export class ShortTermMemory {
|
|
|
31
36
|
await this.initPromise;
|
|
32
37
|
}
|
|
33
38
|
async init() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
try {
|
|
40
|
+
await fs.ensureDir(SESSIONS_DIR);
|
|
41
|
+
const indexExists = await fs.pathExists(INDEX_FILE);
|
|
42
|
+
if (indexExists) {
|
|
43
|
+
await this.loadSessionsFromDisk();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
await this.migrateLegacyIfNeeded();
|
|
47
|
+
// 迁移后再次尝试加载,如果还没有则初始化为空
|
|
48
|
+
if (await fs.pathExists(INDEX_FILE)) {
|
|
49
|
+
await this.loadSessionsFromDisk();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.sessions.clear();
|
|
53
|
+
await this.persistIndex();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
console.log(`[ShortTermMemory] Initialized with ${this.sessions.size} sessions.`);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
console.error('[ShortTermMemory] Initialization failed:', e);
|
|
60
|
+
// 即使失败,sessions 也是空的 Map,不会崩溃,但数据可能丢失
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async loadSessionsFromDisk() {
|
|
64
|
+
try {
|
|
65
|
+
const data = await fs.readJson(INDEX_FILE);
|
|
66
|
+
this.sessions.clear();
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
for (const meta of data) {
|
|
69
|
+
if (meta && typeof meta.id === 'string') {
|
|
70
|
+
this.sessions.set(meta.id, meta);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
39
74
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await fs.writeJson(INDEX_FILE, [], { spaces: 2 });
|
|
75
|
+
catch (e) {
|
|
76
|
+
console.error('[ShortTermMemory] Failed to load index.json:', e);
|
|
77
|
+
// 此时保持 sessions 为空或之前的状态
|
|
44
78
|
}
|
|
45
|
-
|
|
79
|
+
}
|
|
80
|
+
async persistIndex() {
|
|
81
|
+
// 将当前所有 sessions 转为数组并排序
|
|
82
|
+
const metas = Array.from(this.sessions.values());
|
|
83
|
+
metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
84
|
+
// 加入写入队列
|
|
85
|
+
this.saveQueue = this.saveQueue.then(async () => {
|
|
86
|
+
try {
|
|
87
|
+
await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error('[ShortTermMemory] Failed to persist index.json:', e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return this.saveQueue;
|
|
46
94
|
}
|
|
47
95
|
getSessionDir(sessionId) {
|
|
48
96
|
return path.join(SESSIONS_DIR, sessionId);
|
|
@@ -56,34 +104,12 @@ export class ShortTermMemory {
|
|
|
56
104
|
getRunsFile(sessionId) {
|
|
57
105
|
return path.join(this.getSessionDir(sessionId), 'runs.json');
|
|
58
106
|
}
|
|
59
|
-
async loadIndex() {
|
|
60
|
-
try {
|
|
61
|
-
const data = await fs.readJson(INDEX_FILE);
|
|
62
|
-
if (Array.isArray(data))
|
|
63
|
-
return data;
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
async saveIndex(metas) {
|
|
71
|
-
await fs.writeJson(INDEX_FILE, metas, { spaces: 2 });
|
|
72
|
-
}
|
|
73
|
-
async warmMetaCache() {
|
|
74
|
-
const metas = await this.loadIndex();
|
|
75
|
-
this.metaCache.clear();
|
|
76
|
-
for (const meta of metas) {
|
|
77
|
-
if (meta && typeof meta.id === 'string') {
|
|
78
|
-
this.metaCache.set(meta.id, meta);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
107
|
async migrateLegacyIfNeeded() {
|
|
83
108
|
const files = await fs.readdir(SESSIONS_DIR);
|
|
84
109
|
const legacyFiles = files.filter(f => f.endsWith('.json') && f !== 'index.json');
|
|
85
110
|
if (legacyFiles.length === 0)
|
|
86
111
|
return;
|
|
112
|
+
console.log('[ShortTermMemory] Migrating legacy sessions...');
|
|
87
113
|
const metas = [];
|
|
88
114
|
for (const file of legacyFiles) {
|
|
89
115
|
const filePath = path.join(SESSIONS_DIR, file);
|
|
@@ -113,12 +139,16 @@ export class ShortTermMemory {
|
|
|
113
139
|
}
|
|
114
140
|
if (metas.length > 0) {
|
|
115
141
|
metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
116
|
-
|
|
142
|
+
for (const m of metas) {
|
|
143
|
+
this.sessions.set(m.id, m);
|
|
144
|
+
}
|
|
145
|
+
await this.persistIndex();
|
|
117
146
|
}
|
|
118
147
|
}
|
|
119
148
|
async createSession(input) {
|
|
120
149
|
await this.ensureReady();
|
|
121
150
|
const id = uuidv4();
|
|
151
|
+
console.log(`[ShortTermMemory:${this.instanceId}] Creating session: ${id}`);
|
|
122
152
|
const now = Date.now();
|
|
123
153
|
const meta = {
|
|
124
154
|
id,
|
|
@@ -136,35 +166,40 @@ export class ShortTermMemory {
|
|
|
136
166
|
const sessionDir = this.getSessionDir(id);
|
|
137
167
|
await fs.ensureDir(sessionDir);
|
|
138
168
|
await fs.writeJson(this.getMessagesFile(id), [], { spaces: 2 });
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
169
|
+
// 更新内存
|
|
170
|
+
this.sessions.set(id, meta);
|
|
171
|
+
// 异步持久化
|
|
172
|
+
await this.persistIndex();
|
|
173
|
+
console.log(`[ShortTermMemory] Session created: ${id}`);
|
|
143
174
|
return { meta, messages: [] };
|
|
144
175
|
}
|
|
145
176
|
async listSessions() {
|
|
146
177
|
await this.ensureReady();
|
|
147
|
-
const metas =
|
|
178
|
+
const metas = Array.from(this.sessions.values());
|
|
148
179
|
metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
149
180
|
return metas;
|
|
150
181
|
}
|
|
151
182
|
async getSessionMeta(id) {
|
|
152
183
|
await this.ensureReady();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.
|
|
159
|
-
|
|
184
|
+
let meta = this.sessions.get(id) || null;
|
|
185
|
+
// 如果内存中没有,尝试重新加载磁盘(可能是其他进程创建的)
|
|
186
|
+
if (!meta) {
|
|
187
|
+
console.log(`[ShortTermMemory:${this.instanceId}] Cache miss for ${id}, reloading from disk...`);
|
|
188
|
+
await this.loadSessionsFromDisk();
|
|
189
|
+
meta = this.sessions.get(id) || null;
|
|
190
|
+
}
|
|
191
|
+
if (!meta) {
|
|
192
|
+
console.log(`[ShortTermMemory:${this.instanceId}] Session not found in memory after reload: ${id}. Total sessions: ${this.sessions.size}`);
|
|
193
|
+
// Log all available IDs for debugging
|
|
194
|
+
// console.log(`[ShortTermMemory:${this.instanceId}] Available IDs: ${Array.from(this.sessions.keys()).join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
return meta;
|
|
160
197
|
}
|
|
161
198
|
async updateSessionMeta(id, patch) {
|
|
162
199
|
await this.ensureReady();
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
if (idx < 0)
|
|
200
|
+
const current = this.sessions.get(id);
|
|
201
|
+
if (!current)
|
|
166
202
|
throw new Error('Session not found');
|
|
167
|
-
const current = metas[idx];
|
|
168
203
|
const next = {
|
|
169
204
|
...current,
|
|
170
205
|
alias: patch.alias === null ? undefined : (patch.alias !== undefined ? (String(patch.alias).trim() || undefined) : current.alias),
|
|
@@ -192,24 +227,17 @@ export class ShortTermMemory {
|
|
|
192
227
|
next.context = val;
|
|
193
228
|
}
|
|
194
229
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
199
|
-
await this.saveIndex(metas);
|
|
200
|
-
this.metaCache.set(id, next);
|
|
230
|
+
// 更新内存并持久化
|
|
231
|
+
this.sessions.set(id, next);
|
|
232
|
+
await this.persistIndex();
|
|
201
233
|
return next;
|
|
202
234
|
}
|
|
203
235
|
async persistSession(id) {
|
|
204
236
|
await this.ensureReady();
|
|
205
|
-
const current =
|
|
237
|
+
const current = this.sessions.get(id);
|
|
206
238
|
if (!current)
|
|
207
239
|
throw new Error('Session not found');
|
|
208
240
|
const now = Date.now();
|
|
209
|
-
const metas = await this.loadIndex();
|
|
210
|
-
const idx = metas.findIndex(m => m.id === id);
|
|
211
|
-
if (idx < 0)
|
|
212
|
-
throw new Error('Session not found');
|
|
213
241
|
let context = '';
|
|
214
242
|
try {
|
|
215
243
|
const ctxFile = this.getContextFile(id);
|
|
@@ -221,10 +249,8 @@ export class ShortTermMemory {
|
|
|
221
249
|
// ignore
|
|
222
250
|
}
|
|
223
251
|
const next = { ...current, context: context || undefined, updatedAt: now, lastActiveAt: now };
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
await this.saveIndex(metas);
|
|
227
|
-
this.metaCache.set(id, next);
|
|
252
|
+
this.sessions.set(id, next);
|
|
253
|
+
await this.persistIndex();
|
|
228
254
|
await fs.ensureDir(this.getSessionDir(id));
|
|
229
255
|
await fs.ensureFile(this.getMessagesFile(id));
|
|
230
256
|
await fs.ensureFile(this.getRunsFile(id));
|
|
@@ -241,6 +267,7 @@ export class ShortTermMemory {
|
|
|
241
267
|
messages = Array.isArray(data) ? data : [];
|
|
242
268
|
}
|
|
243
269
|
catch {
|
|
270
|
+
// 如果消息文件不存在或读取失败,尝试重新创建
|
|
244
271
|
await fs.ensureDir(this.getSessionDir(id));
|
|
245
272
|
await fs.writeJson(msgFile, [], { spaces: 2 });
|
|
246
273
|
messages = [];
|
|
@@ -259,8 +286,7 @@ export class ShortTermMemory {
|
|
|
259
286
|
}
|
|
260
287
|
async createRun(sessionId, userContent, runId) {
|
|
261
288
|
await this.ensureReady();
|
|
262
|
-
|
|
263
|
-
if (!meta)
|
|
289
|
+
if (!this.sessions.has(sessionId))
|
|
264
290
|
throw new Error('Session not found');
|
|
265
291
|
const now = Date.now();
|
|
266
292
|
const id = runId || uuidv4();
|
|
@@ -323,88 +349,91 @@ export class ShortTermMemory {
|
|
|
323
349
|
}
|
|
324
350
|
}
|
|
325
351
|
async getMessages(sessionId) {
|
|
352
|
+
// 复用 getSession 逻辑,它包含了文件读取和错误处理
|
|
326
353
|
const session = await this.getSession(sessionId);
|
|
327
354
|
return session ? session.messages : [];
|
|
328
355
|
}
|
|
329
356
|
async addMessage(sessionId, message) {
|
|
330
357
|
await this.ensureReady();
|
|
331
|
-
const
|
|
332
|
-
if (!
|
|
358
|
+
const sessionMeta = this.sessions.get(sessionId);
|
|
359
|
+
if (!sessionMeta)
|
|
333
360
|
throw new Error(`Session ${sessionId} not found`);
|
|
334
361
|
const msg = { ...message };
|
|
335
362
|
if (!msg.timestamp)
|
|
336
363
|
msg.timestamp = Date.now();
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
await this.saveIndex(metas);
|
|
349
|
-
this.metaCache.set(sessionId, metas.find(m => m.id === sessionId));
|
|
364
|
+
// 更新 messages.json
|
|
365
|
+
// 注意:这里仍然存在 messages.json 的并发写入风险,但比 index.json 低,因为通常一个 session 是串行的
|
|
366
|
+
// 如果需要严格安全,也应该为每个 session 维护一个 write queue
|
|
367
|
+
const msgFile = this.getMessagesFile(sessionId);
|
|
368
|
+
let messages = [];
|
|
369
|
+
try {
|
|
370
|
+
const data = await fs.readJson(msgFile);
|
|
371
|
+
messages = Array.isArray(data) ? data : [];
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
messages = [];
|
|
350
375
|
}
|
|
376
|
+
messages.push(msg);
|
|
377
|
+
await fs.writeJson(msgFile, messages, { spaces: 2 });
|
|
378
|
+
// 更新 Session Meta
|
|
379
|
+
const nextMeta = {
|
|
380
|
+
...sessionMeta,
|
|
381
|
+
updatedAt: Date.now(),
|
|
382
|
+
lastActiveAt: Date.now()
|
|
383
|
+
};
|
|
384
|
+
this.sessions.set(sessionId, nextMeta);
|
|
385
|
+
await this.persistIndex();
|
|
351
386
|
}
|
|
352
387
|
async clearMessages(sessionId) {
|
|
353
388
|
await this.ensureReady();
|
|
354
|
-
const
|
|
355
|
-
if (!
|
|
389
|
+
const sessionMeta = this.sessions.get(sessionId);
|
|
390
|
+
if (!sessionMeta)
|
|
356
391
|
throw new Error(`Session ${sessionId} not found`);
|
|
357
392
|
await fs.writeJson(this.getMessagesFile(sessionId), [], { spaces: 2 });
|
|
358
393
|
// Update timestamp
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
};
|
|
367
|
-
await this.saveIndex(metas);
|
|
368
|
-
this.metaCache.set(sessionId, metas[idx]);
|
|
369
|
-
}
|
|
394
|
+
const nextMeta = {
|
|
395
|
+
...sessionMeta,
|
|
396
|
+
updatedAt: Date.now(),
|
|
397
|
+
lastActiveAt: Date.now()
|
|
398
|
+
};
|
|
399
|
+
this.sessions.set(sessionId, nextMeta);
|
|
400
|
+
await this.persistIndex();
|
|
370
401
|
}
|
|
371
402
|
async deleteSession(id) {
|
|
372
403
|
await this.ensureReady();
|
|
373
|
-
|
|
374
|
-
const next = metas.filter(m => m.id !== id);
|
|
375
|
-
if (next.length === metas.length)
|
|
404
|
+
if (!this.sessions.has(id))
|
|
376
405
|
throw new Error('Session not found');
|
|
377
|
-
|
|
378
|
-
this.
|
|
379
|
-
|
|
406
|
+
this.sessions.delete(id);
|
|
407
|
+
await this.persistIndex();
|
|
408
|
+
try {
|
|
409
|
+
await fs.remove(this.getSessionDir(id));
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
console.error(`[ShortTermMemory] Failed to delete session dir ${id}:`, e);
|
|
413
|
+
}
|
|
380
414
|
}
|
|
381
415
|
async cleanupOldSessions(maxAgeDays) {
|
|
382
416
|
await this.ensureReady();
|
|
383
|
-
const metas = await this.loadIndex();
|
|
384
417
|
const now = Date.now();
|
|
385
418
|
const msPerDay = 24 * 60 * 60 * 1000;
|
|
386
|
-
const keep = [];
|
|
387
419
|
const toDelete = [];
|
|
388
|
-
for (const meta of
|
|
420
|
+
for (const meta of this.sessions.values()) {
|
|
389
421
|
const updatedAt = typeof meta.updatedAt === 'number' ? meta.updatedAt : 0;
|
|
390
422
|
const ageDays = (now - updatedAt) / msPerDay;
|
|
391
423
|
if (ageDays > maxAgeDays)
|
|
392
|
-
toDelete.push(meta);
|
|
393
|
-
else
|
|
394
|
-
keep.push(meta);
|
|
424
|
+
toDelete.push(meta.id);
|
|
395
425
|
}
|
|
396
|
-
for (const
|
|
426
|
+
for (const id of toDelete) {
|
|
427
|
+
this.sessions.delete(id);
|
|
397
428
|
try {
|
|
398
|
-
await fs.remove(this.getSessionDir(
|
|
399
|
-
this.metaCache.delete(meta.id);
|
|
429
|
+
await fs.remove(this.getSessionDir(id));
|
|
400
430
|
}
|
|
401
431
|
catch (e) {
|
|
402
|
-
console.error(`[ShortTermMemory] Failed to delete session ${
|
|
432
|
+
console.error(`[ShortTermMemory] Failed to delete session ${id}:`, e);
|
|
403
433
|
}
|
|
404
434
|
}
|
|
405
435
|
if (toDelete.length > 0) {
|
|
406
|
-
|
|
407
|
-
await this.saveIndex(keep);
|
|
436
|
+
await this.persistIndex();
|
|
408
437
|
}
|
|
409
438
|
return toDelete.length;
|
|
410
439
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -547,8 +547,10 @@ eventBus.onEvent('message', async (event) => {
|
|
|
547
547
|
const { sessionId, payload, channel: channelName } = event;
|
|
548
548
|
const content = payload.content;
|
|
549
549
|
try {
|
|
550
|
+
console.log(`[EventBus] Processing message for session: ${sessionId}`);
|
|
550
551
|
const session = await memory.getSession(sessionId);
|
|
551
552
|
if (!session) {
|
|
553
|
+
console.error(`[EventBus] Session not found: ${sessionId}`);
|
|
552
554
|
throw new Error('Session not found. Please create or select a session first.');
|
|
553
555
|
}
|
|
554
556
|
await memory.addMessage(sessionId, { role: 'user', content, timestamp: Date.now() });
|
package/package.json
CHANGED