xiaozuoassistant 0.1.52 → 0.1.54

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-DKrBHrNq.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DJdLRlkB.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",
@@ -44,6 +53,15 @@
44
53
  "modelName": "Model Name",
45
54
  "temperature": "Temperature"
46
55
  },
56
+ "identity": {
57
+ "title": "User identity",
58
+ "userId": "UserId",
59
+ "userIdHint": "Used to load identity profile and memories for this userId.",
60
+ "name": "Name",
61
+ "role": "Role",
62
+ "company": "Company",
63
+ "email": "Email"
64
+ },
47
65
  "prompt": {
48
66
  "description": "Define how xiaozuoAssistant behaves and responds.",
49
67
  "placeholder": "You are a helpful AI assistant..."
@@ -51,6 +69,9 @@
51
69
  "scheduler": {
52
70
  "memoryMaintenance": "Memory Maintenance",
53
71
  "description": "Configure when the system should compress old memories and delete expired sessions.",
72
+ "sessionRetentionDays": "Session retention",
73
+ "sessionRetentionHint": "Sessions inactive for longer than this will be deleted during maintenance.",
74
+ "days": "days",
54
75
  "cronSchedule": "Cron Schedule",
55
76
  "cronHint": "Format: Minute Hour Day Month DayOfWeek (e.g., \"0 0 * * *\" for daily at midnight)",
56
77
  "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": "设置",
@@ -44,6 +53,15 @@
44
53
  "modelName": "模型名称",
45
54
  "temperature": "温度 (Temperature)"
46
55
  },
56
+ "identity": {
57
+ "title": "用户身份",
58
+ "userId": "UserId",
59
+ "userIdHint": "用于加载该 UserId 对应的身份资料与记忆。",
60
+ "name": "姓名",
61
+ "role": "岗位",
62
+ "company": "公司",
63
+ "email": "邮箱"
64
+ },
47
65
  "prompt": {
48
66
  "description": "定义 xiaozuoAssistant 的行为和响应方式。",
49
67
  "placeholder": "你是一个有用的 AI 助手..."
@@ -51,6 +69,9 @@
51
69
  "scheduler": {
52
70
  "memoryMaintenance": "记忆维护",
53
71
  "description": "配置系统何时压缩旧记忆并删除过期会话。",
72
+ "sessionRetentionDays": "会话保留天数",
73
+ "sessionRetentionHint": "超过该天数未活跃的会话会在维护任务中被自动删除。",
74
+ "days": "天",
54
75
  "cronSchedule": "Cron 表达式",
55
76
  "cronHint": "格式: 分 时 日 月 周 (例如 \"0 0 * * *\" 表示每天午夜)",
56
77
  "manualTrigger": "手动触发",
@@ -10,9 +10,13 @@ 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();
18
+ if (!loadedConfig.userId)
19
+ loadedConfig.userId = 'default';
16
20
  // Ensure LLM config exists
17
21
  if (!loadedConfig.llm) {
18
22
  loadedConfig.llm = {
@@ -47,6 +51,7 @@ catch (error) {
47
51
  console.warn('Failed to load config.json, creating a new one with defaults');
48
52
  loadedConfig = {
49
53
  server: { port: 3001, host: 'localhost' },
54
+ userId: 'default',
50
55
  llm: {
51
56
  provider: 'qwen',
52
57
  apiKey: '',
@@ -58,7 +63,7 @@ catch (error) {
58
63
  maxToolIterations: 200
59
64
  },
60
65
  logging: { level: 'info' },
61
- scheduler: { memoryMaintenanceCron: '0 0 * * *' },
66
+ scheduler: { memoryMaintenanceCron: '0 0 * * *', sessionRetentionDays: 5 },
62
67
  workspace: process.cwd(),
63
68
  systemPrompt: SYSTEM_PROMPT
64
69
  };
@@ -2,6 +2,7 @@ import { ShortTermMemory } from './short-term.js';
2
2
  import { VectorMemory } from './vector.js';
3
3
  import { StructuredMemory } from './structured.js';
4
4
  import { brain } from '../brain.js';
5
+ import { config } from '../../config/loader.js';
5
6
  export class MemoryManager {
6
7
  constructor() {
7
8
  this.shortTerm = ShortTermMemory.getInstance();
@@ -27,6 +28,9 @@ export class MemoryManager {
27
28
  async updateSessionMeta(id, patch) {
28
29
  return this.shortTerm.updateSessionMeta(id, patch);
29
30
  }
31
+ async persistSession(id) {
32
+ return this.shortTerm.persistSession(id);
33
+ }
30
34
  async deleteSession(id) {
31
35
  return this.shortTerm.deleteSession(id);
32
36
  }
@@ -37,7 +41,8 @@ export class MemoryManager {
37
41
  // For now, we only embed user messages or significant assistant responses
38
42
  // to avoid cluttering.
39
43
  if (message.content.length > 10) { // Simple filter
40
- this.vector.addMemory(message.content, 'recent', { sessionId, role: message.role })
44
+ const userId = config.userId || 'default';
45
+ this.vector.addMemory(message.content, 'recent', { sessionId, role: message.role, userId })
41
46
  .catch(err => console.error('Background vector add failed:', err));
42
47
  }
43
48
  // 3. Extract Facts (Structured) - This usually requires an LLM call to extract
@@ -57,18 +62,20 @@ export class MemoryManager {
57
62
  return this.shortTerm.getMessages(sessionId);
58
63
  }
59
64
  // --- Retrieval ---
60
- async getRelevantContext(query, sessionId) {
65
+ async getRelevantContext(query, sessionId, userId) {
66
+ const uid = userId || config.userId || 'default';
61
67
  // 1. Get Short-term context (recent messages)
62
68
  const history = await this.getHistory(sessionId);
63
69
  const recentMessages = history.slice(-10).map(m => `${m.role}: ${m.content}`).join('\n');
64
70
  // 2. Search Vector Memory (Recent & Long-term)
65
- const vectorResults = await this.vector.search(query, undefined, 3);
66
- const vectorContext = vectorResults.map(r => `[Memory]: ${r.text}`).join('\n');
71
+ const vectorResults = await this.vector.search(query, undefined, 10);
72
+ const vectorFiltered = vectorResults.filter(r => (r.metadata?.userId || 'default') === uid).slice(0, 3);
73
+ const vectorContext = vectorFiltered.map(r => `[Memory]: ${r.text}`).join('\n');
67
74
  // 3. Get User Profile (Structured)
68
- const profile = await this.structured.getUserProfile();
75
+ const profile = await this.structured.getUserProfile(uid);
69
76
  const profileContext = Object.entries(profile).map(([k, v]) => `[User Info] ${k}: ${v}`).join('\n');
70
77
  return `
71
- === User Profile ===
78
+ === User Identity (userId: ${uid}) ===
72
79
  ${profileContext}
73
80
 
74
81
  === Relevant Memories ===
@@ -78,18 +85,34 @@ ${vectorContext}
78
85
  ${recentMessages}
79
86
  `;
80
87
  }
88
+ getUserProfile(userId) {
89
+ const uid = userId || config.userId || 'default';
90
+ return this.structured.getUserProfile(uid);
91
+ }
92
+ updateUserProfile(userId, patch) {
93
+ const uid = userId || config.userId || 'default';
94
+ for (const [k, v] of Object.entries(patch || {})) {
95
+ const key = String(k).trim();
96
+ if (!key)
97
+ continue;
98
+ const value = v === null || v === undefined ? '' : String(v);
99
+ this.structured.updateUserProfile(uid, key, value);
100
+ }
101
+ }
81
102
  // --- Maintenance ---
82
103
  // Managed by Scheduler
83
- async runMaintenance(maxAgeDays = 15) {
84
- console.log(`[MemoryManager] Running maintenance (Max Age: ${maxAgeDays} days)...`);
104
+ async runMaintenance(input) {
105
+ const sessionMaxAgeDays = input?.sessionMaxAgeDays ?? 5;
106
+ const vectorMaxAgeDays = input?.vectorMaxAgeDays ?? 15;
107
+ console.log(`[MemoryManager] Running maintenance (Sessions: ${sessionMaxAgeDays} days, Vector: ${vectorMaxAgeDays} days)...`);
85
108
  try {
86
109
  // 1. Clean up old sessions
87
- const deletedSessions = await this.shortTerm.cleanupOldSessions(maxAgeDays);
110
+ const deletedSessions = await this.shortTerm.cleanupOldSessions(sessionMaxAgeDays);
88
111
  if (deletedSessions > 0) {
89
112
  console.log(`[MemoryManager] Deleted ${deletedSessions} old sessions.`);
90
113
  }
91
114
  // 2. Summarize and Prune Vector Memories
92
- const oldMemories = await this.vector.getMemoriesOlderThan(maxAgeDays);
115
+ const oldMemories = await this.vector.getMemoriesOlderThan(vectorMaxAgeDays);
93
116
  if (oldMemories.length > 0) {
94
117
  console.log(`[MemoryManager] Found ${oldMemories.length} old memories to summarize.`);
95
118
  // Batch process
@@ -112,7 +135,7 @@ ${recentMessages}
112
135
  }
113
136
  }
114
137
  // Prune after summarization
115
- await this.vector.pruneOldMemories(maxAgeDays);
138
+ await this.vector.pruneOldMemories(vectorMaxAgeDays);
116
139
  }
117
140
  else {
118
141
  // 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();
@@ -28,15 +28,18 @@ export class StructuredMemory {
28
28
  timestamp INTEGER
29
29
  )
30
30
  `).run();
31
- // User Profile: specialized facts
31
+ // User Profile: specialized facts (multi-user)
32
32
  this.db.prepare(`
33
33
  CREATE TABLE IF NOT EXISTS user_profile (
34
34
  id INTEGER PRIMARY KEY AUTOINCREMENT,
35
- key TEXT UNIQUE NOT NULL,
35
+ user_id TEXT NOT NULL,
36
+ key TEXT NOT NULL,
36
37
  value TEXT NOT NULL,
37
- updated_at INTEGER
38
+ updated_at INTEGER,
39
+ UNIQUE(user_id, key)
38
40
  )
39
41
  `).run();
42
+ this.migrateUserProfileIfNeeded();
40
43
  // Graph nodes (Entities)
41
44
  this.db.prepare(`
42
45
  CREATE TABLE IF NOT EXISTS entities (
@@ -59,6 +62,66 @@ export class StructuredMemory {
59
62
  )
60
63
  `).run();
61
64
  }
65
+ migrateUserProfileIfNeeded() {
66
+ try {
67
+ const columns = this.db.prepare(`PRAGMA table_info(user_profile)`).all();
68
+ const hasUserId = columns.some(c => c?.name === 'user_id');
69
+ let hasUniqueUserIdKey = false;
70
+ try {
71
+ const indexes = this.db.prepare('PRAGMA index_list(user_profile)').all();
72
+ for (const idx of indexes) {
73
+ if (!idx?.unique)
74
+ continue;
75
+ const cols = this.db.prepare(`PRAGMA index_info(${idx.name})`).all();
76
+ const names = cols.map(c => c?.name).filter(Boolean);
77
+ if (names.length === 2 && names.includes('user_id') && names.includes('key')) {
78
+ hasUniqueUserIdKey = true;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ hasUniqueUserIdKey = false;
85
+ }
86
+ if (hasUserId && hasUniqueUserIdKey)
87
+ return;
88
+ this.db.prepare(`
89
+ CREATE TABLE IF NOT EXISTS user_profile_v2 (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ user_id TEXT NOT NULL,
92
+ key TEXT NOT NULL,
93
+ value TEXT NOT NULL,
94
+ updated_at INTEGER,
95
+ UNIQUE(user_id, key)
96
+ )
97
+ `).run();
98
+ if (!hasUserId) {
99
+ const rows = this.db.prepare('SELECT key, value, updated_at FROM user_profile').all();
100
+ const ins = this.db.prepare('INSERT OR REPLACE INTO user_profile_v2 (user_id, key, value, updated_at) VALUES (?, ?, ?, ?)');
101
+ const tx = this.db.transaction(() => {
102
+ for (const row of rows) {
103
+ ins.run('default', row.key, row.value, row.updated_at ?? Date.now());
104
+ }
105
+ });
106
+ tx();
107
+ }
108
+ else {
109
+ const rows = this.db.prepare('SELECT user_id, key, value, updated_at FROM user_profile').all();
110
+ const ins = this.db.prepare('INSERT OR REPLACE INTO user_profile_v2 (user_id, key, value, updated_at) VALUES (?, ?, ?, ?)');
111
+ const tx = this.db.transaction(() => {
112
+ for (const row of rows) {
113
+ ins.run(row.user_id, row.key, row.value, row.updated_at ?? Date.now());
114
+ }
115
+ });
116
+ tx();
117
+ }
118
+ this.db.prepare('DROP TABLE IF EXISTS user_profile').run();
119
+ this.db.prepare('ALTER TABLE user_profile_v2 RENAME TO user_profile').run();
120
+ }
121
+ catch {
122
+ // ignore
123
+ }
124
+ }
62
125
  addFact(category, key, value, source = 'user') {
63
126
  const stmt = this.db.prepare(`
64
127
  INSERT INTO facts (category, key, value, source, timestamp)
@@ -72,16 +135,16 @@ export class StructuredMemory {
72
135
  }
73
136
  return this.db.prepare('SELECT * FROM facts').all();
74
137
  }
75
- updateUserProfile(key, value) {
138
+ updateUserProfile(userId, key, value) {
76
139
  const stmt = this.db.prepare(`
77
- INSERT INTO user_profile (key, value, updated_at)
78
- VALUES (?, ?, ?)
79
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
140
+ INSERT INTO user_profile (user_id, key, value, updated_at)
141
+ VALUES (?, ?, ?, ?)
142
+ ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
80
143
  `);
81
- stmt.run(key, value, Date.now());
144
+ stmt.run(userId || 'default', key, value, Date.now());
82
145
  }
83
- getUserProfile() {
84
- const rows = this.db.prepare('SELECT key, value FROM user_profile').all();
146
+ getUserProfile(userId) {
147
+ const rows = this.db.prepare('SELECT key, value FROM user_profile WHERE user_id = ?').all(userId || 'default');
85
148
  const profile = {};
86
149
  rows.forEach((row) => {
87
150
  profile[row.key] = row.value;
@@ -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) {
@@ -161,21 +176,25 @@ app.post('/api/fs/list', async (req, res) => {
161
176
  // 获取当前配置
162
177
  app.get('/api/config', async (req, res) => {
163
178
  res.json({
179
+ userId: config.userId,
164
180
  apiKey: config.llm.apiKey,
165
181
  baseURL: config.llm.baseURL,
166
182
  model: config.llm.model,
167
183
  temperature: config.llm.temperature,
168
184
  systemPrompt: config.systemPrompt,
169
185
  memoryMaintenanceCron: config.scheduler?.memoryMaintenanceCron,
186
+ sessionRetentionDays: config.scheduler?.sessionRetentionDays,
170
187
  workspace: config.workspace
171
188
  });
172
189
  });
173
190
  // 更新配置并写入 config.json
174
191
  app.post('/api/config', async (req, res) => {
175
192
  try {
176
- const { apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, workspace } = req.body;
193
+ const { userId, apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, sessionRetentionDays, workspace } = req.body;
177
194
  // 更新配置对象
178
195
  const newConfig = { ...config };
196
+ if (userId !== undefined)
197
+ newConfig.userId = String(userId || 'default').trim() || 'default';
179
198
  if (apiKey !== undefined)
180
199
  newConfig.llm.apiKey = apiKey;
181
200
  if (baseURL !== undefined)
@@ -218,6 +237,15 @@ app.post('/api/config', async (req, res) => {
218
237
  restartScheduler = true;
219
238
  }
220
239
  }
240
+ if (sessionRetentionDays !== undefined) {
241
+ if (!newConfig.scheduler)
242
+ newConfig.scheduler = {};
243
+ const days = Number(sessionRetentionDays);
244
+ if (Number.isFinite(days) && days > 0 && newConfig.scheduler.sessionRetentionDays !== days) {
245
+ newConfig.scheduler.sessionRetentionDays = days;
246
+ restartScheduler = true;
247
+ }
248
+ }
221
249
  // 保存到文件
222
250
  saveConfig(newConfig);
223
251
  // 如果 LLM 配置变了,更新 Brain
@@ -232,6 +260,27 @@ app.post('/api/config', async (req, res) => {
232
260
  res.status(500).json({ status: 'error', message: error.message });
233
261
  }
234
262
  });
263
+ app.get('/api/user/profile', async (req, res) => {
264
+ try {
265
+ const userId = String(req.query.userId || config.userId || 'default');
266
+ const profile = memory.getUserProfile(userId);
267
+ res.json({ userId, profile });
268
+ }
269
+ catch (e) {
270
+ res.status(500).json({ error: String(e?.message || e) });
271
+ }
272
+ });
273
+ app.post('/api/user/profile', async (req, res) => {
274
+ try {
275
+ const userId = String(req.body?.userId || config.userId || 'default');
276
+ const profile = req.body?.profile || {};
277
+ memory.updateUserProfile(userId, profile);
278
+ res.json({ status: 'success' });
279
+ }
280
+ catch (e) {
281
+ res.status(500).json({ error: String(e?.message || e) });
282
+ }
283
+ });
235
284
  // 手动触发记忆维护
236
285
  app.post('/api/scheduler/run', async (req, res) => {
237
286
  try {
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.52",
5
+ "version": "0.1.54",
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",
@@ -44,6 +53,15 @@
44
53
  "modelName": "Model Name",
45
54
  "temperature": "Temperature"
46
55
  },
56
+ "identity": {
57
+ "title": "User identity",
58
+ "userId": "UserId",
59
+ "userIdHint": "Used to load identity profile and memories for this userId.",
60
+ "name": "Name",
61
+ "role": "Role",
62
+ "company": "Company",
63
+ "email": "Email"
64
+ },
47
65
  "prompt": {
48
66
  "description": "Define how xiaozuoAssistant behaves and responds.",
49
67
  "placeholder": "You are a helpful AI assistant..."
@@ -51,6 +69,9 @@
51
69
  "scheduler": {
52
70
  "memoryMaintenance": "Memory Maintenance",
53
71
  "description": "Configure when the system should compress old memories and delete expired sessions.",
72
+ "sessionRetentionDays": "Session retention",
73
+ "sessionRetentionHint": "Sessions inactive for longer than this will be deleted during maintenance.",
74
+ "days": "days",
54
75
  "cronSchedule": "Cron Schedule",
55
76
  "cronHint": "Format: Minute Hour Day Month DayOfWeek (e.g., \"0 0 * * *\" for daily at midnight)",
56
77
  "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": "设置",
@@ -44,6 +53,15 @@
44
53
  "modelName": "模型名称",
45
54
  "temperature": "温度 (Temperature)"
46
55
  },
56
+ "identity": {
57
+ "title": "用户身份",
58
+ "userId": "UserId",
59
+ "userIdHint": "用于加载该 UserId 对应的身份资料与记忆。",
60
+ "name": "姓名",
61
+ "role": "岗位",
62
+ "company": "公司",
63
+ "email": "邮箱"
64
+ },
47
65
  "prompt": {
48
66
  "description": "定义 xiaozuoAssistant 的行为和响应方式。",
49
67
  "placeholder": "你是一个有用的 AI 助手..."
@@ -51,6 +69,9 @@
51
69
  "scheduler": {
52
70
  "memoryMaintenance": "记忆维护",
53
71
  "description": "配置系统何时压缩旧记忆并删除过期会话。",
72
+ "sessionRetentionDays": "会话保留天数",
73
+ "sessionRetentionHint": "超过该天数未活跃的会话会在维护任务中被自动删除。",
74
+ "days": "天",
54
75
  "cronSchedule": "Cron 表达式",
55
76
  "cronHint": "格式: 分 时 日 月 周 (例如 \"0 0 * * *\" 表示每天午夜)",
56
77
  "manualTrigger": "手动触发",