xiaozuoassistant 0.1.51 → 0.1.53

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,8 +5,8 @@
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-BwZQ_7Ty.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-C72E_nIr.css">
8
+ <script type="module" crossorigin src="/assets/index-hkDzKLlN.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-ZHPWrPl3.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -27,8 +27,17 @@
27
27
  "aliasHint": "Alias is shown in the list and header. Empty falls back to session ID.",
28
28
  "workspaceLabel": "Workspace",
29
29
  "workspaceHint": "File tools default to this workspace and are restricted to it.",
30
+ "contextLabel": "Context (optional)",
31
+ "contextPlaceholder": "E.g. project background, terminology, current conclusions, long-lived context...",
32
+ "contextHint": "This is persisted and can be used as session context in complex tasks.",
30
33
  "selectWorkspace": "Select workspace",
31
- "selectCurrentFolder": "Select current folder"
34
+ "selectCurrentFolder": "Select current folder",
35
+ "persistNow": "Persist now",
36
+ "persisting": "Persisting...",
37
+ "persistSuccess": "Persisted",
38
+ "delete": "Delete session",
39
+ "deleting": "Deleting...",
40
+ "deleteConfirm": "Are you sure you want to delete this session?"
32
41
  },
33
42
  "settings": {
34
43
  "title": "Settings",
@@ -51,6 +60,9 @@
51
60
  "scheduler": {
52
61
  "memoryMaintenance": "Memory Maintenance",
53
62
  "description": "Configure when the system should compress old memories and delete expired sessions.",
63
+ "sessionRetentionDays": "Session retention",
64
+ "sessionRetentionHint": "Sessions inactive for longer than this will be deleted during maintenance.",
65
+ "days": "days",
54
66
  "cronSchedule": "Cron Schedule",
55
67
  "cronHint": "Format: Minute Hour Day Month DayOfWeek (e.g., \"0 0 * * *\" for daily at midnight)",
56
68
  "manualTrigger": "Manual Trigger",
@@ -27,8 +27,17 @@
27
27
  "aliasHint": "列表与聊天头部将优先展示别名。留空将回退显示会话 ID。",
28
28
  "workspaceLabel": "Workspace(工作目录)",
29
29
  "workspaceHint": "文件相关工具默认以该目录为工作区,并限制访问范围。",
30
+ "contextLabel": "上下文数据(可选)",
31
+ "contextPlaceholder": "例如:项目背景、术语约定、当前阶段结论、需要长期保持的上下文...",
32
+ "contextHint": "该内容会持久化保存,并在复杂任务中作为会话上下文使用。",
30
33
  "selectWorkspace": "选择工作目录",
31
- "selectCurrentFolder": "选择当前目录"
34
+ "selectCurrentFolder": "选择当前目录",
35
+ "persistNow": "立即持久化",
36
+ "persisting": "持久化中...",
37
+ "persistSuccess": "已持久化",
38
+ "delete": "删除会话",
39
+ "deleting": "删除中...",
40
+ "deleteConfirm": "确定要删除这个会话吗?"
32
41
  },
33
42
  "settings": {
34
43
  "title": "设置",
@@ -51,6 +60,9 @@
51
60
  "scheduler": {
52
61
  "memoryMaintenance": "记忆维护",
53
62
  "description": "配置系统何时压缩旧记忆并删除过期会话。",
63
+ "sessionRetentionDays": "会话保留天数",
64
+ "sessionRetentionHint": "超过该天数未活跃的会话会在维护任务中被自动删除。",
65
+ "days": "天",
54
66
  "cronSchedule": "Cron 表达式",
55
67
  "cronHint": "格式: 分 时 日 月 周 (例如 \"0 0 * * *\" 表示每天午夜)",
56
68
  "manualTrigger": "手动触发",
@@ -10,7 +10,9 @@ try {
10
10
  if (!loadedConfig.systemPrompt)
11
11
  loadedConfig.systemPrompt = SYSTEM_PROMPT;
12
12
  if (!loadedConfig.scheduler)
13
- loadedConfig.scheduler = { memoryMaintenanceCron: '0 0 * * *' };
13
+ loadedConfig.scheduler = { memoryMaintenanceCron: '0 0 * * *', sessionRetentionDays: 5 };
14
+ if (loadedConfig.scheduler.sessionRetentionDays === undefined)
15
+ loadedConfig.scheduler.sessionRetentionDays = 5;
14
16
  if (!loadedConfig.workspace)
15
17
  loadedConfig.workspace = process.cwd();
16
18
  // Ensure LLM config exists
@@ -22,7 +24,8 @@ try {
22
24
  model: 'qwen-plus',
23
25
  temperature: 0.7,
24
26
  requestTimeoutMs: 600000,
25
- maxRetries: 2
27
+ maxRetries: 2,
28
+ maxToolIterations: 200
26
29
  };
27
30
  }
28
31
  else {
@@ -32,6 +35,8 @@ try {
32
35
  loadedConfig.llm.requestTimeoutMs = 600000;
33
36
  if (loadedConfig.llm.maxRetries === undefined)
34
37
  loadedConfig.llm.maxRetries = 2;
38
+ if (loadedConfig.llm.maxToolIterations === undefined)
39
+ loadedConfig.llm.maxToolIterations = 200;
35
40
  }
36
41
  // Override with env vars if present (optional, but good for security)
37
42
  if (process.env.XIAOZUO_LLM_API_KEY)
@@ -51,10 +56,11 @@ catch (error) {
51
56
  model: 'qwen-plus',
52
57
  temperature: 0.7,
53
58
  requestTimeoutMs: 600000,
54
- maxRetries: 2
59
+ maxRetries: 2,
60
+ maxToolIterations: 200
55
61
  },
56
62
  logging: { level: 'info' },
57
- scheduler: { memoryMaintenanceCron: '0 0 * * *' },
63
+ scheduler: { memoryMaintenanceCron: '0 0 * * *', sessionRetentionDays: 5 },
58
64
  workspace: process.cwd(),
59
65
  systemPrompt: SYSTEM_PROMPT
60
66
  };
@@ -32,7 +32,7 @@ export class AgentRuntime {
32
32
  console.log(`[Agent:${this.name}] Calling LLM...`);
33
33
  let response = await this.callLLM(messages);
34
34
  let iterations = 0;
35
- const MAX_ITERATIONS = 10;
35
+ const MAX_ITERATIONS = config.llm.maxToolIterations ?? 200;
36
36
  while (response.choices[0].message.tool_calls && iterations < MAX_ITERATIONS) {
37
37
  iterations++;
38
38
  const toolCalls = response.choices[0].message.tool_calls;
@@ -68,6 +68,10 @@ export class AgentRuntime {
68
68
  }
69
69
  response = await this.callLLM(messages);
70
70
  }
71
+ if (response.choices[0].message.tool_calls && iterations >= MAX_ITERATIONS) {
72
+ const content = response.choices[0].message.content || 'No response generated.';
73
+ return `${content}\n\n(已到达回合上限,建议回复“继续”以接着执行;可在 config.json 设置 llm.maxToolIterations,当前=${MAX_ITERATIONS})`;
74
+ }
71
75
  return response.choices[0].message.content || 'No response generated.';
72
76
  }
73
77
  catch (error) {
@@ -45,7 +45,7 @@ export class Brain {
45
45
  if (process.env.DEBUG)
46
46
  console.log('[Brain] LLM Response (snippet):', contentSnippet);
47
47
  let iterations = 0;
48
- const MAX_ITERATIONS = 10;
48
+ const MAX_ITERATIONS = config.llm.maxToolIterations ?? 200;
49
49
  while (response.choices[0].message.tool_calls && iterations < MAX_ITERATIONS) {
50
50
  iterations++;
51
51
  const toolCalls = response.choices[0].message.tool_calls;
@@ -92,10 +92,13 @@ export class Brain {
92
92
  if (process.env.DEBUG)
93
93
  console.log('[Brain] LLM Response (after tool, snippet):', nextContentSnippet);
94
94
  }
95
+ const hitLimit = Boolean(response.choices[0].message.tool_calls) && iterations >= MAX_ITERATIONS;
95
96
  const finalContent = response.choices[0].message.content || 'I could not generate a response.';
96
97
  if (process.env.DEBUG)
97
98
  console.log('[Brain] Final Response (snippet):', finalContent.substring(0, 100) + '...');
98
- return finalContent;
99
+ if (!hitLimit)
100
+ return finalContent;
101
+ return `${finalContent}\n\n(已到达回合上限,建议回复“继续”以接着执行;可在 config.json 设置 llm.maxToolIterations,当前=${MAX_ITERATIONS})`;
99
102
  }
100
103
  catch (error) {
101
104
  console.error('[Brain] Error in processing:', error);
@@ -27,6 +27,9 @@ export class MemoryManager {
27
27
  async updateSessionMeta(id, patch) {
28
28
  return this.shortTerm.updateSessionMeta(id, patch);
29
29
  }
30
+ async persistSession(id) {
31
+ return this.shortTerm.persistSession(id);
32
+ }
30
33
  async deleteSession(id) {
31
34
  return this.shortTerm.deleteSession(id);
32
35
  }
@@ -80,16 +83,18 @@ ${recentMessages}
80
83
  }
81
84
  // --- Maintenance ---
82
85
  // Managed by Scheduler
83
- async runMaintenance(maxAgeDays = 15) {
84
- console.log(`[MemoryManager] Running maintenance (Max Age: ${maxAgeDays} days)...`);
86
+ async runMaintenance(input) {
87
+ const sessionMaxAgeDays = input?.sessionMaxAgeDays ?? 5;
88
+ const vectorMaxAgeDays = input?.vectorMaxAgeDays ?? 15;
89
+ console.log(`[MemoryManager] Running maintenance (Sessions: ${sessionMaxAgeDays} days, Vector: ${vectorMaxAgeDays} days)...`);
85
90
  try {
86
91
  // 1. Clean up old sessions
87
- const deletedSessions = await this.shortTerm.cleanupOldSessions(maxAgeDays);
92
+ const deletedSessions = await this.shortTerm.cleanupOldSessions(sessionMaxAgeDays);
88
93
  if (deletedSessions > 0) {
89
94
  console.log(`[MemoryManager] Deleted ${deletedSessions} old sessions.`);
90
95
  }
91
96
  // 2. Summarize and Prune Vector Memories
92
- const oldMemories = await this.vector.getMemoriesOlderThan(maxAgeDays);
97
+ const oldMemories = await this.vector.getMemoriesOlderThan(vectorMaxAgeDays);
93
98
  if (oldMemories.length > 0) {
94
99
  console.log(`[MemoryManager] Found ${oldMemories.length} old memories to summarize.`);
95
100
  // Batch process
@@ -112,7 +117,7 @@ ${recentMessages}
112
117
  }
113
118
  }
114
119
  // Prune after summarization
115
- await this.vector.pruneOldMemories(maxAgeDays);
120
+ await this.vector.pruneOldMemories(vectorMaxAgeDays);
116
121
  }
117
122
  else {
118
123
  // No old memories
@@ -50,6 +50,9 @@ export class ShortTermMemory {
50
50
  getMessagesFile(sessionId) {
51
51
  return path.join(this.getSessionDir(sessionId), 'messages.json');
52
52
  }
53
+ getContextFile(sessionId) {
54
+ return path.join(this.getSessionDir(sessionId), 'context.txt');
55
+ }
53
56
  getRunsFile(sessionId) {
54
57
  return path.join(this.getSessionDir(sessionId), 'runs.json');
55
58
  }
@@ -165,10 +168,58 @@ export class ShortTermMemory {
165
168
  updatedAt: Date.now()
166
169
  };
167
170
  await fs.ensureDir(next.workspace);
168
- metas[idx] = next;
171
+ if (patch.context !== undefined) {
172
+ const ctxFile = this.getContextFile(id);
173
+ const val = String(patch.context ?? '').trim();
174
+ if (!val) {
175
+ try {
176
+ await fs.remove(ctxFile);
177
+ }
178
+ catch { /* ignore */ }
179
+ delete next.context;
180
+ }
181
+ else {
182
+ await fs.ensureDir(this.getSessionDir(id));
183
+ await fs.writeFile(ctxFile, val, 'utf-8');
184
+ next.context = val;
185
+ }
186
+ }
187
+ const metaForIndex = { ...next };
188
+ delete metaForIndex.context;
189
+ metas[idx] = metaForIndex;
190
+ metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
191
+ await this.saveIndex(metas);
192
+ this.metaCache.set(id, next);
193
+ return next;
194
+ }
195
+ async persistSession(id) {
196
+ await this.ensureReady();
197
+ const current = await this.getSessionMeta(id);
198
+ if (!current)
199
+ throw new Error('Session not found');
200
+ const now = Date.now();
201
+ const metas = await this.loadIndex();
202
+ const idx = metas.findIndex(m => m.id === id);
203
+ if (idx < 0)
204
+ throw new Error('Session not found');
205
+ let context = '';
206
+ try {
207
+ const ctxFile = this.getContextFile(id);
208
+ if (await fs.pathExists(ctxFile)) {
209
+ context = String(await fs.readFile(ctxFile, 'utf-8') || '').trim();
210
+ }
211
+ }
212
+ catch {
213
+ // ignore
214
+ }
215
+ const next = { ...current, context: context || undefined, updatedAt: now, lastActiveAt: now };
216
+ metas[idx] = { ...metas[idx], updatedAt: now, lastActiveAt: now };
169
217
  metas.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
170
218
  await this.saveIndex(metas);
171
219
  this.metaCache.set(id, next);
220
+ await fs.ensureDir(this.getSessionDir(id));
221
+ await fs.ensureFile(this.getMessagesFile(id));
222
+ await fs.ensureFile(this.getRunsFile(id));
172
223
  return next;
173
224
  }
174
225
  async getSession(id) {
@@ -186,7 +237,17 @@ export class ShortTermMemory {
186
237
  await fs.writeJson(msgFile, [], { spaces: 2 });
187
238
  messages = [];
188
239
  }
189
- return { meta, messages };
240
+ let context = '';
241
+ try {
242
+ const ctxFile = this.getContextFile(id);
243
+ if (await fs.pathExists(ctxFile)) {
244
+ context = String(await fs.readFile(ctxFile, 'utf-8') || '').trim();
245
+ }
246
+ }
247
+ catch {
248
+ // ignore
249
+ }
250
+ return { meta: { ...meta, context: context || undefined }, messages };
190
251
  }
191
252
  async createRun(sessionId, userContent, runId) {
192
253
  await this.ensureReady();
@@ -15,10 +15,12 @@ export const initScheduler = () => {
15
15
  // Or we can assume restart means config changed, so maybe we don't need to run immediately?
16
16
  // Let's keep it simple: only schedule the cron job. Immediate run can be manual via API.
17
17
  cronTask = cron.schedule(cronSchedule, () => {
18
- memoryManager.runMaintenance();
18
+ const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
19
+ memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
19
20
  });
20
21
  };
21
22
  export const runMaintenanceNow = () => {
22
23
  console.log('[Scheduler] Manually triggering maintenance...');
23
- memoryManager.runMaintenance();
24
+ const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
25
+ memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
24
26
  };
@@ -108,8 +108,23 @@ app.post('/api/sessions', async (req, res) => {
108
108
  // 更新会话元信息
109
109
  app.patch('/api/sessions/:id', async (req, res) => {
110
110
  try {
111
- const { alias, workspace } = req.body || {};
112
- const meta = await memory.updateSessionMeta(req.params.id, { alias, workspace });
111
+ const { alias, workspace, context } = req.body || {};
112
+ const meta = await memory.updateSessionMeta(req.params.id, { alias, workspace, context });
113
+ res.json(meta);
114
+ }
115
+ catch (e) {
116
+ const msg = String(e?.message || 'Unknown error');
117
+ if (msg.toLowerCase().includes('not found')) {
118
+ res.status(404).json({ error: 'Session not found' });
119
+ return;
120
+ }
121
+ res.status(500).json({ error: msg });
122
+ }
123
+ });
124
+ // 手动持久化会话(刷新 updatedAt,并确保会话数据文件存在)
125
+ app.post('/api/sessions/:id/persist', async (req, res) => {
126
+ try {
127
+ const meta = await memory.persistSession(req.params.id);
113
128
  res.json(meta);
114
129
  }
115
130
  catch (e) {
@@ -167,13 +182,14 @@ app.get('/api/config', async (req, res) => {
167
182
  temperature: config.llm.temperature,
168
183
  systemPrompt: config.systemPrompt,
169
184
  memoryMaintenanceCron: config.scheduler?.memoryMaintenanceCron,
185
+ sessionRetentionDays: config.scheduler?.sessionRetentionDays,
170
186
  workspace: config.workspace
171
187
  });
172
188
  });
173
189
  // 更新配置并写入 config.json
174
190
  app.post('/api/config', async (req, res) => {
175
191
  try {
176
- const { apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, workspace } = req.body;
192
+ const { apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, sessionRetentionDays, workspace } = req.body;
177
193
  // 更新配置对象
178
194
  const newConfig = { ...config };
179
195
  if (apiKey !== undefined)
@@ -218,6 +234,15 @@ app.post('/api/config', async (req, res) => {
218
234
  restartScheduler = true;
219
235
  }
220
236
  }
237
+ if (sessionRetentionDays !== undefined) {
238
+ if (!newConfig.scheduler)
239
+ newConfig.scheduler = {};
240
+ const days = Number(sessionRetentionDays);
241
+ if (Number.isFinite(days) && days > 0 && newConfig.scheduler.sessionRetentionDays !== days) {
242
+ newConfig.scheduler.sessionRetentionDays = days;
243
+ restartScheduler = true;
244
+ }
245
+ }
221
246
  // 保存到文件
222
247
  saveConfig(newConfig);
223
248
  // 如果 LLM 配置变了,更新 Brain
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.51",
5
+ "version": "0.1.53",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -27,8 +27,17 @@
27
27
  "aliasHint": "Alias is shown in the list and header. Empty falls back to session ID.",
28
28
  "workspaceLabel": "Workspace",
29
29
  "workspaceHint": "File tools default to this workspace and are restricted to it.",
30
+ "contextLabel": "Context (optional)",
31
+ "contextPlaceholder": "E.g. project background, terminology, current conclusions, long-lived context...",
32
+ "contextHint": "This is persisted and can be used as session context in complex tasks.",
30
33
  "selectWorkspace": "Select workspace",
31
- "selectCurrentFolder": "Select current folder"
34
+ "selectCurrentFolder": "Select current folder",
35
+ "persistNow": "Persist now",
36
+ "persisting": "Persisting...",
37
+ "persistSuccess": "Persisted",
38
+ "delete": "Delete session",
39
+ "deleting": "Deleting...",
40
+ "deleteConfirm": "Are you sure you want to delete this session?"
32
41
  },
33
42
  "settings": {
34
43
  "title": "Settings",
@@ -51,6 +60,9 @@
51
60
  "scheduler": {
52
61
  "memoryMaintenance": "Memory Maintenance",
53
62
  "description": "Configure when the system should compress old memories and delete expired sessions.",
63
+ "sessionRetentionDays": "Session retention",
64
+ "sessionRetentionHint": "Sessions inactive for longer than this will be deleted during maintenance.",
65
+ "days": "days",
54
66
  "cronSchedule": "Cron Schedule",
55
67
  "cronHint": "Format: Minute Hour Day Month DayOfWeek (e.g., \"0 0 * * *\" for daily at midnight)",
56
68
  "manualTrigger": "Manual Trigger",
@@ -27,8 +27,17 @@
27
27
  "aliasHint": "列表与聊天头部将优先展示别名。留空将回退显示会话 ID。",
28
28
  "workspaceLabel": "Workspace(工作目录)",
29
29
  "workspaceHint": "文件相关工具默认以该目录为工作区,并限制访问范围。",
30
+ "contextLabel": "上下文数据(可选)",
31
+ "contextPlaceholder": "例如:项目背景、术语约定、当前阶段结论、需要长期保持的上下文...",
32
+ "contextHint": "该内容会持久化保存,并在复杂任务中作为会话上下文使用。",
30
33
  "selectWorkspace": "选择工作目录",
31
- "selectCurrentFolder": "选择当前目录"
34
+ "selectCurrentFolder": "选择当前目录",
35
+ "persistNow": "立即持久化",
36
+ "persisting": "持久化中...",
37
+ "persistSuccess": "已持久化",
38
+ "delete": "删除会话",
39
+ "deleting": "删除中...",
40
+ "deleteConfirm": "确定要删除这个会话吗?"
32
41
  },
33
42
  "settings": {
34
43
  "title": "设置",
@@ -51,6 +60,9 @@
51
60
  "scheduler": {
52
61
  "memoryMaintenance": "记忆维护",
53
62
  "description": "配置系统何时压缩旧记忆并删除过期会话。",
63
+ "sessionRetentionDays": "会话保留天数",
64
+ "sessionRetentionHint": "超过该天数未活跃的会话会在维护任务中被自动删除。",
65
+ "days": "天",
54
66
  "cronSchedule": "Cron 表达式",
55
67
  "cronHint": "格式: 分 时 日 月 周 (例如 \"0 0 * * *\" 表示每天午夜)",
56
68
  "manualTrigger": "手动触发",