xiaozuoassistant 0.1.75 → 0.1.77
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/client/assets/{browser-ponyfill-C_IVAH4X.js → browser-ponyfill-BVTKMYN8.js} +1 -1
- package/dist/client/assets/{index-GtsAgoQR.js → index-B8uwjNT9.js} +2 -2
- package/dist/client/index.html +1 -1
- package/dist/server/config/loader.js +6 -2
- package/dist/server/core/brain.js +6 -2
- package/dist/server/core/memories/manager.js +42 -37
- package/dist/server/core/memories/short-term.js +25 -14
- package/dist/server/index.js +7 -1
- package/package.json +1 -1
package/dist/client/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍇</text></svg>" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>xiaozuoAssistant</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-B8uwjNT9.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-B5Qw98nG.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -27,7 +27,8 @@ try {
|
|
|
27
27
|
temperature: 0.7,
|
|
28
28
|
requestTimeoutMs: 600000,
|
|
29
29
|
maxRetries: 2,
|
|
30
|
-
maxToolIterations: 200
|
|
30
|
+
maxToolIterations: 200,
|
|
31
|
+
maxHistoryMessages: 20
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
else {
|
|
@@ -39,6 +40,8 @@ try {
|
|
|
39
40
|
loadedConfig.llm.maxRetries = 2;
|
|
40
41
|
if (loadedConfig.llm.maxToolIterations === undefined)
|
|
41
42
|
loadedConfig.llm.maxToolIterations = 200;
|
|
43
|
+
if (loadedConfig.llm.maxHistoryMessages === undefined)
|
|
44
|
+
loadedConfig.llm.maxHistoryMessages = 20;
|
|
42
45
|
}
|
|
43
46
|
// Override with env vars if present (optional, but good for security)
|
|
44
47
|
if (process.env.XIAOZUO_LLM_API_KEY)
|
|
@@ -60,7 +63,8 @@ catch (error) {
|
|
|
60
63
|
temperature: 0.7,
|
|
61
64
|
requestTimeoutMs: 600000,
|
|
62
65
|
maxRetries: 2,
|
|
63
|
-
maxToolIterations: 200
|
|
66
|
+
maxToolIterations: 200,
|
|
67
|
+
maxHistoryMessages: 20
|
|
64
68
|
},
|
|
65
69
|
logging: { level: 'info' },
|
|
66
70
|
scheduler: { memoryMaintenanceCron: '0 0 * * *', sessionRetentionDays: 5 },
|
|
@@ -21,7 +21,11 @@ export class Brain {
|
|
|
21
21
|
console.log('[Brain] Processing message:', newMessage);
|
|
22
22
|
const defaultSystemPrompt = systemPrompt || config.systemPrompt || SYSTEM_PROMPT || 'You are xiaozuoAssistant, a helpful AI assistant. You can use tools to help users.';
|
|
23
23
|
// Convert history messages to the format expected by OpenAI
|
|
24
|
-
|
|
24
|
+
// Strategy: Keep last N messages to avoid context window overflow
|
|
25
|
+
// TODO: A better strategy would be token-based truncation.
|
|
26
|
+
const MAX_HISTORY_MESSAGES = config.llm.maxHistoryMessages ?? 20;
|
|
27
|
+
const recentHistory = history.slice(-MAX_HISTORY_MESSAGES);
|
|
28
|
+
const messageHistory = recentHistory.map(m => {
|
|
25
29
|
const msg = { role: m.role, content: m.content };
|
|
26
30
|
if (m.name)
|
|
27
31
|
msg.name = m.name;
|
|
@@ -45,7 +49,7 @@ export class Brain {
|
|
|
45
49
|
if (process.env.DEBUG)
|
|
46
50
|
console.log('[Brain] LLM Response (snippet):', contentSnippet);
|
|
47
51
|
let iterations = 0;
|
|
48
|
-
const MAX_ITERATIONS = config.llm.maxToolIterations ?? 200
|
|
52
|
+
const MAX_ITERATIONS = config.llm.maxToolIterations ?? 15; // Reduced default from 200 to 15 for safety
|
|
49
53
|
while (response.choices[0].message.tool_calls && iterations < MAX_ITERATIONS) {
|
|
50
54
|
iterations++;
|
|
51
55
|
const toolCalls = response.choices[0].message.tool_calls;
|
|
@@ -128,47 +128,52 @@ ${recentMessages}
|
|
|
128
128
|
await this.updateSessionMeta(session.id, { lastArchivedAt: now });
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
// Process in chunks to avoid context overflow
|
|
132
|
+
const CHUNK_SIZE = 50; // Process 50 messages at a time
|
|
133
|
+
for (let i = 0; i < newMessages.length; i += CHUNK_SIZE) {
|
|
134
|
+
const chunk = newMessages.slice(i, i + CHUNK_SIZE);
|
|
135
|
+
const content = chunk.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
136
|
+
// 1. Memory Archiving (Project or General)
|
|
137
|
+
const projectIds = session.projectIds || (session.projectId ? [session.projectId] : []);
|
|
138
|
+
if (projectIds.length > 0) {
|
|
139
|
+
// Project Context
|
|
140
|
+
const projectInfo = await brain.extractKeyInformation(content, 'project');
|
|
141
|
+
if (projectInfo) {
|
|
142
|
+
for (const pid of projectIds) {
|
|
143
|
+
await this.vector.addMemory(projectInfo, 'project_archive', {
|
|
144
|
+
sessionId: session.id,
|
|
145
|
+
projectId: pid,
|
|
146
|
+
archivedAt: now
|
|
147
|
+
});
|
|
148
|
+
console.log(`[MemoryManager] Archived project info for Project ${pid} (Chunk ${i / CHUNK_SIZE + 1})`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// General Context (No Project)
|
|
154
|
+
const generalInfo = await brain.extractKeyInformation(content, 'general');
|
|
155
|
+
if (generalInfo) {
|
|
156
|
+
await this.vector.addMemory(generalInfo, 'long_term', {
|
|
140
157
|
sessionId: session.id,
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
archivedAt: now,
|
|
159
|
+
source: 'auto_archive'
|
|
143
160
|
});
|
|
144
|
-
console.log(`[MemoryManager] Archived
|
|
161
|
+
console.log(`[MemoryManager] Archived general info for Session ${session.id} (Chunk ${i / CHUNK_SIZE + 1})`);
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// 2. Notebook Memory Archiving
|
|
161
|
-
if (session.notebookId) {
|
|
162
|
-
const notebook = this.structured.getNotebook(session.notebookId);
|
|
163
|
-
if (notebook) {
|
|
164
|
-
// Extract notes regardless of keywords (Brain handles fallback)
|
|
165
|
-
const notes = await brain.extractNotebookNotes(content, notebook.keywords);
|
|
166
|
-
if (notes && notes.length > 0) {
|
|
167
|
-
for (const note of notes) {
|
|
168
|
-
if (!note.title || !note.content)
|
|
169
|
-
continue;
|
|
170
|
-
this.structured.createNote(require('uuid').v4(), session.notebookId, note.title, note.content);
|
|
171
|
-
console.log(`[MemoryManager] Created note "${note.title}" in Notebook ${notebook.name}`);
|
|
164
|
+
// 2. Notebook Memory Archiving
|
|
165
|
+
if (session.notebookId) {
|
|
166
|
+
const notebook = this.structured.getNotebook(session.notebookId);
|
|
167
|
+
if (notebook) {
|
|
168
|
+
// Extract notes regardless of keywords (Brain handles fallback)
|
|
169
|
+
const notes = await brain.extractNotebookNotes(content, notebook.keywords);
|
|
170
|
+
if (notes && notes.length > 0) {
|
|
171
|
+
for (const note of notes) {
|
|
172
|
+
if (!note.title || !note.content)
|
|
173
|
+
continue;
|
|
174
|
+
this.structured.createNote(require('uuid').v4(), session.notebookId, note.title, note.content);
|
|
175
|
+
console.log(`[MemoryManager] Created note "${note.title}" in Notebook ${notebook.name} (Chunk ${i / CHUNK_SIZE + 1})`);
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
}
|
|
@@ -20,6 +20,8 @@ export class ShortTermMemory {
|
|
|
20
20
|
this.initPromise = null;
|
|
21
21
|
// 持久化队列,确保串行写入 index.json
|
|
22
22
|
this.saveQueue = Promise.resolve();
|
|
23
|
+
// Session级写入队列,避免messages.json并发冲突
|
|
24
|
+
this.sessionQueues = new Map();
|
|
23
25
|
console.log(`[ShortTermMemory] Instance created: ${this.instanceId}`);
|
|
24
26
|
fs.ensureDirSync(SESSIONS_DIR);
|
|
25
27
|
}
|
|
@@ -416,20 +418,29 @@ export class ShortTermMemory {
|
|
|
416
418
|
const msg = { ...message };
|
|
417
419
|
if (!msg.timestamp)
|
|
418
420
|
msg.timestamp = Date.now();
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
421
|
+
// 使用队列确保对 messages.json 的串行写入
|
|
422
|
+
const queue = this.sessionQueues.get(sessionId) || Promise.resolve();
|
|
423
|
+
const nextTask = queue.then(async () => {
|
|
424
|
+
// 更新 messages.json
|
|
425
|
+
const msgFile = this.getMessagesFile(sessionId);
|
|
426
|
+
let messages = [];
|
|
427
|
+
try {
|
|
428
|
+
const data = await fs.readJson(msgFile);
|
|
429
|
+
messages = Array.isArray(data) ? data : [];
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
messages = [];
|
|
433
|
+
}
|
|
434
|
+
messages.push(msg);
|
|
435
|
+
await fs.writeJson(msgFile, messages, { spaces: 2 });
|
|
436
|
+
}).catch(e => {
|
|
437
|
+
console.error(`[ShortTermMemory] Failed to write message for session ${sessionId}:`, e);
|
|
438
|
+
});
|
|
439
|
+
this.sessionQueues.set(sessionId, nextTask);
|
|
440
|
+
// 等待本次写入完成
|
|
441
|
+
await nextTask;
|
|
442
|
+
// 清理已完成的队列(可选,防止 Map 无限增长,但需小心并发)
|
|
443
|
+
// 简单策略:不清理,或者定期清理。由于 Session 数量有限,暂时保留。
|
|
433
444
|
// 更新 Session Meta
|
|
434
445
|
const nextMeta = {
|
|
435
446
|
...sessionMeta,
|
package/dist/server/index.js
CHANGED
|
@@ -235,6 +235,8 @@ app.get('/api/config', async (req, res) => {
|
|
|
235
235
|
baseURL: config.llm.baseURL,
|
|
236
236
|
model: config.llm.model,
|
|
237
237
|
temperature: config.llm.temperature,
|
|
238
|
+
maxHistoryMessages: config.llm.maxHistoryMessages,
|
|
239
|
+
maxToolIterations: config.llm.maxToolIterations,
|
|
238
240
|
systemPrompt: config.systemPrompt,
|
|
239
241
|
memoryMaintenanceCron: config.scheduler?.memoryMaintenanceCron,
|
|
240
242
|
sessionRetentionDays: config.scheduler?.sessionRetentionDays,
|
|
@@ -244,7 +246,7 @@ app.get('/api/config', async (req, res) => {
|
|
|
244
246
|
// 更新配置并写入 config.json
|
|
245
247
|
app.post('/api/config', async (req, res) => {
|
|
246
248
|
try {
|
|
247
|
-
const { userId, apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, sessionRetentionDays, workspace } = req.body;
|
|
249
|
+
const { userId, apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, sessionRetentionDays, workspace, maxHistoryMessages, maxToolIterations } = req.body;
|
|
248
250
|
// 更新配置对象
|
|
249
251
|
const newConfig = { ...config };
|
|
250
252
|
if (userId !== undefined)
|
|
@@ -257,6 +259,10 @@ app.post('/api/config', async (req, res) => {
|
|
|
257
259
|
newConfig.llm.model = model;
|
|
258
260
|
if (temperature !== undefined)
|
|
259
261
|
newConfig.llm.temperature = parseFloat(temperature);
|
|
262
|
+
if (maxHistoryMessages !== undefined)
|
|
263
|
+
newConfig.llm.maxHistoryMessages = parseInt(maxHistoryMessages, 10);
|
|
264
|
+
if (maxToolIterations !== undefined)
|
|
265
|
+
newConfig.llm.maxToolIterations = parseInt(maxToolIterations, 10);
|
|
260
266
|
if (systemPrompt !== undefined)
|
|
261
267
|
newConfig.systemPrompt = systemPrompt;
|
|
262
268
|
if (workspace !== undefined)
|
package/package.json
CHANGED