xiaozuoassistant 0.1.41
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 +90 -0
- package/bin/cli.js +429 -0
- package/config.json +36 -0
- package/dist/client/assets/browser-ponyfill-DNlTAU2D.js +2 -0
- package/dist/client/assets/index-BPATxdcV.js +153 -0
- package/dist/client/assets/index-CgL5gMVL.css +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/index.html +354 -0
- package/dist/client/locales/en/translation.json +77 -0
- package/dist/client/locales/zh/translation.json +77 -0
- package/dist/server/agents/office.js +23 -0
- package/dist/server/app.js +50 -0
- package/dist/server/channels/base-channel.js +13 -0
- package/dist/server/channels/create-channels.js +18 -0
- package/dist/server/channels/dingtalk.js +83 -0
- package/dist/server/channels/feishu.js +95 -0
- package/dist/server/channels/telegram.js +53 -0
- package/dist/server/channels/terminal.js +49 -0
- package/dist/server/channels/web.js +45 -0
- package/dist/server/channels/wechat.js +107 -0
- package/dist/server/config/loader.js +73 -0
- package/dist/server/config/prompts.js +12 -0
- package/dist/server/core/agents/manager.js +22 -0
- package/dist/server/core/agents/runtime.js +85 -0
- package/dist/server/core/brain.js +131 -0
- package/dist/server/core/event-bus.js +24 -0
- package/dist/server/core/logger.js +71 -0
- package/dist/server/core/memories/manager.js +115 -0
- package/dist/server/core/memories/short-term.js +128 -0
- package/dist/server/core/memories/structured.js +109 -0
- package/dist/server/core/memories/vector.js +138 -0
- package/dist/server/core/memory.js +2 -0
- package/dist/server/core/plugin-manager.js +112 -0
- package/dist/server/core/plugin.js +1 -0
- package/dist/server/core/scheduler.js +24 -0
- package/dist/server/core/types.js +1 -0
- package/dist/server/index.js +318 -0
- package/dist/server/llm/openai.js +23 -0
- package/dist/server/routes/auth.js +28 -0
- package/dist/server/server/create-http.js +17 -0
- package/dist/server/server.js +29 -0
- package/dist/server/skills/base-skill.js +16 -0
- package/dist/server/skills/create-agent.js +58 -0
- package/dist/server/skills/delegate.js +39 -0
- package/dist/server/skills/file-system.js +137 -0
- package/dist/server/skills/list-agents.js +24 -0
- package/dist/server/skills/office-excel.js +84 -0
- package/dist/server/skills/office-ppt.js +58 -0
- package/dist/server/skills/office-word.js +90 -0
- package/dist/server/skills/registry.js +27 -0
- package/dist/server/skills/search.js +31 -0
- package/dist/server/skills/system-time.js +27 -0
- package/package.json +116 -0
- package/public/favicon.svg +4 -0
- package/public/locales/en/translation.json +77 -0
- package/public/locales/zh/translation.json +77 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { config } from '../../config/loader.js';
|
|
5
|
+
import { createOpenAIClient } from '../../llm/openai.js';
|
|
6
|
+
const VECTOR_DB_DIR = path.resolve(process.cwd(), 'data/lancedb');
|
|
7
|
+
export class VectorMemory {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.openai = null;
|
|
10
|
+
this.isInitialized = false;
|
|
11
|
+
fs.ensureDirSync(VECTOR_DB_DIR);
|
|
12
|
+
this.updateClient();
|
|
13
|
+
this.initDB();
|
|
14
|
+
}
|
|
15
|
+
updateClient() {
|
|
16
|
+
this.openai = config.llm.apiKey ? createOpenAIClient(config.llm) : null;
|
|
17
|
+
}
|
|
18
|
+
static getInstance() {
|
|
19
|
+
if (!VectorMemory.instance) {
|
|
20
|
+
VectorMemory.instance = new VectorMemory();
|
|
21
|
+
}
|
|
22
|
+
return VectorMemory.instance;
|
|
23
|
+
}
|
|
24
|
+
async initDB() {
|
|
25
|
+
try {
|
|
26
|
+
this.db = await lancedb.connect(VECTOR_DB_DIR);
|
|
27
|
+
// Ensure table exists
|
|
28
|
+
const tableNames = await this.db.tableNames();
|
|
29
|
+
if (!tableNames.includes('memories')) {
|
|
30
|
+
// Create table with dummy data to define schema, then delete it
|
|
31
|
+
// LanceDB schema inference is based on data
|
|
32
|
+
const dummyData = [{
|
|
33
|
+
id: 'init',
|
|
34
|
+
text: 'init',
|
|
35
|
+
vector: Array(1536).fill(0), // OpenAI embedding dimension
|
|
36
|
+
metadata: { type: 'recent', timestamp: Date.now(), sessionId: 'init', tags: '' }
|
|
37
|
+
}];
|
|
38
|
+
this.table = await this.db.createTable('memories', dummyData);
|
|
39
|
+
// await this.table.delete('id = "init"'); // Older lancedb syntax might differ
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.table = await this.db.openTable('memories');
|
|
43
|
+
}
|
|
44
|
+
this.isInitialized = true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('Failed to init LanceDB:', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async getEmbedding(text) {
|
|
51
|
+
if (!this.openai) {
|
|
52
|
+
console.warn('OpenAI client not initialized (missing API Key), skipping embedding.');
|
|
53
|
+
return Array(1536).fill(0);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
// Use OpenAI compatible embedding endpoint
|
|
57
|
+
// Note: Some compatible providers might use different models/dimensions.
|
|
58
|
+
// Default to text-embedding-ada-002 or similar standard.
|
|
59
|
+
// For DashScope/Qwen, we might need 'text-embedding-v1' or similar if 'text-embedding-v3-small' fails.
|
|
60
|
+
// We'll try to use a more standard one or catch the error.
|
|
61
|
+
const response = await this.openai.embeddings.create({
|
|
62
|
+
model: 'text-embedding-v1', // Common alias in compatible APIs, or use config if available
|
|
63
|
+
input: text,
|
|
64
|
+
});
|
|
65
|
+
return response.data[0].embedding;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Embedding failed:', error);
|
|
69
|
+
// Fallback: return zero vector or throw?
|
|
70
|
+
// For MVP robustness, return zeros if embedding fails (search won't work but app won't crash)
|
|
71
|
+
return Array(1536).fill(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async addMemory(text, type, metadata = {}) {
|
|
75
|
+
if (!this.isInitialized)
|
|
76
|
+
await this.initDB();
|
|
77
|
+
if (!this.table)
|
|
78
|
+
return; // DB failed to init
|
|
79
|
+
const vector = await this.getEmbedding(text);
|
|
80
|
+
const memory = {
|
|
81
|
+
id: Math.random().toString(36).substring(7),
|
|
82
|
+
text,
|
|
83
|
+
vector,
|
|
84
|
+
metadata: {
|
|
85
|
+
type,
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
...metadata
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
await this.table.add([memory]);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error('Failed to add memory to vector db:', e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async search(query, type, limit = 5) {
|
|
98
|
+
if (!this.isInitialized)
|
|
99
|
+
await this.initDB();
|
|
100
|
+
if (!this.table)
|
|
101
|
+
return [];
|
|
102
|
+
const vector = await this.getEmbedding(query);
|
|
103
|
+
// Check if table supports search
|
|
104
|
+
if (this.table.search) {
|
|
105
|
+
let search = this.table.search(vector).limit(limit);
|
|
106
|
+
if (type) {
|
|
107
|
+
try {
|
|
108
|
+
// Using simpler filter to avoid Dictionary type issues
|
|
109
|
+
// search = search.where(`metadata.type = '${type}'`);
|
|
110
|
+
// Skip filtering for now to avoid lancedb issues
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
console.warn('Filter not supported in this LanceDB version/mode, filtering manually');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const results = await search.execute();
|
|
118
|
+
return results.map((r) => ({
|
|
119
|
+
id: r.id,
|
|
120
|
+
text: r.text,
|
|
121
|
+
vector: r.vector,
|
|
122
|
+
metadata: r.metadata
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
console.error('Vector search failed:', e);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
async getMemoriesOlderThan(maxAgeDays) {
|
|
133
|
+
return []; // Disable for now to avoid build errors with older lancedb types
|
|
134
|
+
}
|
|
135
|
+
async pruneOldMemories(maxAgeDays) {
|
|
136
|
+
return 0; // Disable for now
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { skillRegistry } from '../skills/registry.js';
|
|
4
|
+
export class PluginManager {
|
|
5
|
+
constructor(pluginDir) {
|
|
6
|
+
this.plugins = new Map();
|
|
7
|
+
this.channels = [];
|
|
8
|
+
this.pluginDir = pluginDir;
|
|
9
|
+
}
|
|
10
|
+
getChannels() {
|
|
11
|
+
return this.channels;
|
|
12
|
+
}
|
|
13
|
+
createContext(pluginName) {
|
|
14
|
+
return {
|
|
15
|
+
registerSkill: (skill) => {
|
|
16
|
+
console.log(`[PluginManager] Plugin '${pluginName}' registered skill: ${skill.name}`);
|
|
17
|
+
// Inject plugin name into skill for tracking if needed
|
|
18
|
+
skill.pluginName = pluginName;
|
|
19
|
+
skillRegistry.register(skill);
|
|
20
|
+
},
|
|
21
|
+
registerChannel: (channel) => {
|
|
22
|
+
console.log(`[PluginManager] Plugin '${pluginName}' registered channel: ${channel.name}`);
|
|
23
|
+
this.channels.push(channel);
|
|
24
|
+
// Note: Channels registered after app start might need manual starting if the app is already running.
|
|
25
|
+
// For simplicity in this MVP, we assume plugins are loaded at startup.
|
|
26
|
+
},
|
|
27
|
+
logger: {
|
|
28
|
+
info: (msg) => console.log(`[Plugin:${pluginName}] ${msg}`),
|
|
29
|
+
warn: (msg) => console.warn(`[Plugin:${pluginName}] ${msg}`),
|
|
30
|
+
error: (msg) => console.error(`[Plugin:${pluginName}] ${msg}`),
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async loadPlugins() {
|
|
35
|
+
try {
|
|
36
|
+
// Ensure plugin directory exists
|
|
37
|
+
try {
|
|
38
|
+
await fs.access(this.pluginDir);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.log(`[PluginManager] Plugin directory ${this.pluginDir} does not exist. Creating...`);
|
|
42
|
+
await fs.mkdir(this.pluginDir, { recursive: true });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const entries = await fs.readdir(this.pluginDir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
await this.loadPluginFromDir(path.join(this.pluginDir, entry.name));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('[PluginManager] Error loading plugins:', error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async loadPluginFromDir(dirPath) {
|
|
57
|
+
try {
|
|
58
|
+
// Check for package.json or index.js/ts
|
|
59
|
+
const pkgPath = path.join(dirPath, 'package.json');
|
|
60
|
+
const indexPath = path.join(dirPath, 'index.js'); // Assuming compiled JS for now, or ts-node handling
|
|
61
|
+
let entryPoint = indexPath;
|
|
62
|
+
// Try to read package.json main field
|
|
63
|
+
try {
|
|
64
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf-8');
|
|
65
|
+
const pkg = JSON.parse(pkgContent);
|
|
66
|
+
if (pkg.main) {
|
|
67
|
+
entryPoint = path.join(dirPath, pkg.main);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// No package.json, assume index.js
|
|
72
|
+
}
|
|
73
|
+
// Dynamic import
|
|
74
|
+
// Note: In a real-world scenario, we might need to handle CJS/ESM compatibility carefully.
|
|
75
|
+
// Here we assume the plugin exports a default object implementing the Plugin interface.
|
|
76
|
+
// Ensure we are importing a file that exists
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(entryPoint);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Try .ts if .js doesn't exist (for dev mode with tsx/ts-node)
|
|
82
|
+
if (entryPoint.endsWith('.js')) {
|
|
83
|
+
const tsEntryPoint = entryPoint.replace(/\.js$/, '.ts');
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(tsEntryPoint);
|
|
86
|
+
entryPoint = tsEntryPoint;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// console.warn(`[PluginManager] Could not find entry point for plugin at ${dirPath}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const pluginModule = await import(entryPoint);
|
|
98
|
+
const plugin = pluginModule.default || pluginModule;
|
|
99
|
+
if (!plugin.metadata || !plugin.onLoad) {
|
|
100
|
+
console.warn(`[PluginManager] Invalid plugin structure in ${dirPath}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(`[PluginManager] Loading plugin: ${plugin.metadata.name} (${plugin.metadata.version})`);
|
|
104
|
+
const context = this.createContext(plugin.metadata.name);
|
|
105
|
+
await plugin.onLoad(context);
|
|
106
|
+
this.plugins.set(plugin.metadata.name, plugin);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error(`[PluginManager] Failed to load plugin from ${dirPath}:`, error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import cron from 'node-cron';
|
|
2
|
+
import { memoryManager } from './memories/manager.js';
|
|
3
|
+
import { config } from '../config/loader.js';
|
|
4
|
+
let cronTask = null;
|
|
5
|
+
export const initScheduler = () => {
|
|
6
|
+
// Stop existing task if any
|
|
7
|
+
if (cronTask) {
|
|
8
|
+
cronTask.stop();
|
|
9
|
+
cronTask = null;
|
|
10
|
+
}
|
|
11
|
+
// Default: Every day at midnight
|
|
12
|
+
const cronSchedule = config.scheduler?.memoryMaintenanceCron || '0 0 * * *';
|
|
13
|
+
console.log(`[Scheduler] Initializing memory maintenance job with schedule: "${cronSchedule}"`);
|
|
14
|
+
// Only run immediately on initial startup, not every restart (optional)
|
|
15
|
+
// Or we can assume restart means config changed, so maybe we don't need to run immediately?
|
|
16
|
+
// Let's keep it simple: only schedule the cron job. Immediate run can be manual via API.
|
|
17
|
+
cronTask = cron.schedule(cronSchedule, () => {
|
|
18
|
+
memoryManager.runMaintenance();
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export const runMaintenanceNow = () => {
|
|
22
|
+
console.log('[Scheduler] Manually triggering maintenance...');
|
|
23
|
+
memoryManager.runMaintenance();
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,318 @@
|
|
|
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 { memory } from './core/memory.js';
|
|
13
|
+
import { skillRegistry } from './skills/registry.js';
|
|
14
|
+
import { SystemTimeSkill } from './skills/system-time.js';
|
|
15
|
+
import { SearchSkill } from './skills/search.js';
|
|
16
|
+
import { ListDirectorySkill, ReadFileSkill, WriteFileSkill, DeleteFileSkill } from './skills/file-system.js';
|
|
17
|
+
import { PluginManager } from './core/plugin-manager.js';
|
|
18
|
+
import { initScheduler, runMaintenanceNow } from './core/scheduler.js';
|
|
19
|
+
import { agentManager } from './core/agents/manager.js';
|
|
20
|
+
import { createOfficeAgent } from './agents/office.js';
|
|
21
|
+
import { DelegateSkill } from './skills/delegate.js';
|
|
22
|
+
import { createChannels } from './channels/create-channels.js';
|
|
23
|
+
import { createHttpStack } from './server/create-http.js';
|
|
24
|
+
// 解决 ES Module 中没有 __dirname 的问题
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
const { app, httpServer, io } = createHttpStack();
|
|
28
|
+
// Request logging middleware
|
|
29
|
+
app.use((req, res, next) => {
|
|
30
|
+
console.log(`[HTTP] ${req.method} ${req.url}`);
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
33
|
+
// 静态文件服务
|
|
34
|
+
// 在生产环境中(dist/server/index.js),客户端构建文件通常位于 ../../dist/client 或 ../client
|
|
35
|
+
// 我们假设构建结构如下:
|
|
36
|
+
// dist/
|
|
37
|
+
// server/
|
|
38
|
+
// index.js
|
|
39
|
+
// client/
|
|
40
|
+
// index.html
|
|
41
|
+
// assets/
|
|
42
|
+
//
|
|
43
|
+
// 这样在 server/index.js 中,客户端路径应该是 path.join(__dirname, '../client')
|
|
44
|
+
const clientBuildPath = path.join(__dirname, '../client');
|
|
45
|
+
console.log(`[Server] Serving static files from: ${clientBuildPath}`);
|
|
46
|
+
// 或者在开发模式下,可能是不同的路径,但开发模式通常用 Vite 代理,所以这里主要关注生产环境。
|
|
47
|
+
app.use(express.static(clientBuildPath));
|
|
48
|
+
// API 路由
|
|
49
|
+
// ... existing routes ...
|
|
50
|
+
// 前端路由回退处理 (SPA 支持)
|
|
51
|
+
// 必须放在所有 API 路由之后
|
|
52
|
+
app.get('*', (req, res, next) => {
|
|
53
|
+
// 如果是 API 请求,跳过
|
|
54
|
+
if (req.path.startsWith('/api')) {
|
|
55
|
+
return next();
|
|
56
|
+
}
|
|
57
|
+
const indexPath = path.join(clientBuildPath, 'index.html');
|
|
58
|
+
if (fs.existsSync(indexPath)) { // 这里需要确保 fs 已导入,或者用 fs.access
|
|
59
|
+
res.sendFile(indexPath);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// 如果找不到 index.html(例如开发模式只启动了后端),则返回简单的提示
|
|
63
|
+
res.send('xiaozuoAssistant backend is running. Frontend static files not found in ' + clientBuildPath);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// ... rest of the file ...
|
|
67
|
+
// API 路由
|
|
68
|
+
app.get('/api/health', (req, res) => {
|
|
69
|
+
res.json({ status: 'ok', message: 'xiaozuoAssistant backend is running' });
|
|
70
|
+
});
|
|
71
|
+
// 获取所有会话
|
|
72
|
+
app.get('/api/sessions', async (req, res) => {
|
|
73
|
+
const sessions = await memory.listSessions();
|
|
74
|
+
res.json(sessions);
|
|
75
|
+
});
|
|
76
|
+
// 获取会话详情
|
|
77
|
+
app.get('/api/sessions/:id', async (req, res) => {
|
|
78
|
+
const session = await memory.getSession(req.params.id);
|
|
79
|
+
if (session) {
|
|
80
|
+
res.json(session);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
res.status(404).json({ error: 'Session not found' });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// 创建新会话
|
|
87
|
+
app.post('/api/sessions', async (req, res) => {
|
|
88
|
+
const session = await memory.createSession();
|
|
89
|
+
res.json(session);
|
|
90
|
+
});
|
|
91
|
+
// 删除会话
|
|
92
|
+
app.delete('/api/sessions/:id', async (req, res) => {
|
|
93
|
+
await memory.deleteSession(req.params.id);
|
|
94
|
+
res.json({ status: 'success', message: 'Session deleted' });
|
|
95
|
+
});
|
|
96
|
+
// 文件选择接口 (使用系统默认文件对话框可能需要Electron,这里作为Web服务,我们提供目录列表供选择)
|
|
97
|
+
// 简单的目录列举接口
|
|
98
|
+
app.post('/api/fs/list', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const dirPath = req.body.path || process.cwd();
|
|
101
|
+
// Use fsPromises for async operations which returns proper types for withFileTypes
|
|
102
|
+
const items = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
103
|
+
const result = items.map(item => ({
|
|
104
|
+
name: item.name,
|
|
105
|
+
isDirectory: item.isDirectory(),
|
|
106
|
+
path: path.join(dirPath, item.name)
|
|
107
|
+
})).filter(item => item.isDirectory && !item.name.startsWith('.')); // 仅列出目录,忽略隐藏目录
|
|
108
|
+
res.json({
|
|
109
|
+
current: dirPath,
|
|
110
|
+
parent: path.dirname(dirPath),
|
|
111
|
+
items: result
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
res.status(500).json({ error: e.message });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// 获取当前配置
|
|
119
|
+
app.get('/api/config', async (req, res) => {
|
|
120
|
+
res.json({
|
|
121
|
+
apiKey: config.llm.apiKey,
|
|
122
|
+
baseURL: config.llm.baseURL,
|
|
123
|
+
model: config.llm.model,
|
|
124
|
+
temperature: config.llm.temperature,
|
|
125
|
+
systemPrompt: config.systemPrompt,
|
|
126
|
+
memoryMaintenanceCron: config.scheduler?.memoryMaintenanceCron,
|
|
127
|
+
workspace: config.workspace
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// 更新配置并写入 config.json
|
|
131
|
+
app.post('/api/config', async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const { apiKey, baseURL, model, temperature, systemPrompt, memoryMaintenanceCron, workspace } = req.body;
|
|
134
|
+
// 更新配置对象
|
|
135
|
+
const newConfig = { ...config };
|
|
136
|
+
if (apiKey !== undefined)
|
|
137
|
+
newConfig.llm.apiKey = apiKey;
|
|
138
|
+
if (baseURL !== undefined)
|
|
139
|
+
newConfig.llm.baseURL = baseURL;
|
|
140
|
+
if (model !== undefined)
|
|
141
|
+
newConfig.llm.model = model;
|
|
142
|
+
if (temperature !== undefined)
|
|
143
|
+
newConfig.llm.temperature = parseFloat(temperature);
|
|
144
|
+
if (systemPrompt !== undefined)
|
|
145
|
+
newConfig.systemPrompt = systemPrompt;
|
|
146
|
+
if (workspace !== undefined)
|
|
147
|
+
newConfig.workspace = workspace;
|
|
148
|
+
// Handle System Prompt update logic
|
|
149
|
+
// We only update systemPrompt automatically if workspace changed OR it's a new config
|
|
150
|
+
// But if user manually edited systemPrompt in the same request, we should respect that?
|
|
151
|
+
// The current frontend sends all fields.
|
|
152
|
+
// If workspace changed, we append/update it.
|
|
153
|
+
// Simplification: Always ensure the workspace is in the system prompt if configured.
|
|
154
|
+
if (newConfig.workspace) {
|
|
155
|
+
const workspaceLine = `Current Workspace: ${newConfig.workspace}`;
|
|
156
|
+
// If system prompt doesn't have it, append it.
|
|
157
|
+
if (!newConfig.systemPrompt) {
|
|
158
|
+
newConfig.systemPrompt = workspaceLine;
|
|
159
|
+
}
|
|
160
|
+
else if (!newConfig.systemPrompt.includes('Current Workspace:')) {
|
|
161
|
+
newConfig.systemPrompt += `\n${workspaceLine}`;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// It has it, let's update it to match new workspace
|
|
165
|
+
// We use a regex to replace the line
|
|
166
|
+
newConfig.systemPrompt = newConfig.systemPrompt.replace(/Current Workspace: .*/, workspaceLine);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let restartScheduler = false;
|
|
170
|
+
if (memoryMaintenanceCron !== undefined) {
|
|
171
|
+
if (!newConfig.scheduler)
|
|
172
|
+
newConfig.scheduler = {};
|
|
173
|
+
if (newConfig.scheduler.memoryMaintenanceCron !== memoryMaintenanceCron) {
|
|
174
|
+
newConfig.scheduler.memoryMaintenanceCron = memoryMaintenanceCron;
|
|
175
|
+
restartScheduler = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 保存到文件
|
|
179
|
+
saveConfig(newConfig);
|
|
180
|
+
// 如果 LLM 配置变了,更新 Brain
|
|
181
|
+
brain.updateClient();
|
|
182
|
+
// 如果定时任务配置变了,重启 Scheduler
|
|
183
|
+
if (restartScheduler) {
|
|
184
|
+
initScheduler();
|
|
185
|
+
}
|
|
186
|
+
res.json({ status: 'success', message: 'Config updated and saved to config.json' });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
res.status(500).json({ status: 'error', message: error.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// 手动触发记忆维护
|
|
193
|
+
app.post('/api/scheduler/run', async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
runMaintenanceNow();
|
|
196
|
+
res.json({ status: 'success', message: 'Memory maintenance triggered.' });
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
res.status(500).json({ status: 'error', message: e.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
import { ReadWordSkill, CreateWordSkill } from './skills/office-word.js';
|
|
203
|
+
import { ReadExcelSkill, CreateExcelSkill } from './skills/office-excel.js';
|
|
204
|
+
import { CreatePptxSkill } from './skills/office-ppt.js';
|
|
205
|
+
import { CreateAgentSkill } from './skills/create-agent.js';
|
|
206
|
+
import { ListAgentsSkill } from './skills/list-agents.js';
|
|
207
|
+
// 初始化 Agents
|
|
208
|
+
agentManager.registerAgent(createOfficeAgent());
|
|
209
|
+
// 注册技能
|
|
210
|
+
skillRegistry.register(new CreateAgentSkill()); // 允许动态创建 Agent
|
|
211
|
+
skillRegistry.register(new ListAgentsSkill()); // 允许列出所有 Agent
|
|
212
|
+
skillRegistry.register(new DelegateSkill()); // 允许主 Brain 委派任务
|
|
213
|
+
skillRegistry.register(new SystemTimeSkill());
|
|
214
|
+
skillRegistry.register(new SearchSkill());
|
|
215
|
+
skillRegistry.register(new ListDirectorySkill());
|
|
216
|
+
skillRegistry.register(new ReadFileSkill());
|
|
217
|
+
skillRegistry.register(new WriteFileSkill());
|
|
218
|
+
skillRegistry.register(new DeleteFileSkill());
|
|
219
|
+
skillRegistry.register(new ReadWordSkill());
|
|
220
|
+
skillRegistry.register(new CreateWordSkill());
|
|
221
|
+
skillRegistry.register(new ReadExcelSkill());
|
|
222
|
+
skillRegistry.register(new CreateExcelSkill());
|
|
223
|
+
skillRegistry.register(new CreatePptxSkill());
|
|
224
|
+
// 初始化插件管理器
|
|
225
|
+
const pluginManager = new PluginManager(path.resolve(process.cwd(), 'plugins'));
|
|
226
|
+
await pluginManager.loadPlugins();
|
|
227
|
+
// 初始化通道
|
|
228
|
+
const channels = createChannels({ app, io, config, pluginManager });
|
|
229
|
+
// 启动通道并绑定事件
|
|
230
|
+
channels.forEach(channel => {
|
|
231
|
+
channel.start();
|
|
232
|
+
// 监听通道消息 -> 发送到 EventBus
|
|
233
|
+
channel.onMessage((sessionId, message) => {
|
|
234
|
+
eventBus.emitEvent({
|
|
235
|
+
type: 'message',
|
|
236
|
+
payload: { content: message },
|
|
237
|
+
channel: channel.name,
|
|
238
|
+
sessionId: sessionId,
|
|
239
|
+
timestamp: Date.now()
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// 核心逻辑:监听 EventBus 消息 -> Brain 处理 -> 发回 EventBus
|
|
244
|
+
eventBus.onEvent('message', async (event) => {
|
|
245
|
+
const { sessionId, payload, channel: channelName } = event;
|
|
246
|
+
const content = payload.content;
|
|
247
|
+
try {
|
|
248
|
+
// 1. 获取会话上下文
|
|
249
|
+
let session = await memory.getSession(sessionId);
|
|
250
|
+
if (!session) {
|
|
251
|
+
session = await memory.createSession();
|
|
252
|
+
// 如果是新创建的会话,可能需要通知前端更新 ID(略复杂,暂且假设前端已获取 ID)
|
|
253
|
+
}
|
|
254
|
+
// 2. 记录用户消息
|
|
255
|
+
await memory.addMessage(sessionId, { role: 'user', content });
|
|
256
|
+
// 3. Brain 处理
|
|
257
|
+
// Use MemoryManager to retrieve full context (Short-term + Recent + Structured)
|
|
258
|
+
const context = await memory.getRelevantContext(content, sessionId);
|
|
259
|
+
// We pass the raw messages to processMessage for now, but Brain should ideally use the enhanced context.
|
|
260
|
+
// Let's modify Brain.processMessage to accept an optional context string or just prepend it.
|
|
261
|
+
// For this MVP, we will prepend the context to the system prompt or as a new system message.
|
|
262
|
+
// Or simpler: We just pass session.messages as before, but Brain needs to know about the context.
|
|
263
|
+
// Let's change how we call brain.processMessage.
|
|
264
|
+
// Construct a temporary history with enhanced context for the LLM
|
|
265
|
+
const enhancedSystemPrompt = `You are xiaozuoAssistant, a helpful AI assistant.
|
|
266
|
+
Here is some relevant context from your memory:
|
|
267
|
+
${context}`;
|
|
268
|
+
// We need to inject this into the Brain.
|
|
269
|
+
// Since Brain.processMessage takes history, let's just pass the history but we need to tell Brain to use this system prompt.
|
|
270
|
+
// We can modify Brain to accept a system prompt override.
|
|
271
|
+
// Or we can just create a new method in Brain or pass it as an argument.
|
|
272
|
+
// Let's update Brain.processMessage signature in next step. For now, let's just pass the history.
|
|
273
|
+
const responseContent = await brain.processMessage(session.messages, content, enhancedSystemPrompt);
|
|
274
|
+
// 4. 记录 AI 响应
|
|
275
|
+
await memory.addMessage(sessionId, { role: 'assistant', content: responseContent });
|
|
276
|
+
// 5. 发送响应事件
|
|
277
|
+
eventBus.emitEvent({
|
|
278
|
+
type: 'response',
|
|
279
|
+
payload: { content: responseContent },
|
|
280
|
+
channel: channelName,
|
|
281
|
+
sessionId: sessionId,
|
|
282
|
+
timestamp: Date.now()
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error('Error processing message:', error);
|
|
287
|
+
eventBus.emitEvent({
|
|
288
|
+
type: 'error',
|
|
289
|
+
payload: { error: error.message },
|
|
290
|
+
channel: channelName,
|
|
291
|
+
sessionId: sessionId,
|
|
292
|
+
timestamp: Date.now()
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
// 监听 EventBus 响应 -> 发送给通道
|
|
297
|
+
eventBus.onEvent('response', (event) => {
|
|
298
|
+
const { sessionId, payload, channel: channelName } = event;
|
|
299
|
+
const channel = channels.find(c => c.name === channelName);
|
|
300
|
+
if (channel) {
|
|
301
|
+
channel.send(sessionId, payload.content);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
eventBus.onEvent('error', (event) => {
|
|
305
|
+
const { sessionId, payload, channel: channelName } = event;
|
|
306
|
+
const channel = channels.find(c => c.name === channelName);
|
|
307
|
+
if (channel) {
|
|
308
|
+
channel.send(sessionId, `Error: ${payload.error}`);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// 启动记忆维护任务(自动清理过期记忆)
|
|
312
|
+
initScheduler();
|
|
313
|
+
// 启动服务器
|
|
314
|
+
const PORT = config.server.port;
|
|
315
|
+
httpServer.listen(PORT, () => {
|
|
316
|
+
console.log(`Server running on port ${PORT}`);
|
|
317
|
+
console.log(`Socket.IO server running`);
|
|
318
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
export function resolveBaseURL(config) {
|
|
3
|
+
switch (config.provider) {
|
|
4
|
+
case 'deepseek':
|
|
5
|
+
return 'https://api.deepseek.com';
|
|
6
|
+
case 'minimax':
|
|
7
|
+
return 'https://api.minimax.chat/v1';
|
|
8
|
+
case 'doubao':
|
|
9
|
+
return 'https://ark.cn-beijing.volces.com/api/v3';
|
|
10
|
+
case 'qwen':
|
|
11
|
+
return 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1';
|
|
12
|
+
case 'custom':
|
|
13
|
+
case 'openai':
|
|
14
|
+
default:
|
|
15
|
+
return config.baseURL;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createOpenAIClient(config) {
|
|
19
|
+
return new OpenAI({
|
|
20
|
+
apiKey: config.apiKey || '',
|
|
21
|
+
baseURL: resolveBaseURL(config)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is a user authentication API route demo.
|
|
3
|
+
* Handle user registration, login, token management, etc.
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
const router = Router();
|
|
7
|
+
/**
|
|
8
|
+
* User Login
|
|
9
|
+
* POST /api/auth/register
|
|
10
|
+
*/
|
|
11
|
+
router.post('/register', async (req, res) => {
|
|
12
|
+
// TODO: Implement register logic
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* User Login
|
|
16
|
+
* POST /api/auth/login
|
|
17
|
+
*/
|
|
18
|
+
router.post('/login', async (req, res) => {
|
|
19
|
+
// TODO: Implement login logic
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* User Logout
|
|
23
|
+
* POST /api/auth/logout
|
|
24
|
+
*/
|
|
25
|
+
router.post('/logout', async (req, res) => {
|
|
26
|
+
// TODO: Implement logout logic
|
|
27
|
+
});
|
|
28
|
+
export default router;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { Server } from 'socket.io';
|
|
4
|
+
import cors from 'cors';
|
|
5
|
+
export function createHttpStack() {
|
|
6
|
+
const app = express();
|
|
7
|
+
const httpServer = createServer(app);
|
|
8
|
+
const io = new Server(httpServer, {
|
|
9
|
+
cors: {
|
|
10
|
+
origin: '*',
|
|
11
|
+
methods: ['GET', 'POST']
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
app.use(cors());
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
return { app, httpServer, io };
|
|
17
|
+
}
|