xiaozuoassistant 0.1.77 → 0.1.79
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/README.md +10 -1
- package/bin/cli.js +91 -52
- package/dist/server/config/loader.js +2 -1
- package/dist/server/core/brain.js +20 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,13 @@ xiaozuoAssistant 是一个**本地优先(Local-first)**的个人 AI 助手
|
|
|
16
16
|
- **项目空间**:管理项目元数据(描述、成员、目录等),为 AI 提供项目上下文(支持多项目关联)。
|
|
17
17
|
- **智能笔记本**:记录笔记和待办事项(Todo),支持关键字自动归档。
|
|
18
18
|
- **更易接入新模型**:统一 LLM(OpenAI 兼容) client 工厂与 provider→baseURL 解析,便于新增 provider。
|
|
19
|
-
- **CLI 管理**:支持 `start/stop/doctor/export/import
|
|
19
|
+
- **CLI 管理**:支持 `start/stop/doctor/export/import`,完美支持 Windows/macOS/Linux 跨平台运行。
|
|
20
|
+
|
|
21
|
+
## 系统要求
|
|
22
|
+
|
|
23
|
+
- **操作系统**: macOS, Linux, Windows (WSL 或 PowerShell/CMD)
|
|
24
|
+
- **Node.js**: v18.0.0 或更高版本
|
|
25
|
+
- **Python**: v3.0+ (某些依赖可能需要)
|
|
20
26
|
|
|
21
27
|
## 快速开始(推荐:npm 全局安装)
|
|
22
28
|
|
|
@@ -86,6 +92,9 @@ xiaozuoAssistant start
|
|
|
86
92
|
|
|
87
93
|
- 你可以通过 `config.json` 或 API (`/api/config`) 更新 LLM 配置。
|
|
88
94
|
- 支持 OpenAI 兼容的多 provider(示例:`openai/deepseek/minimax/doubao/qwen/custom`),并会自动映射 baseURL。
|
|
95
|
+
- **配置项增强**:
|
|
96
|
+
- `maxHistoryMessages`: 控制发送给 LLM 的历史消息数量(默认 20),防止 Context Window 溢出。
|
|
97
|
+
- `maxToolIterations`: 限制工具调用的最大迭代次数(默认 15),防止死循环。
|
|
89
98
|
- 可用环境变量:`XIAOZUO_LLM_API_KEY`(覆盖 `config.json` 中的 key)。
|
|
90
99
|
|
|
91
100
|
## 文档
|
package/bin/cli.js
CHANGED
|
@@ -192,69 +192,103 @@ async function isPortOpen(port) {
|
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
async function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const tryKill = (targetPid, signal) => {
|
|
203
|
-
try {
|
|
204
|
-
process.kill(targetPid, signal);
|
|
205
|
-
return true;
|
|
206
|
-
} catch (e) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const killedGroup = tryKill(-pid, 'SIGTERM');
|
|
212
|
-
if (!killedGroup) {
|
|
213
|
-
tryKill(pid, 'SIGTERM');
|
|
195
|
+
async function killProcessTree(pid) {
|
|
196
|
+
if (process.platform === 'win32') {
|
|
197
|
+
try {
|
|
198
|
+
await runCommand('taskkill', ['/pid', pid.toString(), '/T', '/F'], { stdio: 'ignore' });
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
214
202
|
}
|
|
203
|
+
}
|
|
215
204
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
205
|
+
// Unix-like implementation
|
|
206
|
+
const tryKill = (targetPid, signal) => {
|
|
207
|
+
try {
|
|
208
|
+
process.kill(targetPid, signal);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return false;
|
|
219
212
|
}
|
|
213
|
+
};
|
|
220
214
|
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
tryKill(-pid, 'SIGTERM');
|
|
216
|
+
tryKill(pid, 'SIGTERM');
|
|
217
|
+
|
|
218
|
+
// Wait a bit
|
|
219
|
+
await new Promise(r => setTimeout(r, 500));
|
|
220
|
+
|
|
221
|
+
if (isProcessRunning(pid)) {
|
|
222
|
+
tryKill(-pid, 'SIGKILL');
|
|
223
223
|
tryKill(pid, 'SIGKILL');
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
await waitGone(300);
|
|
229
|
-
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return !isProcessRunning(pid);
|
|
227
|
+
}
|
|
230
228
|
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
async function stopServer() {
|
|
230
|
+
const pidFile = getPidFilePath();
|
|
231
|
+
const port = getPortFromConfig();
|
|
233
232
|
|
|
234
233
|
const killByPort = async () => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
234
|
+
if (process.platform === 'win32') {
|
|
235
|
+
// Windows implementation using netstat
|
|
236
|
+
try {
|
|
237
|
+
const netstat = spawn('netstat', ['-ano'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
238
|
+
let output = '';
|
|
239
|
+
netstat.stdout.on('data', d => output += d.toString());
|
|
240
|
+
await new Promise(r => netstat.on('close', r));
|
|
241
|
+
|
|
242
|
+
const lines = output.split('\n');
|
|
243
|
+
const pids = new Set();
|
|
244
|
+
for (const line of lines) {
|
|
245
|
+
if (line.includes(`:${port}`)) {
|
|
246
|
+
const parts = line.trim().split(/\s+/);
|
|
247
|
+
const pid = parts[parts.length - 1];
|
|
248
|
+
if (pid && !isNaN(Number(pid)) && Number(pid) > 0) {
|
|
249
|
+
pids.add(Number(pid));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pids.size === 0) return false;
|
|
255
|
+
|
|
256
|
+
for (const pid of pids) {
|
|
257
|
+
await killProcessTree(pid);
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error('[CLI] Windows port kill failed:', e);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
// Unix implementation using lsof
|
|
266
|
+
const lsof = spawn('lsof', ['-ti', `tcp:${port}`], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
267
|
+
let output = '';
|
|
268
|
+
lsof.stdout.on('data', (d) => { output += d.toString(); });
|
|
269
|
+
await new Promise((resolve) => lsof.on('close', resolve));
|
|
270
|
+
|
|
271
|
+
const pids = output
|
|
272
|
+
.split(/\s+/)
|
|
273
|
+
.map(s => s.trim())
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.map(Number)
|
|
276
|
+
.filter(n => Number.isFinite(n) && n > 0);
|
|
277
|
+
|
|
278
|
+
if (pids.length === 0) return false;
|
|
279
|
+
|
|
280
|
+
for (const pid of pids) {
|
|
281
|
+
await killProcessTree(pid);
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
251
284
|
}
|
|
252
|
-
return true;
|
|
253
285
|
};
|
|
254
286
|
|
|
255
287
|
if (fs.existsSync(pidFile)) {
|
|
256
288
|
const pidStr = fs.readFileSync(pidFile, 'utf-8').trim();
|
|
257
289
|
const pid = Number(pidStr);
|
|
290
|
+
|
|
291
|
+
// ... existing pid checks ...
|
|
258
292
|
if (!Number.isFinite(pid) || pid <= 0) {
|
|
259
293
|
fs.unlinkSync(pidFile);
|
|
260
294
|
console.log('[CLI] 未找到可用的 PID(已清理 pid 文件)。');
|
|
@@ -268,7 +302,7 @@ async function stopServer() {
|
|
|
268
302
|
}
|
|
269
303
|
|
|
270
304
|
console.log(`[CLI] 正在停止服务(PID: ${pid})...`);
|
|
271
|
-
await
|
|
305
|
+
await killProcessTree(pid);
|
|
272
306
|
await killByPort();
|
|
273
307
|
try { fs.unlinkSync(pidFile); } catch (e) {}
|
|
274
308
|
|
|
@@ -276,9 +310,14 @@ async function stopServer() {
|
|
|
276
310
|
console.log('[CLI] ✅ 服务已停止。');
|
|
277
311
|
return;
|
|
278
312
|
}
|
|
279
|
-
|
|
313
|
+
|
|
314
|
+
// Fallback message
|
|
280
315
|
console.error('[CLI] ❌ 停止失败:端口仍在占用。');
|
|
281
|
-
|
|
316
|
+
if (process.platform !== 'win32') {
|
|
317
|
+
console.error(`[CLI] 你可以手动执行:lsof -ti tcp:${port} | xargs kill -9`);
|
|
318
|
+
} else {
|
|
319
|
+
console.error(`[CLI] 你可以手动执行:netstat -ano | findstr :${port} 并 kill 对应 PID`);
|
|
320
|
+
}
|
|
282
321
|
return;
|
|
283
322
|
}
|
|
284
323
|
|
|
@@ -64,7 +64,8 @@ catch (error) {
|
|
|
64
64
|
requestTimeoutMs: 600000,
|
|
65
65
|
maxRetries: 2,
|
|
66
66
|
maxToolIterations: 200,
|
|
67
|
-
maxHistoryMessages: 20
|
|
67
|
+
maxHistoryMessages: 20,
|
|
68
|
+
sessionWakeupHours: 24
|
|
68
69
|
},
|
|
69
70
|
logging: { level: 'info' },
|
|
70
71
|
scheduler: { memoryMaintenanceCron: '0 0 * * *', sessionRetentionDays: 5 },
|
|
@@ -20,6 +20,25 @@ export class Brain {
|
|
|
20
20
|
if (process.env.DEBUG)
|
|
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
|
+
// Check for long inactivity (Session Wake-up)
|
|
24
|
+
const sessionLastActive = context?.session?.lastActiveAt || 0;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const hoursSinceActive = (now - sessionLastActive) / (1000 * 60 * 60);
|
|
27
|
+
const WAKEUP_THRESHOLD = config.llm.sessionWakeupHours ?? 24;
|
|
28
|
+
let wakeupContext = '';
|
|
29
|
+
if (sessionLastActive > 0 && hoursSinceActive > WAKEUP_THRESHOLD) {
|
|
30
|
+
if (process.env.DEBUG)
|
|
31
|
+
console.log(`[Brain] Session wake-up detected (${hoursSinceActive.toFixed(1)}h > ${WAKEUP_THRESHOLD}h). Generating recap...`);
|
|
32
|
+
// Use recent history for quick summary, or could query Vector DB for deeper context
|
|
33
|
+
// For now, simple summary of recent messages to refresh context
|
|
34
|
+
const recentText = history.slice(-50).map(m => `${m.role}: ${m.content}`).join('\n');
|
|
35
|
+
if (recentText.length > 100) {
|
|
36
|
+
const summary = await this.generateSummary(recentText);
|
|
37
|
+
if (summary) {
|
|
38
|
+
wakeupContext = `\n\n[System Notice]: The user has returned after ${Math.floor(hoursSinceActive)} hours. Here is a brief recap of the previous conversation context to help you catch up:\n${summary}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
23
42
|
// Convert history messages to the format expected by OpenAI
|
|
24
43
|
// Strategy: Keep last N messages to avoid context window overflow
|
|
25
44
|
// TODO: A better strategy would be token-based truncation.
|
|
@@ -36,7 +55,7 @@ export class Brain {
|
|
|
36
55
|
return msg;
|
|
37
56
|
});
|
|
38
57
|
const messages = [
|
|
39
|
-
{ role: 'system', content: defaultSystemPrompt },
|
|
58
|
+
{ role: 'system', content: defaultSystemPrompt + wakeupContext },
|
|
40
59
|
...messageHistory,
|
|
41
60
|
{ role: 'user', content: newMessage }
|
|
42
61
|
];
|
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.
|
|
5
|
+
"version": "0.1.79",
|
|
6
6
|
"author": "mantle.lau",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"check": "tsc --noEmit",
|
|
40
40
|
"server:dev": "nodemon --watch src --watch config.json --exec tsx src/index.ts",
|
|
41
41
|
"dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"",
|
|
42
|
-
"postinstall": "node scripts/init-app-home.cjs || true
|
|
42
|
+
"postinstall": "node scripts/init-app-home.cjs || true"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@lancedb/lancedb": "0.22.3",
|