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.
- package/config.json +3 -1
- package/dist/client/assets/{browser-ponyfill-Bz0RS90l.js → browser-ponyfill-Dd_NZzSH.js} +1 -1
- package/dist/client/assets/index-DJdLRlkB.css +1 -0
- package/dist/client/assets/index-DKrBHrNq.js +163 -0
- package/dist/client/index.html +2 -2
- package/dist/client/locales/en/translation.json +22 -1
- package/dist/client/locales/zh/translation.json +22 -1
- package/dist/server/config/loader.js +7 -2
- package/dist/server/core/memories/manager.js +34 -11
- package/dist/server/core/memories/short-term.js +63 -2
- package/dist/server/core/memories/structured.js +73 -10
- package/dist/server/core/scheduler.js +4 -2
- package/dist/server/index.js +52 -3
- package/package.json +1 -1
- package/public/locales/en/translation.json +22 -1
- package/public/locales/zh/translation.json +22 -1
- package/dist/client/assets/index-BwZQ_7Ty.js +0 -158
- package/dist/client/assets/index-C72E_nIr.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
-
|
|
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,
|
|
66
|
-
const
|
|
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
|
|
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(
|
|
84
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
|
|
25
|
+
memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
|
|
24
26
|
};
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
@@ -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": "手动触发",
|