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.
@@ -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-GtsAgoQR.js"></script>
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
- const messageHistory = history.map(m => {
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
- const content = newMessages.map(m => `${m.role}: ${m.content}`).join('\n');
132
- // 1. Memory Archiving (Project or General)
133
- const projectIds = session.projectIds || (session.projectId ? [session.projectId] : []);
134
- if (projectIds.length > 0) {
135
- // Project Context
136
- const projectInfo = await brain.extractKeyInformation(content, 'project');
137
- if (projectInfo) {
138
- for (const pid of projectIds) {
139
- await this.vector.addMemory(projectInfo, 'project_archive', {
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
- projectId: pid,
142
- archivedAt: now
158
+ archivedAt: now,
159
+ source: 'auto_archive'
143
160
  });
144
- console.log(`[MemoryManager] Archived project info for Project ${pid}`);
161
+ console.log(`[MemoryManager] Archived general info for Session ${session.id} (Chunk ${i / CHUNK_SIZE + 1})`);
145
162
  }
146
163
  }
147
- }
148
- else {
149
- // General Context (No Project)
150
- const generalInfo = await brain.extractKeyInformation(content, 'general');
151
- if (generalInfo) {
152
- await this.vector.addMemory(generalInfo, 'long_term', {
153
- sessionId: session.id,
154
- archivedAt: now,
155
- source: 'auto_archive'
156
- });
157
- console.log(`[MemoryManager] Archived general info for Session ${session.id}`);
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
- // 更新 messages.json
420
- // 注意:这里仍然存在 messages.json 的并发写入风险,但比 index.json 低,因为通常一个 session 是串行的
421
- // 如果需要严格安全,也应该为每个 session 维护一个 write queue
422
- const msgFile = this.getMessagesFile(sessionId);
423
- let messages = [];
424
- try {
425
- const data = await fs.readJson(msgFile);
426
- messages = Array.isArray(data) ? data : [];
427
- }
428
- catch {
429
- messages = [];
430
- }
431
- messages.push(msg);
432
- await fs.writeJson(msgFile, messages, { spaces: 2 });
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,
@@ -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
@@ -2,7 +2,7 @@
2
2
  "name": "xiaozuoassistant",
3
3
  "private": false,
4
4
  "description": "Your personal, locally-hosted AI assistant for office productivity.",
5
- "version": "0.1.75",
5
+ "version": "0.1.77",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {