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