xiaozuoassistant 0.2.19 → 0.2.21

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.
Files changed (61) hide show
  1. package/README.md +119 -119
  2. package/bin/cli.js +896 -896
  3. package/config.json +41 -41
  4. package/dist/client/assets/browser-ponyfill-Bcpejndl.js +2 -0
  5. package/dist/client/assets/index-BfvHy-SS.js +201 -0
  6. package/dist/client/assets/index-u0lXmgyZ.css +1 -0
  7. package/dist/client/favicon.svg +4 -0
  8. package/dist/client/index.html +14 -0
  9. package/dist/client/locales/en/translation.json +110 -0
  10. package/dist/client/locales/zh/translation.json +112 -0
  11. package/dist/server/agents/office.js +23 -0
  12. package/dist/server/app.js +50 -0
  13. package/dist/server/channels/base-channel.js +23 -0
  14. package/dist/server/channels/create-channels.js +18 -0
  15. package/dist/server/channels/dingtalk.js +83 -0
  16. package/dist/server/channels/feishu.js +139 -0
  17. package/dist/server/channels/telegram.js +53 -0
  18. package/dist/server/channels/terminal.js +49 -0
  19. package/dist/server/channels/web.js +66 -0
  20. package/dist/server/channels/wechat.js +107 -0
  21. package/dist/server/config/loader.js +111 -0
  22. package/dist/server/config/paths.js +24 -0
  23. package/dist/server/config/prompts.js +12 -0
  24. package/dist/server/core/agents/manager.js +27 -0
  25. package/dist/server/core/agents/runtime.js +92 -0
  26. package/dist/server/core/brain.js +255 -0
  27. package/dist/server/core/event-bus.js +24 -0
  28. package/dist/server/core/logger.js +71 -0
  29. package/dist/server/core/memories/manager.js +238 -0
  30. package/dist/server/core/memories/short-term.js +512 -0
  31. package/dist/server/core/memories/structured.js +357 -0
  32. package/dist/server/core/memories/vector.js +137 -0
  33. package/dist/server/core/memory.js +2 -0
  34. package/dist/server/core/plugin-manager.js +128 -0
  35. package/dist/server/core/plugin.js +1 -0
  36. package/dist/server/core/scheduler.js +85 -0
  37. package/dist/server/core/task-queue.js +104 -0
  38. package/dist/server/core/types.js +1 -0
  39. package/dist/server/index.js +878 -0
  40. package/dist/server/llm/openai.js +23 -0
  41. package/dist/server/plugins/core-skills/src/create-agent.js +58 -0
  42. package/dist/server/plugins/core-skills/src/delegate.js +39 -0
  43. package/dist/server/plugins/core-skills/src/file-system.js +142 -0
  44. package/dist/server/plugins/core-skills/src/index.js +26 -0
  45. package/dist/server/plugins/core-skills/src/list-agents.js +24 -0
  46. package/dist/server/plugins/core-skills/src/search.js +31 -0
  47. package/dist/server/plugins/core-skills/src/system-time.js +27 -0
  48. package/dist/server/plugins/office-skills/src/index.js +19 -0
  49. package/dist/server/plugins/office-skills/src/office-excel.js +84 -0
  50. package/dist/server/plugins/office-skills/src/office-ppt.js +58 -0
  51. package/dist/server/plugins/office-skills/src/office-word.js +90 -0
  52. package/dist/server/routes/auth.js +28 -0
  53. package/dist/server/server/create-http.js +22 -0
  54. package/dist/server/server.js +29 -0
  55. package/dist/server/skills/base-skill.js +20 -0
  56. package/dist/server/skills/registry.js +52 -0
  57. package/package.json +116 -116
  58. package/public/favicon.svg +4 -4
  59. package/public/locales/en/translation.json +110 -110
  60. package/public/locales/zh/translation.json +112 -112
  61. package/scripts/init-app-home.cjs +43 -43
@@ -0,0 +1,878 @@
1
+ import { overrideConsole } from './core/logger.js'; // Import logger override
2
+ import express from 'express';
3
+ // Initialize logger override immediately to capture all logs
4
+ overrideConsole();
5
+ import { config, saveConfig } from './config/loader.js';
6
+ import { eventBus } from './core/event-bus.js';
7
+ import { brain } from './core/brain.js';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import fs from 'fs'; // Ensure fs is imported for sync operations like existsSync
11
+ import fsPromises from 'fs/promises'; // For async file operations if needed
12
+ import crypto from 'crypto';
13
+ import axios from 'axios';
14
+ import { memory } from './core/memory.js';
15
+ import { PluginManager } from './core/plugin-manager.js';
16
+ import { initScheduler, runMaintenanceNow, getSchedulerStatus, startScheduler, stopScheduler } from './core/scheduler.js';
17
+ import { agentManager } from './core/agents/manager.js';
18
+ import { createOfficeAgent } from './agents/office.js';
19
+ import { createChannels } from './channels/create-channels.js';
20
+ import { createHttpStack } from './server/create-http.js';
21
+ import { taskQueue } from './core/task-queue.js';
22
+ import { WebChannel } from './channels/web.js';
23
+ // 解决 ES Module 中没有 __dirname 的问题
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ const { app, httpServer, io } = createHttpStack();
27
+ // Request logging middleware
28
+ app.use((req, res, next) => {
29
+ console.log(`[HTTP] ${req.method} ${req.url}`);
30
+ next();
31
+ });
32
+ // 静态文件服务
33
+ // 在生产环境中(dist/server/index.js),客户端构建文件通常位于 ../../dist/client 或 ../client
34
+ // 我们假设构建结构如下:
35
+ // dist/
36
+ // server/
37
+ // index.js
38
+ // client/
39
+ // index.html
40
+ // assets/
41
+ //
42
+ // 这样在 server/index.js 中,客户端路径应该是 path.join(__dirname, '../client')
43
+ const clientBuildPath = path.join(__dirname, '../client');
44
+ console.log(`[Server] Serving static files from: ${clientBuildPath}`);
45
+ // 或者在开发模式下,可能是不同的路径,但开发模式通常用 Vite 代理,所以这里主要关注生产环境。
46
+ app.use(express.static(clientBuildPath));
47
+ // API 路由
48
+ // ... existing routes ...
49
+ // 前端路由回退处理 (SPA 支持)
50
+ // 必须放在所有 API 路由之后
51
+ app.get('*', (req, res, next) => {
52
+ // 如果是 API 请求,跳过
53
+ if (req.path.startsWith('/api')) {
54
+ return next();
55
+ }
56
+ const indexPath = path.join(clientBuildPath, 'index.html');
57
+ if (fs.existsSync(indexPath)) { // 这里需要确保 fs 已导入,或者用 fs.access
58
+ res.sendFile(indexPath);
59
+ }
60
+ else {
61
+ // 如果找不到 index.html(例如开发模式只启动了后端),则返回简单的提示
62
+ res.send('xiaozuoAssistant backend is running. Frontend static files not found in ' + clientBuildPath);
63
+ }
64
+ });
65
+ // ... rest of the file ...
66
+ // API 路由
67
+ app.get('/api/health', (req, res) => {
68
+ res.json({ status: 'ok', message: 'xiaozuoAssistant backend is running' });
69
+ });
70
+ // 获取所有会话
71
+ app.get('/api/sessions', async (req, res) => {
72
+ try {
73
+ const sessions = await memory.listSessions();
74
+ res.json(sessions);
75
+ }
76
+ catch (e) {
77
+ res.status(500).json({ error: e.message });
78
+ }
79
+ });
80
+ // 获取会话详情
81
+ app.get('/api/sessions/:id', async (req, res) => {
82
+ try {
83
+ const session = await memory.getSession(req.params.id);
84
+ if (!session) {
85
+ res.status(404).json({ error: 'Session not found' });
86
+ return;
87
+ }
88
+ res.json(session);
89
+ }
90
+ catch (e) {
91
+ res.status(500).json({ error: e.message });
92
+ }
93
+ });
94
+ // 创建新会话
95
+ app.post('/api/sessions', async (req, res) => {
96
+ try {
97
+ const { alias, workspace } = req.body || {};
98
+ const session = await memory.createSession({ alias, workspace });
99
+ res.json(session.meta);
100
+ }
101
+ catch (e) {
102
+ res.status(500).json({ error: e.message });
103
+ }
104
+ });
105
+ // 追加一条消息到会话(用于欢迎语/系统消息等)
106
+ app.post('/api/sessions/:id/messages', async (req, res) => {
107
+ try {
108
+ const role = String(req.body?.role || '').trim();
109
+ const content = String(req.body?.content || '');
110
+ const timestamp = req.body?.timestamp;
111
+ const name = req.body?.name;
112
+ if (!['system', 'user', 'assistant', 'tool'].includes(role)) {
113
+ res.status(400).json({ error: 'Invalid role' });
114
+ return;
115
+ }
116
+ if (!content.trim()) {
117
+ res.status(400).json({ error: 'Content cannot be empty' });
118
+ return;
119
+ }
120
+ await memory.addMessage(req.params.id, {
121
+ role: role,
122
+ content,
123
+ ...(timestamp ? { timestamp: Number(timestamp) } : {}),
124
+ ...(name ? { name: String(name) } : {})
125
+ });
126
+ res.json({ status: 'success' });
127
+ }
128
+ catch (e) {
129
+ const msg = String(e?.message || 'Unknown error');
130
+ if (msg.toLowerCase().includes('not found')) {
131
+ res.status(404).json({ error: 'Session not found' });
132
+ return;
133
+ }
134
+ res.status(500).json({ error: msg });
135
+ }
136
+ });
137
+ // 更新会话元信息
138
+ app.patch('/api/sessions/:id', async (req, res) => {
139
+ try {
140
+ const { alias, workspace, context, projectId, projectIds, notebookId } = req.body || {};
141
+ const meta = await memory.updateSessionMeta(req.params.id, {
142
+ alias,
143
+ workspace,
144
+ context,
145
+ projectId: projectId || null,
146
+ projectIds: projectIds || null,
147
+ notebookId: notebookId || null
148
+ });
149
+ res.json(meta);
150
+ }
151
+ catch (e) {
152
+ const msg = String(e?.message || 'Unknown error');
153
+ if (msg.toLowerCase().includes('not found')) {
154
+ res.status(404).json({ error: 'Session not found' });
155
+ return;
156
+ }
157
+ res.status(500).json({ error: msg });
158
+ }
159
+ });
160
+ // 手动持久化会话(刷新 updatedAt,并确保会话数据文件存在)
161
+ app.post('/api/sessions/:id/persist', async (req, res) => {
162
+ try {
163
+ const meta = await memory.persistSession(req.params.id);
164
+ res.json(meta);
165
+ }
166
+ catch (e) {
167
+ const msg = String(e?.message || 'Unknown error');
168
+ if (msg.toLowerCase().includes('not found')) {
169
+ res.status(404).json({ error: 'Session not found' });
170
+ return;
171
+ }
172
+ res.status(500).json({ error: msg });
173
+ }
174
+ });
175
+ // 重置会话消息
176
+ app.post('/api/sessions/:id/reset', async (req, res) => {
177
+ try {
178
+ await memory.clearSessionMessages(req.params.id);
179
+ res.json({ status: 'success' });
180
+ }
181
+ catch (e) {
182
+ const msg = String(e?.message || 'Unknown error');
183
+ if (msg.toLowerCase().includes('not found')) {
184
+ res.status(404).json({ error: 'Session not found' });
185
+ return;
186
+ }
187
+ res.status(500).json({ error: msg });
188
+ }
189
+ });
190
+ // 删除会话
191
+ app.delete('/api/sessions/:id', async (req, res) => {
192
+ try {
193
+ await memory.deleteSession(req.params.id);
194
+ res.json({ status: 'success', message: 'Session deleted' });
195
+ }
196
+ catch (e) {
197
+ const msg = String(e?.message || 'Unknown error');
198
+ if (msg.toLowerCase().includes('not found')) {
199
+ res.status(404).json({ error: 'Session not found' });
200
+ return;
201
+ }
202
+ res.status(500).json({ error: msg });
203
+ }
204
+ });
205
+ // 文件选择接口 (使用系统默认文件对话框可能需要Electron,这里作为Web服务,我们提供目录列表供选择)
206
+ // 简单的目录列举接口
207
+ app.post('/api/fs/list', async (req, res) => {
208
+ try {
209
+ const dirPath = req.body.path || process.cwd();
210
+ // Use fsPromises for async operations which returns proper types for withFileTypes
211
+ const items = await fsPromises.readdir(dirPath, { withFileTypes: true });
212
+ const result = items.map(item => ({
213
+ name: item.name,
214
+ isDirectory: item.isDirectory(),
215
+ path: path.join(dirPath, item.name)
216
+ })).filter(item => item.isDirectory && !item.name.startsWith('.')); // 仅列出目录,忽略隐藏目录
217
+ res.json({
218
+ current: dirPath,
219
+ parent: path.dirname(dirPath),
220
+ items: result
221
+ });
222
+ }
223
+ catch (e) {
224
+ res.status(500).json({ error: e.message });
225
+ }
226
+ });
227
+ // 获取当前配置
228
+ app.get('/api/config', async (req, res) => {
229
+ res.json({
230
+ userId: config.userId,
231
+ apiKey: config.llm.apiKey,
232
+ baseURL: config.llm.baseURL,
233
+ model: config.llm.model,
234
+ embeddingModel: config.llm.embeddingModel,
235
+ temperature: config.llm.temperature,
236
+ maxHistoryMessages: config.llm.maxHistoryMessages,
237
+ maxToolIterations: config.llm.maxToolIterations,
238
+ systemPrompt: config.systemPrompt,
239
+ memoryMaintenanceCron: config.scheduler?.memoryMaintenanceCron,
240
+ sessionRetentionDays: config.scheduler?.sessionRetentionDays,
241
+ workspace: config.workspace,
242
+ feishuAppId: Array.isArray(config.channels?.feishu) ? config.channels?.feishu[0]?.appId : config.channels?.feishu?.appId || '',
243
+ feishuAppSecret: Array.isArray(config.channels?.feishu) ? config.channels?.feishu[0]?.appSecret : config.channels?.feishu?.appSecret || '',
244
+ feishu: config.channels?.feishu || []
245
+ });
246
+ });
247
+ // 更新配置并写入 config.json
248
+ app.post('/api/config', async (req, res) => {
249
+ try {
250
+ const { userId, apiKey, baseURL, model, embeddingModel, temperature, systemPrompt, memoryMaintenanceCron, sessionRetentionDays, workspace, maxHistoryMessages, maxToolIterations, feishu, feishuAppId, feishuAppSecret } = req.body;
251
+ // 更新配置对象
252
+ const newConfig = { ...config };
253
+ if (userId !== undefined)
254
+ newConfig.userId = String(userId || 'default').trim() || 'default';
255
+ if (apiKey !== undefined)
256
+ newConfig.llm.apiKey = apiKey;
257
+ if (baseURL !== undefined)
258
+ newConfig.llm.baseURL = baseURL;
259
+ if (model !== undefined)
260
+ newConfig.llm.model = model;
261
+ if (embeddingModel !== undefined)
262
+ newConfig.llm.embeddingModel = embeddingModel;
263
+ if (temperature !== undefined)
264
+ newConfig.llm.temperature = parseFloat(temperature);
265
+ if (maxHistoryMessages !== undefined)
266
+ newConfig.llm.maxHistoryMessages = parseInt(maxHistoryMessages, 10);
267
+ if (maxToolIterations !== undefined)
268
+ newConfig.llm.maxToolIterations = parseInt(maxToolIterations, 10);
269
+ if (systemPrompt !== undefined)
270
+ newConfig.systemPrompt = systemPrompt;
271
+ if (workspace !== undefined)
272
+ newConfig.workspace = workspace;
273
+ if (feishu !== undefined) {
274
+ if (!newConfig.channels)
275
+ newConfig.channels = {};
276
+ newConfig.channels.feishu = feishu;
277
+ }
278
+ else if (feishuAppId !== undefined || feishuAppSecret !== undefined) {
279
+ if (!newConfig.channels)
280
+ newConfig.channels = {};
281
+ const currentFeishu = newConfig.channels.feishu;
282
+ const firstBot = Array.isArray(currentFeishu)
283
+ ? (currentFeishu[0] || { name: 'default', appId: '', appSecret: '' })
284
+ : { name: 'default', appId: currentFeishu?.appId || '', appSecret: currentFeishu?.appSecret || '' };
285
+ newConfig.channels.feishu = [{
286
+ ...firstBot,
287
+ name: firstBot.name || 'default',
288
+ appId: feishuAppId !== undefined ? feishuAppId : firstBot.appId,
289
+ appSecret: feishuAppSecret !== undefined ? feishuAppSecret : firstBot.appSecret
290
+ }];
291
+ }
292
+ // System prompt is fully controlled by the UI (identity + base prompt).
293
+ let restartScheduler = false;
294
+ if (memoryMaintenanceCron !== undefined) {
295
+ if (!newConfig.scheduler)
296
+ newConfig.scheduler = {};
297
+ if (newConfig.scheduler.memoryMaintenanceCron !== memoryMaintenanceCron) {
298
+ newConfig.scheduler.memoryMaintenanceCron = memoryMaintenanceCron;
299
+ restartScheduler = true;
300
+ }
301
+ }
302
+ if (sessionRetentionDays !== undefined) {
303
+ if (!newConfig.scheduler)
304
+ newConfig.scheduler = {};
305
+ const days = Number(sessionRetentionDays);
306
+ if (Number.isFinite(days) && days > 0 && newConfig.scheduler.sessionRetentionDays !== days) {
307
+ newConfig.scheduler.sessionRetentionDays = days;
308
+ restartScheduler = true;
309
+ }
310
+ }
311
+ // 保存到文件
312
+ saveConfig(newConfig);
313
+ // 如果 LLM 配置变了,更新 Brain
314
+ brain.updateClient();
315
+ // 同步更新所有 Agent 与向量记忆客户端(否则仍会用旧 apiKey)
316
+ agentManager.updateAllConfigs();
317
+ memory.updateClients();
318
+ // 如果定时任务配置变了,重启 Scheduler
319
+ if (restartScheduler) {
320
+ initScheduler();
321
+ }
322
+ // 如果飞书配置变了,尝试重启或启动飞书通道
323
+ if (feishu !== undefined || feishuAppId !== undefined || feishuAppSecret !== undefined) {
324
+ const existingFeishu = channels.find(c => c.name === 'feishu');
325
+ if (existingFeishu) {
326
+ // Restart existing channel
327
+ await existingFeishu.stop();
328
+ await existingFeishu.start();
329
+ }
330
+ else if (newConfig.channels?.feishu && Array.isArray(newConfig.channels.feishu) && newConfig.channels.feishu.length > 0) {
331
+ // Start new channel if it wasn't running
332
+ const { FeishuChannel } = await import('./channels/feishu.js');
333
+ const feishuChannel = new FeishuChannel();
334
+ channels.push(feishuChannel);
335
+ await feishuChannel.start();
336
+ feishuChannel.onMessage((sessionId, message) => {
337
+ eventBus.emitEvent({
338
+ type: 'message',
339
+ payload: { content: message },
340
+ channel: feishuChannel.name,
341
+ sessionId: sessionId,
342
+ timestamp: Date.now()
343
+ });
344
+ });
345
+ }
346
+ }
347
+ res.json({ status: 'success', message: 'Config updated and saved to config.json' });
348
+ }
349
+ catch (error) {
350
+ res.status(500).json({ status: 'error', message: error.message });
351
+ }
352
+ });
353
+ app.get('/api/user/profile', async (req, res) => {
354
+ try {
355
+ const userId = String(req.query.userId || config.userId || 'default');
356
+ const profile = memory.getUserProfile(userId);
357
+ res.json({ userId, profile });
358
+ }
359
+ catch (e) {
360
+ res.status(500).json({ error: String(e?.message || e) });
361
+ }
362
+ });
363
+ app.post('/api/user/profile', async (req, res) => {
364
+ try {
365
+ const userId = String(req.body?.userId || config.userId || 'default');
366
+ const profile = req.body?.profile || {};
367
+ memory.updateUserProfile(userId, profile);
368
+ res.json({ status: 'success' });
369
+ }
370
+ catch (e) {
371
+ res.status(500).json({ error: String(e?.message || e) });
372
+ }
373
+ });
374
+ // 手动触发记忆归档
375
+ app.post('/api/sessions/:id/archive', async (req, res) => {
376
+ try {
377
+ const sessionId = req.params.id;
378
+ const session = await memory.getSession(sessionId);
379
+ if (!session) {
380
+ res.status(404).json({ error: 'Session not found' });
381
+ return;
382
+ }
383
+ // 强制触发一次归档逻辑,忽略时间间隔检查
384
+ // 我们复用 MemoryManager 的 runSessionArchiving,但需要它支持指定 session
385
+ // 或者我们在这里手动调用类似的逻辑,为了复用,建议在 MemoryManager 中公开一个 archiveSession 方法
386
+ // 由于 MemoryManager.runSessionArchiving 是批量处理,我们暂时修改 MemoryManager 来支持单个 Session 归档
387
+ // 或者简单地,我们在 index.ts 里调用 memory.runSessionArchiving(),它会扫描所有 session,
388
+ // 但为了强制归档,我们需要重置 lastArchivedAt 为 0 (或者很久以前)
389
+ // 方案:先更新 lastArchivedAt 为 0,然后调用 runSessionArchiving
390
+ await memory.updateSessionMeta(sessionId, { lastArchivedAt: 0 });
391
+ await memory.runSessionArchiving();
392
+ res.json({ status: 'success', message: 'Session archived successfully.' });
393
+ }
394
+ catch (e) {
395
+ res.status(500).json({ status: 'error', message: e.message });
396
+ }
397
+ });
398
+ // 获取定时任务状态
399
+ app.get('/api/scheduler/status', (req, res) => {
400
+ res.json(getSchedulerStatus());
401
+ });
402
+ // 启动定时任务
403
+ app.post('/api/scheduler/start', (req, res) => {
404
+ startScheduler();
405
+ res.json({ status: 'success', message: 'Scheduler started.', data: getSchedulerStatus() });
406
+ });
407
+ // 停止定时任务
408
+ app.post('/api/scheduler/stop', (req, res) => {
409
+ stopScheduler();
410
+ res.json({ status: 'success', message: 'Scheduler stopped.', data: getSchedulerStatus() });
411
+ });
412
+ // 手动触发记忆维护
413
+ app.post('/api/scheduler/run', async (req, res) => {
414
+ try {
415
+ await runMaintenanceNow();
416
+ res.json({ status: 'success', message: 'Memory maintenance completed.' });
417
+ }
418
+ catch (e) {
419
+ res.status(500).json({ status: 'error', message: e.message });
420
+ }
421
+ });
422
+ // 预设第三方插件的元数据
423
+ const thirdPartySkillsMeta = {
424
+ 'wps_office': {
425
+ id: 'wps_office',
426
+ name: 'WPS Office 办公助手',
427
+ description: '授权 AI 读取和编辑本地 WPS 文档、表格和演示文稿。',
428
+ enabled: false,
429
+ isBuiltIn: false
430
+ },
431
+ 'lark_docs': {
432
+ id: 'lark_docs',
433
+ name: '飞书文档 (Lark Docs) 助手',
434
+ description: '授权 AI 读取您的飞书云文档知识库,并在对话中进行总结和答疑。',
435
+ enabled: false,
436
+ isBuiltIn: false
437
+ }
438
+ };
439
+ app.get('/api/plugins', (req, res) => {
440
+ try {
441
+ const plugins = pluginManager.getPlugins();
442
+ // Merge with third-party auth state (WPS/Lark)
443
+ // Todo: This should be handled by the plugin system itself
444
+ const enhancedPlugins = plugins.map(p => {
445
+ if (p.name === 'wps_office') {
446
+ return { ...p, enabled: thirdPartySkillsMeta.wps_office.enabled };
447
+ }
448
+ if (p.name === 'lark_docs') {
449
+ return { ...p, enabled: thirdPartySkillsMeta.lark_docs.enabled };
450
+ }
451
+ return p;
452
+ });
453
+ // Add pending third-party plugins if not loaded yet
454
+ if (!enhancedPlugins.find(p => p.name === 'wps_office')) {
455
+ enhancedPlugins.push(thirdPartySkillsMeta.wps_office);
456
+ }
457
+ if (!enhancedPlugins.find(p => p.name === 'lark_docs')) {
458
+ enhancedPlugins.push(thirdPartySkillsMeta.lark_docs);
459
+ }
460
+ res.json(enhancedPlugins);
461
+ }
462
+ catch (e) {
463
+ res.status(500).json({ error: e.message });
464
+ }
465
+ });
466
+ // 存储 OAuth 状态以防止 CSRF 攻击
467
+ const oauthStates = new Map();
468
+ // 模拟的 Token 存储 (实际应存入 SQLite 或加密文件)
469
+ const oauthTokens = new Map();
470
+ // Mock OAuth 授权接口
471
+ app.get('/api/auth/:provider/authorize', (req, res) => {
472
+ const provider = req.params.provider;
473
+ const state = crypto.randomBytes(16).toString('hex');
474
+ oauthStates.set(state, { provider, expiresAt: Date.now() + 10 * 60 * 1000 });
475
+ let authUrl = '';
476
+ // 使用相对路径或动态获取 host,这里使用 req.get('host') 自动适配当前后端运行的端口
477
+ const protocol = req.protocol || 'http';
478
+ const host = req.get('host') || 'localhost:3001';
479
+ const redirectUri = encodeURIComponent(`${protocol}://${host}/api/auth/${provider}/callback`);
480
+ if (provider === 'lark') {
481
+ const firstFeishuBot = config.channels?.feishu?.[0];
482
+ const appId = firstFeishuBot?.appId;
483
+ const appSecret = firstFeishuBot?.appSecret;
484
+ if (!appId || !appSecret) {
485
+ return res.status(500).send('Feishu credentials not configured');
486
+ }
487
+ authUrl = `https://open.feishu.cn/open-apis/authen/v1/user_auth_page_beta?app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
488
+ }
489
+ else if (provider === 'wps') {
490
+ // WPS OAuth 2.0 文档参考:https://open.wps.cn/docs/office/auth
491
+ const appId = process.env.WPS_APP_ID || ''; // 需要在 WPS 开放平台申请
492
+ authUrl = `https://openapi.wps.cn/oauthapi/v2/authorize?response_type=code&app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
493
+ }
494
+ else {
495
+ return res.status(400).send('Unsupported provider');
496
+ }
497
+ res.redirect(authUrl);
498
+ });
499
+ // OAuth 回调接口 (处理真实的 Code 换 Token)
500
+ app.get('/api/auth/:provider/callback', async (req, res) => {
501
+ const provider = req.params.provider;
502
+ const { code, state } = req.query;
503
+ if (!code || !state) {
504
+ return res.status(400).send('Missing code or state');
505
+ }
506
+ // 验证 State
507
+ const stateData = oauthStates.get(state);
508
+ if (!stateData || stateData.provider !== provider || stateData.expiresAt < Date.now()) {
509
+ return res.status(400).send('Invalid or expired state');
510
+ }
511
+ oauthStates.delete(state);
512
+ // 尝试判断前端地址,如果是开发环境通常是 5173,生产环境可能是同源
513
+ const protocol = req.protocol || 'http';
514
+ const host = req.get('host') || 'localhost:3001';
515
+ // 简单策略:重定向回同源的 /settings(前端路由会处理)
516
+ const frontendBaseUrl = host.includes('3001') && process.env.NODE_ENV !== 'production'
517
+ ? 'http://localhost:5173'
518
+ : `${protocol}://${host}`;
519
+ try {
520
+ let tokenData;
521
+ if (provider === 'lark') {
522
+ const firstFeishuBot = config.channels?.feishu?.[0];
523
+ const appId = firstFeishuBot?.appId;
524
+ const appSecret = firstFeishuBot?.appSecret;
525
+ if (!appId || !appSecret) {
526
+ throw new Error('Feishu credentials not configured');
527
+ }
528
+ // 1. 获取 app_access_token
529
+ const appTokenRes = await axios.post('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
530
+ app_id: appId,
531
+ app_secret: appSecret
532
+ });
533
+ const appAccessToken = appTokenRes.data.app_access_token;
534
+ // 2. 用 code 换取 user_access_token
535
+ const userTokenRes = await axios.post('https://open.feishu.cn/open-apis/authen/v1/oidc/access_token', {
536
+ grant_type: 'authorization_code',
537
+ code: code
538
+ }, {
539
+ headers: { 'Authorization': `Bearer ${appAccessToken}` }
540
+ });
541
+ tokenData = userTokenRes.data.data;
542
+ oauthTokens.set('lark', tokenData); // 实际应持久化
543
+ }
544
+ else if (provider === 'wps') {
545
+ const appId = process.env.WPS_APP_ID || '';
546
+ const appSecret = process.env.WPS_APP_SECRET || '';
547
+ if (!appId || !appSecret) {
548
+ throw new Error('WPS credentials not configured in environment variables');
549
+ }
550
+ // WPS 用 code 换 token
551
+ const tokenRes = await axios.post('https://openapi.wps.cn/oauthapi/v2/token', {
552
+ app_id: appId,
553
+ app_secret: appSecret,
554
+ grant_type: 'authorization_code',
555
+ code: code
556
+ });
557
+ tokenData = tokenRes.data;
558
+ oauthTokens.set('wps', tokenData); // 实际应持久化
559
+ }
560
+ console.log(`[OAuth] Successfully authenticated with ${provider}`);
561
+ // 更新内存中的插件状态为已启用
562
+ const pluginId = provider === 'wps' ? 'wps_office' : 'lark_docs';
563
+ if (thirdPartySkillsMeta[pluginId]) {
564
+ thirdPartySkillsMeta[pluginId].enabled = true;
565
+ // 实际项目中,这里应该实例化对应的 Skill 类并调用 skillRegistry.register()
566
+ // 例如: skillRegistry.register(new LarkDocSkill(tokenData.access_token));
567
+ }
568
+ // 授权成功,重定向回前端并带上成功标志
569
+ res.redirect(`${frontendBaseUrl}/?auth_success=true&provider=${provider}`);
570
+ }
571
+ catch (error) {
572
+ console.error(`[OAuth] Failed to exchange token for ${provider}:`, error.response?.data || error.message);
573
+ res.redirect(`${frontendBaseUrl}/?auth_success=false&error=token_exchange_failed`);
574
+ }
575
+ });
576
+ app.get('/api/projects', async (req, res) => {
577
+ try {
578
+ const list = memory.structured.listProjects();
579
+ res.json(list);
580
+ }
581
+ catch (e) {
582
+ res.status(500).json({ error: e.message });
583
+ }
584
+ });
585
+ app.post('/api/projects', async (req, res) => {
586
+ try {
587
+ const { id, name, description, departments, members, directory } = req.body;
588
+ if (!id || !name) {
589
+ res.status(400).json({ error: 'id and name are required' });
590
+ return;
591
+ }
592
+ memory.structured.createProject(id, name, description, departments, members, directory);
593
+ const item = memory.structured.getProject(id);
594
+ res.json(item);
595
+ }
596
+ catch (e) {
597
+ res.status(500).json({ error: e.message });
598
+ }
599
+ });
600
+ app.patch('/api/projects/:id', async (req, res) => {
601
+ try {
602
+ const { name, description, departments, members, directory } = req.body;
603
+ memory.structured.updateProject(req.params.id, { name, description, departments, members, directory });
604
+ const item = memory.structured.getProject(req.params.id);
605
+ res.json(item);
606
+ }
607
+ catch (e) {
608
+ res.status(500).json({ error: e.message });
609
+ }
610
+ });
611
+ app.delete('/api/projects/:id', async (req, res) => {
612
+ try {
613
+ memory.structured.deleteProject(req.params.id);
614
+ res.json({ status: 'success' });
615
+ }
616
+ catch (e) {
617
+ res.status(500).json({ error: e.message });
618
+ }
619
+ });
620
+ // Notebooks CRUD
621
+ app.get('/api/notebooks', async (req, res) => {
622
+ try {
623
+ const list = memory.structured.listNotebooks();
624
+ res.json(list);
625
+ }
626
+ catch (e) {
627
+ res.status(500).json({ error: e.message });
628
+ }
629
+ });
630
+ app.post('/api/notebooks', async (req, res) => {
631
+ try {
632
+ const { id, name, description, keywords } = req.body;
633
+ if (!id || !name) {
634
+ res.status(400).json({ error: 'id and name are required' });
635
+ return;
636
+ }
637
+ memory.structured.createNotebook(id, name, description, keywords);
638
+ const item = memory.structured.getNotebook(id);
639
+ res.json(item);
640
+ }
641
+ catch (e) {
642
+ res.status(500).json({ error: e.message });
643
+ }
644
+ });
645
+ app.patch('/api/notebooks/:id', async (req, res) => {
646
+ try {
647
+ const { name, description, keywords } = req.body;
648
+ memory.structured.updateNotebook(req.params.id, { name, description, keywords });
649
+ const item = memory.structured.getNotebook(req.params.id);
650
+ res.json(item);
651
+ }
652
+ catch (e) {
653
+ res.status(500).json({ error: e.message });
654
+ }
655
+ });
656
+ app.delete('/api/notebooks/:id', async (req, res) => {
657
+ try {
658
+ memory.structured.deleteNotebook(req.params.id);
659
+ res.json({ status: 'success' });
660
+ }
661
+ catch (e) {
662
+ res.status(500).json({ error: e.message });
663
+ }
664
+ });
665
+ // Notes CRUD
666
+ app.get('/api/notes', async (req, res) => {
667
+ try {
668
+ const notebookId = req.query.notebookId;
669
+ const list = memory.structured.listNotes(notebookId);
670
+ res.json(list);
671
+ }
672
+ catch (e) {
673
+ res.status(500).json({ error: e.message });
674
+ }
675
+ });
676
+ app.post('/api/notes', async (req, res) => {
677
+ try {
678
+ const { id, notebookId, title, content, isTodo, done } = req.body;
679
+ if (!id || !notebookId || !title) {
680
+ res.status(400).json({ error: 'id, notebookId and title are required' });
681
+ return;
682
+ }
683
+ memory.structured.createNote(id, notebookId, title, content, isTodo, done);
684
+ const item = memory.structured.getNote(id);
685
+ res.json(item);
686
+ }
687
+ catch (e) {
688
+ res.status(500).json({ error: e.message });
689
+ }
690
+ });
691
+ app.patch('/api/notes/:id', async (req, res) => {
692
+ try {
693
+ const { title, content, isTodo, done } = req.body;
694
+ memory.structured.updateNote(req.params.id, { title, content, isTodo, done });
695
+ const item = memory.structured.getNote(req.params.id);
696
+ res.json(item);
697
+ }
698
+ catch (e) {
699
+ res.status(500).json({ error: e.message });
700
+ }
701
+ });
702
+ app.delete('/api/notes/:id', async (req, res) => {
703
+ try {
704
+ memory.structured.deleteNote(req.params.id);
705
+ res.json({ status: 'success' });
706
+ }
707
+ catch (e) {
708
+ res.status(500).json({ error: e.message });
709
+ }
710
+ });
711
+ // 初始化 Agents
712
+ agentManager.registerAgent(createOfficeAgent());
713
+ // 初始化插件管理器
714
+ const pluginManager = new PluginManager(path.join(process.cwd(), 'plugins'));
715
+ await pluginManager.loadPlugins();
716
+ // 初始化通道
717
+ const channels = createChannels({ app, io, config, pluginManager });
718
+ for (const ch of channels) {
719
+ if (ch instanceof WebChannel) {
720
+ ch.setSyncHandler(async (sessionId, since) => {
721
+ const session = await memory.getSession(sessionId);
722
+ if (!session)
723
+ return { messages: [], runs: [] };
724
+ const runs = await memory.listRuns(sessionId);
725
+ const sinceTs = typeof since === 'number' ? since : 0;
726
+ const messages = (session.messages || []).filter((m) => (m.timestamp ?? 0) > sinceTs);
727
+ return { messages, runs };
728
+ });
729
+ }
730
+ }
731
+ // 启动通道并绑定事件
732
+ channels.forEach(channel => {
733
+ channel.start();
734
+ // 监听通道消息 -> 发送到 EventBus
735
+ channel.onMessage((sessionId, message) => {
736
+ eventBus.emitEvent({
737
+ type: 'message',
738
+ payload: { content: message },
739
+ channel: channel.name,
740
+ sessionId: sessionId,
741
+ timestamp: Date.now()
742
+ });
743
+ });
744
+ });
745
+ // 核心逻辑:监听 EventBus 消息 -> Brain 处理 -> 发回 EventBus
746
+ eventBus.onEvent('message', async (event) => {
747
+ const { sessionId, payload, channel: channelName } = event;
748
+ const content = payload.content;
749
+ try {
750
+ console.log(`[EventBus] Processing message for session: ${sessionId}`);
751
+ let session = await memory.getSession(sessionId);
752
+ if (!session) {
753
+ // 对于第三方 Channel (如 Feishu),如果 Session 不存在,自动创建
754
+ if (channelName === 'feishu' || channelName === 'telegram' || channelName === 'dingtalk' || channelName === 'wechat') {
755
+ console.log(`[EventBus] Auto-creating session for ${channelName}: ${sessionId}`);
756
+ // 使用传入的 sessionId 作为 id,避免生成新的 uuid
757
+ // 注意:memory.createSession 默认生成 UUID,我们需要支持指定 ID
758
+ // 但 memory.createSession 目前不支持指定 ID,它调用 shortTerm.createSession
759
+ // 所以我们需要扩展 createSession 或者使用一种特殊方式
760
+ // 这里采用一种更简单的策略:如果是第三方渠道,我们允许 ShortTermMemory 懒加载/自动创建
761
+ // 或者在这里先创建一个标准 Session,然后映射?
762
+ // 不,sessionId 必须保持一致 (e.g. feishu:chat_id) 才能在后续消息中找到上下文
763
+ // 让我们修改 ShortTermMemory 的 createSession 逻辑,或者直接在这里 hack
764
+ // 但最好的方式是让 memory.createSession 支持自定义 ID
765
+ // 临时方案:如果 memory 支持 ensureSession
766
+ // 实际上,我们应该修改 MemoryManager 和 ShortTermMemory 支持 `createSession({ id: ... })`
767
+ // 由于现在无法直接修改底层接口,我们尝试调用 createSession 并忽略返回的 ID?不对。
768
+ // 正确的做法:
769
+ // 第三方 Channel 的 SessionID 通常是固定的 (如 `feishu:123`)。
770
+ // 我们需要 ShortTermMemory 支持 `getOrCreateSession(id)`
771
+ // 暂时先用 createSession 创建一个带 alias 的 session,
772
+ // 但这样 sessionId 会变,导致无法匹配。
773
+ // 关键点:EventBus 接收到的 sessionId 是 `feishu:xxx`。
774
+ // 我们必须确保数据库里有一个 ID 为 `feishu:xxx` 的 session。
775
+ // 现有的 `createSession` 会生成 UUID。
776
+ // 让我们修改 memory.createSession 及其底层实现来支持自定义 ID。
777
+ // 现在先报错,提示需要修改 ShortTermMemory。
778
+ // 但为了不中断流程,我们假设 ShortTermMemory.createSession 能够接受 id 参数
779
+ // 实际上,查看 memory.ts,createSession 调用 shortTerm.createSession
780
+ // 查看 short-term.ts (需要 read),createSession 确实生成了 uuid。
781
+ // 为了快速修复,我们在 index.ts 里做不到,必须去修改 ShortTermMemory。
782
+ // 假设我们已经修改了(下一步操作),这里我们调用一个新的方法
783
+ session = await memory.createSession({ id: sessionId, alias: `${channelName} Session` });
784
+ }
785
+ else {
786
+ console.error(`[EventBus] Session not found: ${sessionId}`);
787
+ throw new Error('Session not found. Please create or select a session first.');
788
+ }
789
+ }
790
+ await memory.addMessage(sessionId, { role: 'user', content, timestamp: Date.now() });
791
+ const run = await memory.createRun(sessionId, content);
792
+ eventBus.emitEvent({
793
+ type: 'run_ack',
794
+ payload: { runId: run.id },
795
+ channel: channelName,
796
+ sessionId,
797
+ timestamp: Date.now()
798
+ });
799
+ eventBus.emitEvent({
800
+ type: 'run_status',
801
+ payload: { runId: run.id, status: 'queued' },
802
+ channel: channelName,
803
+ sessionId,
804
+ timestamp: Date.now()
805
+ });
806
+ taskQueue.enqueue({ run, channel: channelName });
807
+ }
808
+ catch (error) {
809
+ console.error('Error processing message:', error);
810
+ try {
811
+ await memory.addMessage(sessionId, { role: 'assistant', content: `Error: ${error.message}`, timestamp: Date.now() });
812
+ }
813
+ catch {
814
+ // ignore
815
+ }
816
+ eventBus.emitEvent({
817
+ type: 'error',
818
+ payload: { error: error.message },
819
+ channel: channelName,
820
+ sessionId: sessionId,
821
+ timestamp: Date.now()
822
+ });
823
+ }
824
+ });
825
+ // 监听 EventBus 响应 -> 发送给通道
826
+ eventBus.onEvent('response', (event) => {
827
+ const { sessionId, payload, channel: channelName } = event;
828
+ const channel = channels.find(c => c.name === channelName);
829
+ if (channel) {
830
+ channel.send(sessionId, payload.content);
831
+ }
832
+ });
833
+ eventBus.onEvent('run_ack', (event) => {
834
+ const { sessionId, payload, channel: channelName } = event;
835
+ const channel = channels.find(c => c.name === channelName);
836
+ if (channel?.sendEvent) {
837
+ channel.sendEvent(sessionId, 'run_ack', payload);
838
+ }
839
+ });
840
+ eventBus.onEvent('run_status', (event) => {
841
+ const { sessionId, payload, channel: channelName } = event;
842
+ const channel = channels.find(c => c.name === channelName);
843
+ if (channel?.sendEvent) {
844
+ channel.sendEvent(sessionId, 'run_status', payload);
845
+ }
846
+ });
847
+ eventBus.onEvent('error', (event) => {
848
+ const { sessionId, payload, channel: channelName } = event;
849
+ const channel = channels.find(c => c.name === channelName);
850
+ if (channel) {
851
+ channel.send(sessionId, `Error: ${payload.error}`);
852
+ }
853
+ });
854
+ // 启动记忆维护任务(自动清理过期记忆)
855
+ initScheduler();
856
+ // 启动服务器
857
+ const PORT = config.server.port;
858
+ httpServer.listen(PORT, () => {
859
+ console.log(`Server running on port ${PORT}`);
860
+ console.log(`Socket.IO server running`);
861
+ });
862
+ httpServer.on('error', (err) => {
863
+ console.error('[Server] Startup error:', err);
864
+ if (err.code === 'EADDRINUSE') {
865
+ console.error(`Port ${PORT} is already in use.`);
866
+ process.exit(1);
867
+ }
868
+ });
869
+ // Graceful shutdown
870
+ const shutdown = () => {
871
+ console.log('Shutting down server...');
872
+ httpServer.close(() => {
873
+ console.log('Server closed');
874
+ process.exit(0);
875
+ });
876
+ };
877
+ process.on('SIGTERM', shutdown);
878
+ process.on('SIGINT', shutdown);