xiaozuoassistant 0.1.50 → 0.1.52

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-BfEuvtsz.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BwZQ_7Ty.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-C72E_nIr.css">
10
10
  </head>
11
11
  <body>
@@ -2,6 +2,12 @@ export class BaseChannel {
2
2
  constructor() {
3
3
  this.messageHandler = null;
4
4
  }
5
+ sendEvent(sessionId, event, payload) {
6
+ const text = event === 'message'
7
+ ? String(payload?.content ?? '')
8
+ : `[${event}] ${typeof payload === 'string' ? payload : JSON.stringify(payload)}`;
9
+ void this.send(sessionId, text);
10
+ }
5
11
  onMessage(handler) {
6
12
  this.messageHandler = handler;
7
13
  }
@@ -2,6 +2,7 @@ export class WebChannel {
2
2
  constructor(io) {
3
3
  this.name = 'web';
4
4
  this.messageHandler = null;
5
+ this.syncHandler = null;
5
6
  this.io = io;
6
7
  }
7
8
  async start() {
@@ -12,6 +13,20 @@ export class WebChannel {
12
13
  socket.join(sessionId);
13
14
  console.log(`Socket ${socket.id} joined session ${sessionId}`);
14
15
  });
16
+ socket.on('sync_session', async (data) => {
17
+ try {
18
+ if (!this.syncHandler)
19
+ return;
20
+ const { sessionId, since } = data || {};
21
+ if (!sessionId)
22
+ return;
23
+ const snapshot = await this.syncHandler(sessionId, since);
24
+ socket.emit('session_sync', { sessionId, ...snapshot });
25
+ }
26
+ catch (e) {
27
+ socket.emit('session_sync', { sessionId: data?.sessionId, messages: [], runs: [], error: String(e?.message || e) });
28
+ }
29
+ });
15
30
  // 当客户端发送消息
16
31
  socket.on('message', (data) => {
17
32
  console.log(`[WebChannel] Received message from ${data.sessionId}: ${data.content}`);
@@ -39,7 +54,13 @@ export class WebChannel {
39
54
  timestamp: Date.now()
40
55
  });
41
56
  }
57
+ sendEvent(sessionId, event, payload) {
58
+ this.io.to(sessionId).emit(event, payload);
59
+ }
42
60
  onMessage(handler) {
43
61
  this.messageHandler = handler;
44
62
  }
63
+ setSyncHandler(handler) {
64
+ this.syncHandler = handler;
65
+ }
45
66
  }
@@ -22,7 +22,8 @@ try {
22
22
  model: 'qwen-plus',
23
23
  temperature: 0.7,
24
24
  requestTimeoutMs: 600000,
25
- maxRetries: 2
25
+ maxRetries: 2,
26
+ maxToolIterations: 200
26
27
  };
27
28
  }
28
29
  else {
@@ -32,6 +33,8 @@ try {
32
33
  loadedConfig.llm.requestTimeoutMs = 600000;
33
34
  if (loadedConfig.llm.maxRetries === undefined)
34
35
  loadedConfig.llm.maxRetries = 2;
36
+ if (loadedConfig.llm.maxToolIterations === undefined)
37
+ loadedConfig.llm.maxToolIterations = 200;
35
38
  }
36
39
  // Override with env vars if present (optional, but good for security)
37
40
  if (process.env.XIAOZUO_LLM_API_KEY)
@@ -51,7 +54,8 @@ catch (error) {
51
54
  model: 'qwen-plus',
52
55
  temperature: 0.7,
53
56
  requestTimeoutMs: 600000,
54
- maxRetries: 2
57
+ maxRetries: 2,
58
+ maxToolIterations: 200
55
59
  },
56
60
  logging: { level: 'info' },
57
61
  scheduler: { memoryMaintenanceCron: '0 0 * * *' },
@@ -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);
@@ -44,6 +44,15 @@ export class MemoryManager {
44
44
  // structured info from the message. For MVP, we skip automatic extraction here
45
45
  // but provide the API for the Brain to call explicitly.
46
46
  }
47
+ async createRun(sessionId, userContent, runId) {
48
+ return this.shortTerm.createRun(sessionId, userContent, runId);
49
+ }
50
+ async updateRun(sessionId, runId, patch) {
51
+ return this.shortTerm.updateRun(sessionId, runId, patch);
52
+ }
53
+ async listRuns(sessionId) {
54
+ return this.shortTerm.listRuns(sessionId);
55
+ }
47
56
  async getHistory(sessionId) {
48
57
  return this.shortTerm.getMessages(sessionId);
49
58
  }
@@ -50,6 +50,9 @@ export class ShortTermMemory {
50
50
  getMessagesFile(sessionId) {
51
51
  return path.join(this.getSessionDir(sessionId), 'messages.json');
52
52
  }
53
+ getRunsFile(sessionId) {
54
+ return path.join(this.getSessionDir(sessionId), 'runs.json');
55
+ }
53
56
  async loadIndex() {
54
57
  try {
55
58
  const data = await fs.readJson(INDEX_FILE);
@@ -185,6 +188,71 @@ export class ShortTermMemory {
185
188
  }
186
189
  return { meta, messages };
187
190
  }
191
+ async createRun(sessionId, userContent, runId) {
192
+ await this.ensureReady();
193
+ const meta = await this.getSessionMeta(sessionId);
194
+ if (!meta)
195
+ throw new Error('Session not found');
196
+ const now = Date.now();
197
+ const id = runId || uuidv4();
198
+ const run = {
199
+ id,
200
+ sessionId,
201
+ status: 'queued',
202
+ userContent,
203
+ createdAt: now,
204
+ updatedAt: now
205
+ };
206
+ const file = this.getRunsFile(sessionId);
207
+ let runs = [];
208
+ try {
209
+ const existing = await fs.readJson(file);
210
+ runs = Array.isArray(existing) ? existing : [];
211
+ }
212
+ catch {
213
+ await fs.ensureDir(this.getSessionDir(sessionId));
214
+ }
215
+ runs.push(run);
216
+ await fs.writeJson(file, runs, { spaces: 2 });
217
+ return run;
218
+ }
219
+ async updateRun(sessionId, runId, patch) {
220
+ await this.ensureReady();
221
+ const file = this.getRunsFile(sessionId);
222
+ let runs = [];
223
+ try {
224
+ const existing = await fs.readJson(file);
225
+ runs = Array.isArray(existing) ? existing : [];
226
+ }
227
+ catch {
228
+ runs = [];
229
+ }
230
+ const idx = runs.findIndex(r => r.id === runId);
231
+ if (idx < 0)
232
+ throw new Error('Run not found');
233
+ const next = {
234
+ ...runs[idx],
235
+ ...patch,
236
+ status: patch.status || runs[idx].status,
237
+ updatedAt: patch.updatedAt ?? Date.now()
238
+ };
239
+ runs[idx] = next;
240
+ await fs.writeJson(file, runs, { spaces: 2 });
241
+ return next;
242
+ }
243
+ async listRuns(sessionId) {
244
+ await this.ensureReady();
245
+ const file = this.getRunsFile(sessionId);
246
+ try {
247
+ const existing = await fs.readJson(file);
248
+ const runs = Array.isArray(existing) ? existing : [];
249
+ runs.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
250
+ return runs;
251
+ }
252
+ catch {
253
+ return [];
254
+ }
255
+ }
188
256
  async getMessages(sessionId) {
189
257
  const session = await this.getSession(sessionId);
190
258
  return session ? session.messages : [];
@@ -0,0 +1,104 @@
1
+ import { memory } from './memory.js';
2
+ import { brain } from './brain.js';
3
+ import { eventBus } from './event-bus.js';
4
+ export class TaskQueue {
5
+ constructor() {
6
+ this.chains = new Map();
7
+ }
8
+ enqueue(task) {
9
+ const key = task.run.sessionId;
10
+ const prev = this.chains.get(key) || Promise.resolve();
11
+ const next = prev
12
+ .catch(() => { })
13
+ .then(() => this.process(task))
14
+ .finally(() => {
15
+ if (this.chains.get(key) === next)
16
+ this.chains.delete(key);
17
+ });
18
+ this.chains.set(key, next);
19
+ }
20
+ async process(task) {
21
+ const { run, channel } = task;
22
+ try {
23
+ const session = await memory.getSession(run.sessionId);
24
+ if (!session) {
25
+ await memory.updateRun(run.sessionId, run.id, { status: 'failed', error: 'Session not found', completedAt: Date.now() });
26
+ eventBus.emitEvent({
27
+ type: 'run_status',
28
+ payload: { runId: run.id, status: 'failed', error: 'Session not found' },
29
+ channel,
30
+ sessionId: run.sessionId,
31
+ timestamp: Date.now()
32
+ });
33
+ return;
34
+ }
35
+ await memory.updateRun(run.sessionId, run.id, { status: 'running', updatedAt: Date.now() });
36
+ eventBus.emitEvent({
37
+ type: 'run_status',
38
+ payload: { runId: run.id, status: 'running' },
39
+ channel,
40
+ sessionId: run.sessionId,
41
+ timestamp: Date.now()
42
+ });
43
+ const context = await memory.getRelevantContext(run.userContent, run.sessionId);
44
+ const sessionAlias = session.meta.alias ? `Session Alias: ${session.meta.alias}` : '';
45
+ const workspaceLine = session.meta.workspace ? `Current Workspace: ${session.meta.workspace}` : '';
46
+ const enhancedSystemPrompt = `You are xiaozuoAssistant, a helpful AI assistant.
47
+ ${sessionAlias}
48
+ ${workspaceLine}
49
+ Here is some relevant context from your memory:
50
+ ${context}`;
51
+ const ctx = {
52
+ sessionId: run.sessionId,
53
+ history: session.messages,
54
+ channel,
55
+ session: session.meta,
56
+ metadata: {
57
+ workspace: session.meta.workspace,
58
+ alias: session.meta.alias,
59
+ runId: run.id
60
+ }
61
+ };
62
+ const responseContent = await brain.processMessage(session.messages, run.userContent, enhancedSystemPrompt, ctx);
63
+ await memory.addMessage(run.sessionId, { role: 'assistant', content: responseContent, timestamp: Date.now() });
64
+ await memory.updateRun(run.sessionId, run.id, { status: 'completed', completedAt: Date.now() });
65
+ eventBus.emitEvent({
66
+ type: 'run_status',
67
+ payload: { runId: run.id, status: 'completed' },
68
+ channel,
69
+ sessionId: run.sessionId,
70
+ timestamp: Date.now()
71
+ });
72
+ eventBus.emitEvent({
73
+ type: 'response',
74
+ payload: { content: responseContent, runId: run.id },
75
+ channel,
76
+ sessionId: run.sessionId,
77
+ timestamp: Date.now()
78
+ });
79
+ }
80
+ catch (e) {
81
+ const msg = String(e?.message || e);
82
+ try {
83
+ await memory.updateRun(run.sessionId, run.id, { status: 'failed', error: msg, completedAt: Date.now() });
84
+ }
85
+ catch {
86
+ // ignore
87
+ }
88
+ try {
89
+ await memory.addMessage(run.sessionId, { role: 'assistant', content: `Error: ${msg}`, timestamp: Date.now() });
90
+ }
91
+ catch {
92
+ // ignore
93
+ }
94
+ eventBus.emitEvent({
95
+ type: 'run_status',
96
+ payload: { runId: run.id, status: 'failed', error: msg },
97
+ channel,
98
+ sessionId: run.sessionId,
99
+ timestamp: Date.now()
100
+ });
101
+ }
102
+ }
103
+ }
104
+ export const taskQueue = new TaskQueue();
@@ -21,6 +21,8 @@ import { createOfficeAgent } from './agents/office.js';
21
21
  import { DelegateSkill } from './skills/delegate.js';
22
22
  import { createChannels } from './channels/create-channels.js';
23
23
  import { createHttpStack } from './server/create-http.js';
24
+ import { taskQueue } from './core/task-queue.js';
25
+ import { WebChannel } from './channels/web.js';
24
26
  // 解决 ES Module 中没有 __dirname 的问题
25
27
  const __filename = fileURLToPath(import.meta.url);
26
28
  const __dirname = path.dirname(__filename);
@@ -267,6 +269,19 @@ const pluginManager = new PluginManager(path.resolve(process.cwd(), 'plugins'));
267
269
  await pluginManager.loadPlugins();
268
270
  // 初始化通道
269
271
  const channels = createChannels({ app, io, config, pluginManager });
272
+ for (const ch of channels) {
273
+ if (ch instanceof WebChannel) {
274
+ ch.setSyncHandler(async (sessionId, since) => {
275
+ const session = await memory.getSession(sessionId);
276
+ if (!session)
277
+ return { messages: [], runs: [] };
278
+ const runs = await memory.listRuns(sessionId);
279
+ const sinceTs = typeof since === 'number' ? since : 0;
280
+ const messages = (session.messages || []).filter((m) => (m.timestamp ?? 0) > sinceTs);
281
+ return { messages, runs };
282
+ });
283
+ }
284
+ }
270
285
  // 启动通道并绑定事件
271
286
  channels.forEach(channel => {
272
287
  channel.start();
@@ -286,55 +301,27 @@ eventBus.onEvent('message', async (event) => {
286
301
  const { sessionId, payload, channel: channelName } = event;
287
302
  const content = payload.content;
288
303
  try {
289
- // 1. 获取会话上下文
290
304
  const session = await memory.getSession(sessionId);
291
305
  if (!session) {
292
306
  throw new Error('Session not found. Please create or select a session first.');
293
307
  }
294
- // 2. 记录用户消息
295
308
  await memory.addMessage(sessionId, { role: 'user', content, timestamp: Date.now() });
296
- // 3. Brain 处理
297
- // Use MemoryManager to retrieve full context (Short-term + Recent + Structured)
298
- const context = await memory.getRelevantContext(content, sessionId);
299
- // We pass the raw messages to processMessage for now, but Brain should ideally use the enhanced context.
300
- // Let's modify Brain.processMessage to accept an optional context string or just prepend it.
301
- // For this MVP, we will prepend the context to the system prompt or as a new system message.
302
- // Or simpler: We just pass session.messages as before, but Brain needs to know about the context.
303
- // Let's change how we call brain.processMessage.
304
- // Construct a temporary history with enhanced context for the LLM
305
- const sessionAlias = session.meta.alias ? `Session Alias: ${session.meta.alias}` : '';
306
- const workspaceLine = session.meta.workspace ? `Current Workspace: ${session.meta.workspace}` : '';
307
- const enhancedSystemPrompt = `You are xiaozuoAssistant, a helpful AI assistant.
308
- ${sessionAlias}
309
- ${workspaceLine}
310
- Here is some relevant context from your memory:
311
- ${context}`;
312
- // We need to inject this into the Brain.
313
- // Since Brain.processMessage takes history, let's just pass the history but we need to tell Brain to use this system prompt.
314
- // We can modify Brain to accept a system prompt override.
315
- // Or we can just create a new method in Brain or pass it as an argument.
316
- // Let's update Brain.processMessage signature in next step. For now, let's just pass the history.
317
- const ctx = {
318
- sessionId,
319
- history: session.messages,
309
+ const run = await memory.createRun(sessionId, content);
310
+ eventBus.emitEvent({
311
+ type: 'run_ack',
312
+ payload: { runId: run.id },
320
313
  channel: channelName,
321
- session: session.meta,
322
- metadata: {
323
- workspace: session.meta.workspace,
324
- alias: session.meta.alias
325
- }
326
- };
327
- const responseContent = await brain.processMessage(session.messages, content, enhancedSystemPrompt, ctx);
328
- // 4. 记录 AI 响应
329
- await memory.addMessage(sessionId, { role: 'assistant', content: responseContent, timestamp: Date.now() });
330
- // 5. 发送响应事件
314
+ sessionId,
315
+ timestamp: Date.now()
316
+ });
331
317
  eventBus.emitEvent({
332
- type: 'response',
333
- payload: { content: responseContent },
318
+ type: 'run_status',
319
+ payload: { runId: run.id, status: 'queued' },
334
320
  channel: channelName,
335
- sessionId: sessionId,
321
+ sessionId,
336
322
  timestamp: Date.now()
337
323
  });
324
+ taskQueue.enqueue({ run, channel: channelName });
338
325
  }
339
326
  catch (error) {
340
327
  console.error('Error processing message:', error);
@@ -361,6 +348,20 @@ eventBus.onEvent('response', (event) => {
361
348
  channel.send(sessionId, payload.content);
362
349
  }
363
350
  });
351
+ eventBus.onEvent('run_ack', (event) => {
352
+ const { sessionId, payload, channel: channelName } = event;
353
+ const channel = channels.find(c => c.name === channelName);
354
+ if (channel?.sendEvent) {
355
+ channel.sendEvent(sessionId, 'run_ack', payload);
356
+ }
357
+ });
358
+ eventBus.onEvent('run_status', (event) => {
359
+ const { sessionId, payload, channel: channelName } = event;
360
+ const channel = channels.find(c => c.name === channelName);
361
+ if (channel?.sendEvent) {
362
+ channel.sendEvent(sessionId, 'run_status', payload);
363
+ }
364
+ });
364
365
  eventBus.onEvent('error', (event) => {
365
366
  const { sessionId, payload, channel: channelName } = event;
366
367
  const channel = channels.find(c => c.name === channelName);
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.50",
5
+ "version": "0.1.52",
6
6
  "author": "mantle.lau",
7
7
  "license": "MIT",
8
8
  "repository": {