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,13 @@
|
|
|
1
|
+
export class BaseChannel {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.messageHandler = null;
|
|
4
|
+
}
|
|
5
|
+
onMessage(handler) {
|
|
6
|
+
this.messageHandler = handler;
|
|
7
|
+
}
|
|
8
|
+
emitMessage(sessionId, message) {
|
|
9
|
+
if (this.messageHandler) {
|
|
10
|
+
this.messageHandler(sessionId, message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TerminalChannel } from './terminal.js';
|
|
2
|
+
import { WebChannel } from './web.js';
|
|
3
|
+
import { TelegramChannel } from './telegram.js';
|
|
4
|
+
import { FeishuChannel } from './feishu.js';
|
|
5
|
+
import { DingTalkChannel } from './dingtalk.js';
|
|
6
|
+
import { WechatChannel } from './wechat.js';
|
|
7
|
+
export function createChannels({ app, io, config, pluginManager }) {
|
|
8
|
+
const channels = [
|
|
9
|
+
new TerminalChannel(),
|
|
10
|
+
new WebChannel(io),
|
|
11
|
+
...(config.channels?.telegram?.token ? [new TelegramChannel()] : []),
|
|
12
|
+
...(config.channels?.feishu?.appId ? [new FeishuChannel(app)] : []),
|
|
13
|
+
...(config.channels?.dingtalk?.clientId ? [new DingTalkChannel(app)] : []),
|
|
14
|
+
...(config.channels?.wechat?.enabled ? [new WechatChannel()] : []),
|
|
15
|
+
...pluginManager.getChannels()
|
|
16
|
+
];
|
|
17
|
+
return channels;
|
|
18
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BaseChannel } from './base-channel.js';
|
|
2
|
+
import { config } from '../config/loader.js';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
export class DingTalkChannel extends BaseChannel {
|
|
5
|
+
constructor(app) {
|
|
6
|
+
super();
|
|
7
|
+
this.app = app;
|
|
8
|
+
this.name = 'dingtalk';
|
|
9
|
+
this.accessToken = '';
|
|
10
|
+
this.tokenExpire = 0;
|
|
11
|
+
this.clientId = config.channels?.dingtalk?.clientId;
|
|
12
|
+
this.clientSecret = config.channels?.dingtalk?.clientSecret;
|
|
13
|
+
}
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.clientId || !this.clientSecret) {
|
|
16
|
+
console.log('DingTalk credentials not configured, skipping DingTalk channel.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Register webhook route
|
|
20
|
+
this.app.post('/api/channels/dingtalk/event', async (req, res) => {
|
|
21
|
+
// DingTalk sends messages here
|
|
22
|
+
// Note: Actual implementation depends on DingTalk's specific event format (Stream mode vs HTTP)
|
|
23
|
+
// For simplicity, we assume standard HTTP callback for enterprise internal apps
|
|
24
|
+
const { msgtype, text, conversationId, senderId } = req.body;
|
|
25
|
+
if (msgtype === 'text' && text?.content) {
|
|
26
|
+
const sessionId = `dingtalk:${conversationId || senderId}`;
|
|
27
|
+
// Remove @Bot prefix if present
|
|
28
|
+
const content = text.content.trim();
|
|
29
|
+
this.emitMessage(sessionId, content);
|
|
30
|
+
}
|
|
31
|
+
res.json({ msg: 'success' });
|
|
32
|
+
});
|
|
33
|
+
console.log('DingTalk Channel Started (Webhook at /api/channels/dingtalk/event)');
|
|
34
|
+
}
|
|
35
|
+
async stop() {
|
|
36
|
+
}
|
|
37
|
+
async getAccessToken() {
|
|
38
|
+
if (this.accessToken && Date.now() < this.tokenExpire) {
|
|
39
|
+
return this.accessToken;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const res = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
|
|
43
|
+
appKey: this.clientId,
|
|
44
|
+
appSecret: this.clientSecret
|
|
45
|
+
});
|
|
46
|
+
if (res.data.accessToken) {
|
|
47
|
+
this.accessToken = res.data.accessToken;
|
|
48
|
+
this.tokenExpire = Date.now() + (res.data.expireIn - 60) * 1000;
|
|
49
|
+
return this.accessToken;
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error('Error getting DingTalk token:', error);
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async send(sessionId, message) {
|
|
59
|
+
if (!sessionId.startsWith('dingtalk:'))
|
|
60
|
+
return;
|
|
61
|
+
// For DingTalk, sending back usually requires knowing the conversation ID or using the Robot Webhook if it's a group
|
|
62
|
+
// This is a simplified implementation for Enterprise Internal Robot
|
|
63
|
+
const chatId = sessionId.split(':')[1];
|
|
64
|
+
const token = await this.getAccessToken();
|
|
65
|
+
if (!token)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
await axios.post('https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend', {
|
|
69
|
+
robotCode: config.channels?.dingtalk?.robotCode,
|
|
70
|
+
userIds: [chatId], // Assuming sessionId is userId for direct messages
|
|
71
|
+
msgKey: 'sampleText',
|
|
72
|
+
msgParam: JSON.stringify({ content: message })
|
|
73
|
+
}, {
|
|
74
|
+
headers: {
|
|
75
|
+
'x-acs-dingtalk-access-token': token
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Failed to send DingTalk message:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { BaseChannel } from './base-channel.js';
|
|
2
|
+
import { config } from '../config/loader.js';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
// Note: Feishu requires a public endpoint for Webhooks.
|
|
5
|
+
// In development, you can use ngrok or similar tools.
|
|
6
|
+
// The endpoint will be mounted on the main express app at /api/channels/feishu
|
|
7
|
+
export class FeishuChannel extends BaseChannel {
|
|
8
|
+
constructor(app) {
|
|
9
|
+
super();
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.name = 'feishu';
|
|
12
|
+
this.tenantAccessToken = '';
|
|
13
|
+
this.tokenExpire = 0;
|
|
14
|
+
this.appId = config.channels?.feishu?.appId;
|
|
15
|
+
this.appSecret = config.channels?.feishu?.appSecret;
|
|
16
|
+
}
|
|
17
|
+
async start() {
|
|
18
|
+
if (!this.appId || !this.appSecret) {
|
|
19
|
+
console.log('Feishu credentials not configured, skipping Feishu channel.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Register webhook route
|
|
23
|
+
this.app.post('/api/channels/feishu/event', async (req, res) => {
|
|
24
|
+
const { challenge, type } = req.body;
|
|
25
|
+
// URL Verification
|
|
26
|
+
if (type === 'url_verification') {
|
|
27
|
+
res.json({ challenge });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Handle Message Event (v2.0)
|
|
31
|
+
if (req.body.header?.event_type === 'im.message.receive_v1') {
|
|
32
|
+
const event = req.body.event;
|
|
33
|
+
const message = event.message;
|
|
34
|
+
const sender = event.sender;
|
|
35
|
+
// Ignore messages from bots to prevent loops
|
|
36
|
+
if (sender.sender_type === 'user') {
|
|
37
|
+
const content = JSON.parse(message.content).text;
|
|
38
|
+
const sessionId = `feishu:${message.chat_id}`; // Use chat_id as session
|
|
39
|
+
this.emitMessage(sessionId, content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
res.json({ msg: 'ok' });
|
|
43
|
+
});
|
|
44
|
+
console.log('Feishu Channel Started (Webhook at /api/channels/feishu/event)');
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
// No explicit stop needed for webhook
|
|
48
|
+
}
|
|
49
|
+
async getTenantAccessToken() {
|
|
50
|
+
if (this.tenantAccessToken && Date.now() < this.tokenExpire) {
|
|
51
|
+
return this.tenantAccessToken;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const res = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
|
|
55
|
+
app_id: this.appId,
|
|
56
|
+
app_secret: this.appSecret
|
|
57
|
+
});
|
|
58
|
+
if (res.data.code === 0) {
|
|
59
|
+
this.tenantAccessToken = res.data.tenant_access_token;
|
|
60
|
+
this.tokenExpire = Date.now() + (res.data.expire - 60) * 1000;
|
|
61
|
+
return this.tenantAccessToken;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.error('Failed to get Feishu access token:', res.data);
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('Error fetching Feishu token:', error);
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async send(sessionId, message) {
|
|
74
|
+
if (!sessionId.startsWith('feishu:'))
|
|
75
|
+
return;
|
|
76
|
+
const chatId = sessionId.split(':')[1];
|
|
77
|
+
const token = await this.getTenantAccessToken();
|
|
78
|
+
if (!token)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
await axios.post(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id`, {
|
|
82
|
+
receive_id: chatId,
|
|
83
|
+
msg_type: 'text',
|
|
84
|
+
content: JSON.stringify({ text: message })
|
|
85
|
+
}, {
|
|
86
|
+
headers: {
|
|
87
|
+
Authorization: `Bearer ${token}`
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error(`Failed to send Feishu message to ${chatId}:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BaseChannel } from './base-channel.js';
|
|
2
|
+
import { Telegraf } from 'telegraf';
|
|
3
|
+
import { config } from '../config/loader.js';
|
|
4
|
+
export class TelegramChannel extends BaseChannel {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this.name = 'telegram';
|
|
8
|
+
this.bot = null;
|
|
9
|
+
}
|
|
10
|
+
async start() {
|
|
11
|
+
const token = config.channels?.telegram?.token;
|
|
12
|
+
if (!token) {
|
|
13
|
+
console.log('Telegram token not configured, skipping Telegram channel.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
this.bot = new Telegraf(token);
|
|
18
|
+
this.bot.on('text', (ctx) => {
|
|
19
|
+
// Use chat ID as session ID for simple mapping
|
|
20
|
+
const sessionId = `telegram:${ctx.chat.id}`;
|
|
21
|
+
const message = ctx.message.text;
|
|
22
|
+
// Emit message to EventBus
|
|
23
|
+
this.emitMessage(sessionId, message);
|
|
24
|
+
});
|
|
25
|
+
// Launch the bot (polling mode)
|
|
26
|
+
this.bot.launch(() => {
|
|
27
|
+
console.log('Telegram Channel Started (Polling)');
|
|
28
|
+
});
|
|
29
|
+
// Graceful stop
|
|
30
|
+
process.once('SIGINT', () => this.bot?.stop('SIGINT'));
|
|
31
|
+
process.once('SIGTERM', () => this.bot?.stop('SIGTERM'));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('Failed to start Telegram channel:', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
if (this.bot) {
|
|
39
|
+
this.bot.stop();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
send(sessionId, message) {
|
|
43
|
+
if (!this.bot)
|
|
44
|
+
return;
|
|
45
|
+
// Parse session ID to get chat ID
|
|
46
|
+
if (sessionId.startsWith('telegram:')) {
|
|
47
|
+
const chatId = sessionId.split(':')[1];
|
|
48
|
+
this.bot.telegram.sendMessage(chatId, message).catch(err => {
|
|
49
|
+
console.error(`Failed to send Telegram message to ${chatId}:`, err);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
export class TerminalChannel {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.name = 'terminal';
|
|
5
|
+
this.rl = null;
|
|
6
|
+
this.messageHandler = null;
|
|
7
|
+
this.currentSessionId = 'terminal-session'; // Simple fixed session for terminal
|
|
8
|
+
}
|
|
9
|
+
async start() {
|
|
10
|
+
if (!process.stdin.isTTY) {
|
|
11
|
+
console.log('Terminal channel disabled (non-interactive session)');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
this.rl = readline.createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
prompt: 'xiaozuoAssistant> '
|
|
18
|
+
});
|
|
19
|
+
this.rl.prompt();
|
|
20
|
+
this.rl.on('line', (line) => {
|
|
21
|
+
const input = line.trim();
|
|
22
|
+
if (input && this.messageHandler) {
|
|
23
|
+
this.messageHandler(this.currentSessionId, input);
|
|
24
|
+
}
|
|
25
|
+
// Note: prompt will be called after response is received
|
|
26
|
+
});
|
|
27
|
+
this.rl.on('close', () => {
|
|
28
|
+
console.log('Terminal session closed.');
|
|
29
|
+
});
|
|
30
|
+
console.log('Terminal Channel Started');
|
|
31
|
+
}
|
|
32
|
+
async stop() {
|
|
33
|
+
if (this.rl) {
|
|
34
|
+
this.rl.close();
|
|
35
|
+
this.rl = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
send(sessionId, message) {
|
|
39
|
+
if (sessionId === this.currentSessionId) {
|
|
40
|
+
console.log(message);
|
|
41
|
+
if (this.rl) {
|
|
42
|
+
this.rl.prompt();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onMessage(handler) {
|
|
47
|
+
this.messageHandler = handler;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class WebChannel {
|
|
2
|
+
constructor(io) {
|
|
3
|
+
this.name = 'web';
|
|
4
|
+
this.messageHandler = null;
|
|
5
|
+
this.io = io;
|
|
6
|
+
}
|
|
7
|
+
async start() {
|
|
8
|
+
this.io.on('connection', (socket) => {
|
|
9
|
+
console.log(`Web client connected: ${socket.id}`);
|
|
10
|
+
// 当客户端加入会话房间
|
|
11
|
+
socket.on('join_session', (sessionId) => {
|
|
12
|
+
socket.join(sessionId);
|
|
13
|
+
console.log(`Socket ${socket.id} joined session ${sessionId}`);
|
|
14
|
+
});
|
|
15
|
+
// 当客户端发送消息
|
|
16
|
+
socket.on('message', (data) => {
|
|
17
|
+
console.log(`[WebChannel] Received message from ${data.sessionId}: ${data.content}`);
|
|
18
|
+
if (this.messageHandler) {
|
|
19
|
+
this.messageHandler(data.sessionId, data.content);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
socket.on('disconnect', () => {
|
|
23
|
+
console.log(`Web client disconnected: ${socket.id}`);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
console.log('Web Channel Started');
|
|
27
|
+
}
|
|
28
|
+
async stop() {
|
|
29
|
+
// Socket.io handles close with the http server,
|
|
30
|
+
// but we can disconnect all sockets if needed
|
|
31
|
+
this.io.disconnectSockets();
|
|
32
|
+
}
|
|
33
|
+
send(sessionId, message) {
|
|
34
|
+
console.log(`[WebChannel] Sending message to ${sessionId}: ${message}`);
|
|
35
|
+
// 发送消息到特定的会话房间
|
|
36
|
+
this.io.to(sessionId).emit('message', {
|
|
37
|
+
role: 'assistant',
|
|
38
|
+
content: message,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
onMessage(handler) {
|
|
43
|
+
this.messageHandler = handler;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { WechatyBuilder, ScanStatus } from 'wechaty';
|
|
2
|
+
import qrcodeTerminal from 'qrcode-terminal';
|
|
3
|
+
import { BaseChannel } from './base-channel.js';
|
|
4
|
+
import { config } from '../config/loader.js';
|
|
5
|
+
export class WechatChannel extends BaseChannel {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.name = 'wechat';
|
|
9
|
+
this.isReady = false;
|
|
10
|
+
const options = {
|
|
11
|
+
name: 'xiaozuo-bot',
|
|
12
|
+
};
|
|
13
|
+
if (config.channels?.wechat?.puppet) {
|
|
14
|
+
options.puppet = config.channels.wechat.puppet;
|
|
15
|
+
}
|
|
16
|
+
if (config.channels?.wechat?.token) {
|
|
17
|
+
options.puppetOptions = {
|
|
18
|
+
token: config.channels.wechat.token,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
this.bot = WechatyBuilder.build(options);
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
this.bot
|
|
25
|
+
.on('scan', (qrcode, status) => {
|
|
26
|
+
if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) {
|
|
27
|
+
console.log(`[WeChat] Scan QR Code to login: ${status}\nhttps://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`);
|
|
28
|
+
qrcodeTerminal.generate(qrcode, { small: true });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`[WeChat] Scan status: ${status}`);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.on('login', (user) => {
|
|
35
|
+
console.log(`[WeChat] User ${user} logged in`);
|
|
36
|
+
this.isReady = true;
|
|
37
|
+
})
|
|
38
|
+
.on('message', async (message) => {
|
|
39
|
+
if (message.self())
|
|
40
|
+
return;
|
|
41
|
+
// Only handle text messages for now
|
|
42
|
+
if (message.type() !== this.bot.Message.Type.Text)
|
|
43
|
+
return;
|
|
44
|
+
const room = message.room();
|
|
45
|
+
const talker = message.talker();
|
|
46
|
+
const text = message.text();
|
|
47
|
+
// If in a room, maybe only respond if mentioned?
|
|
48
|
+
// For simplicity, let's respond to everything in direct messages,
|
|
49
|
+
// and only mentions in rooms if possible, or everything for now (can be noisy).
|
|
50
|
+
// Let's implement mention check for rooms.
|
|
51
|
+
if (room) {
|
|
52
|
+
if (await message.mentionSelf()) {
|
|
53
|
+
// Remove the mention part from text if needed, or just pass it.
|
|
54
|
+
// Wechaty usually keeps the text as is.
|
|
55
|
+
// We use room.id as sessionId for room messages
|
|
56
|
+
console.log(`[WeChat] Room Message from ${talker.name()} in ${await room.topic()}: ${text}`);
|
|
57
|
+
this.emitMessage(room.id, text.replace(/^@\w+\s+/, '')); // Simple strip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Direct message
|
|
62
|
+
console.log(`[WeChat] Direct Message from ${talker.name()}: ${text}`);
|
|
63
|
+
// Use talker.id as sessionId
|
|
64
|
+
this.emitMessage(talker.id, text);
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.on('logout', (user) => {
|
|
68
|
+
console.log(`[WeChat] User ${user} logged out`);
|
|
69
|
+
this.isReady = false;
|
|
70
|
+
})
|
|
71
|
+
.on('error', (e) => {
|
|
72
|
+
console.error('[WeChat] Error:', e);
|
|
73
|
+
});
|
|
74
|
+
await this.bot.start();
|
|
75
|
+
console.log('[WeChat] Bot started');
|
|
76
|
+
}
|
|
77
|
+
async stop() {
|
|
78
|
+
if (this.bot) {
|
|
79
|
+
await this.bot.stop();
|
|
80
|
+
console.log('[WeChat] Bot stopped');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async send(sessionId, message) {
|
|
84
|
+
if (!this.isReady) {
|
|
85
|
+
console.warn('[WeChat] Bot not ready, cannot send message');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
// Try to find room first
|
|
90
|
+
const room = await this.bot.Room.find({ id: sessionId });
|
|
91
|
+
if (room) {
|
|
92
|
+
await room.say(message);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Try to find contact
|
|
96
|
+
const contact = await this.bot.Contact.find({ id: sessionId });
|
|
97
|
+
if (contact) {
|
|
98
|
+
await contact.say(message);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
console.warn(`[WeChat] Could not find contact or room with ID: ${sessionId}`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`[WeChat] Failed to send message to ${sessionId}:`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { SYSTEM_PROMPT } from './prompts.js';
|
|
4
|
+
const configPath = path.resolve(process.cwd(), 'config.json');
|
|
5
|
+
let loadedConfig;
|
|
6
|
+
try {
|
|
7
|
+
const fileContent = fs.readFileSync(configPath, 'utf-8');
|
|
8
|
+
loadedConfig = JSON.parse(fileContent);
|
|
9
|
+
// Set defaults if missing
|
|
10
|
+
if (!loadedConfig.systemPrompt)
|
|
11
|
+
loadedConfig.systemPrompt = SYSTEM_PROMPT;
|
|
12
|
+
if (!loadedConfig.scheduler)
|
|
13
|
+
loadedConfig.scheduler = { memoryMaintenanceCron: '0 0 * * *' };
|
|
14
|
+
if (!loadedConfig.workspace)
|
|
15
|
+
loadedConfig.workspace = process.cwd();
|
|
16
|
+
// Ensure LLM config exists
|
|
17
|
+
if (!loadedConfig.llm) {
|
|
18
|
+
loadedConfig.llm = {
|
|
19
|
+
provider: 'qwen',
|
|
20
|
+
apiKey: '',
|
|
21
|
+
baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
|
22
|
+
model: 'qwen-plus',
|
|
23
|
+
temperature: 0.7
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
if (!loadedConfig.llm.apiKey)
|
|
28
|
+
loadedConfig.llm.apiKey = '';
|
|
29
|
+
}
|
|
30
|
+
// Override with env vars if present (optional, but good for security)
|
|
31
|
+
if (process.env.XIAOZUO_LLM_API_KEY)
|
|
32
|
+
loadedConfig.llm.apiKey = process.env.XIAOZUO_LLM_API_KEY;
|
|
33
|
+
// Log the loaded API key (masked)
|
|
34
|
+
console.log('[Config] API Key Status:', loadedConfig.llm.apiKey ? 'Configured' : 'Not configured (Empty)');
|
|
35
|
+
console.log('[Config] Base URL:', loadedConfig.llm.baseURL);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn('Failed to load config.json, creating a new one with defaults');
|
|
39
|
+
loadedConfig = {
|
|
40
|
+
server: { port: 3001, host: 'localhost' },
|
|
41
|
+
llm: {
|
|
42
|
+
provider: 'qwen',
|
|
43
|
+
apiKey: '',
|
|
44
|
+
baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
|
45
|
+
model: 'qwen-plus',
|
|
46
|
+
temperature: 0.7
|
|
47
|
+
},
|
|
48
|
+
logging: { level: 'info' },
|
|
49
|
+
scheduler: { memoryMaintenanceCron: '0 0 * * *' },
|
|
50
|
+
workspace: process.cwd(),
|
|
51
|
+
systemPrompt: SYSTEM_PROMPT
|
|
52
|
+
};
|
|
53
|
+
// Auto-create config.json if it doesn't exist
|
|
54
|
+
try {
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify(loadedConfig, null, 2), 'utf-8');
|
|
56
|
+
console.log(`[Config] Created new config.json at ${configPath}`);
|
|
57
|
+
}
|
|
58
|
+
catch (writeError) {
|
|
59
|
+
console.error('[Config] Failed to create default config.json', writeError);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const config = loadedConfig;
|
|
63
|
+
export const saveConfig = (newConfig) => {
|
|
64
|
+
try {
|
|
65
|
+
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf-8');
|
|
66
|
+
// Update memory
|
|
67
|
+
Object.assign(config, newConfig);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error('Failed to save config.json', error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const SYSTEM_PROMPT = `
|
|
2
|
+
你现在是 我的个人AI助手,叫xiaozuoAssistant。
|
|
3
|
+
|
|
4
|
+
核心规则:
|
|
5
|
+
1. 所有记忆完全本地存储,不上传任何内容到云端。
|
|
6
|
+
2. 默认工作模式:优先使用 office_work 类别的记忆。
|
|
7
|
+
3. 当用户提到 Word、PPT、Excel、汇报、客户、模板、格式、数据整理、邮件等办公相关内容时,**必须先检索相关记忆**,然后再回答。
|
|
8
|
+
4. 如果记忆中有明确偏好,直接应用,不要再次询问。
|
|
9
|
+
5. 风格:专业、简洁、高效,像一个靠谱的行政/助理。
|
|
10
|
+
6. 语言:默认用中文,必要时中英混用(比如函数名、文件名)。
|
|
11
|
+
7. 主动建议:如果用户在处理重复性工作,可以提醒“您上次处理类似内容时用了XX方法,要不要继续沿用?”
|
|
12
|
+
`.trim();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class AgentManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.agents = new Map();
|
|
4
|
+
}
|
|
5
|
+
static getInstance() {
|
|
6
|
+
if (!AgentManager.instance) {
|
|
7
|
+
AgentManager.instance = new AgentManager();
|
|
8
|
+
}
|
|
9
|
+
return AgentManager.instance;
|
|
10
|
+
}
|
|
11
|
+
registerAgent(agent) {
|
|
12
|
+
this.agents.set(agent.name, agent);
|
|
13
|
+
console.log(`[AgentManager] Registered agent: ${agent.name}`);
|
|
14
|
+
}
|
|
15
|
+
getAgent(name) {
|
|
16
|
+
return this.agents.get(name);
|
|
17
|
+
}
|
|
18
|
+
getAllAgents() {
|
|
19
|
+
return Array.from(this.agents.values());
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const agentManager = AgentManager.getInstance();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { config } from '../../config/loader.js';
|
|
2
|
+
import { createOpenAIClient } from '../../llm/openai.js';
|
|
3
|
+
export class AgentRuntime {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.openai = createOpenAIClient(config.llm);
|
|
6
|
+
this.name = options.name;
|
|
7
|
+
this.description = options.description;
|
|
8
|
+
this.systemPrompt = options.systemPrompt;
|
|
9
|
+
this.skills = options.skills || [];
|
|
10
|
+
this.model = options.model || config.llm.model;
|
|
11
|
+
this.temperature = options.temperature || config.llm.temperature;
|
|
12
|
+
this.openai = createOpenAIClient(config.llm);
|
|
13
|
+
}
|
|
14
|
+
updateConfig() {
|
|
15
|
+
this.openai = createOpenAIClient(config.llm);
|
|
16
|
+
this.model = config.llm.model;
|
|
17
|
+
this.temperature = config.llm.temperature;
|
|
18
|
+
}
|
|
19
|
+
getToolsDefinition() {
|
|
20
|
+
return this.skills.map(skill => skill.toJSON());
|
|
21
|
+
}
|
|
22
|
+
async process(history, newMessage, contextPrompt) {
|
|
23
|
+
const finalSystemPrompt = contextPrompt
|
|
24
|
+
? `${this.systemPrompt}\n${contextPrompt}`
|
|
25
|
+
: this.systemPrompt;
|
|
26
|
+
const messages = [
|
|
27
|
+
{ role: 'system', content: finalSystemPrompt },
|
|
28
|
+
...history.map(m => ({ role: m.role, content: m.content, name: m.name, tool_call_id: m.tool_call_id, tool_calls: m.tool_calls })),
|
|
29
|
+
{ role: 'user', content: newMessage }
|
|
30
|
+
];
|
|
31
|
+
try {
|
|
32
|
+
console.log(`[Agent:${this.name}] Calling LLM...`);
|
|
33
|
+
let response = await this.callLLM(messages);
|
|
34
|
+
let iterations = 0;
|
|
35
|
+
const MAX_ITERATIONS = 10;
|
|
36
|
+
while (response.choices[0].message.tool_calls && iterations < MAX_ITERATIONS) {
|
|
37
|
+
iterations++;
|
|
38
|
+
const toolCalls = response.choices[0].message.tool_calls;
|
|
39
|
+
messages.push(response.choices[0].message);
|
|
40
|
+
for (const toolCall of toolCalls) {
|
|
41
|
+
if (toolCall.type !== 'function')
|
|
42
|
+
continue;
|
|
43
|
+
const functionName = toolCall.function.name;
|
|
44
|
+
const functionArgs = JSON.parse(toolCall.function.arguments);
|
|
45
|
+
console.log(`[Agent:${this.name}] Executing tool: ${functionName}`);
|
|
46
|
+
const skill = this.skills.find(s => s.name === functionName);
|
|
47
|
+
let toolResult = '';
|
|
48
|
+
if (skill) {
|
|
49
|
+
try {
|
|
50
|
+
const result = await skill.execute(functionArgs);
|
|
51
|
+
toolResult = JSON.stringify(result);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
toolResult = JSON.stringify({ error: error.message });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
toolResult = JSON.stringify({ error: 'Tool not found' });
|
|
59
|
+
}
|
|
60
|
+
messages.push({
|
|
61
|
+
role: 'tool',
|
|
62
|
+
tool_call_id: toolCall.id,
|
|
63
|
+
content: toolResult
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
response = await this.callLLM(messages);
|
|
67
|
+
}
|
|
68
|
+
return response.choices[0].message.content || 'No response generated.';
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(`[Agent:${this.name}] Error:`, error);
|
|
72
|
+
return `Error: ${error.message}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async callLLM(messages) {
|
|
76
|
+
const tools = this.getToolsDefinition();
|
|
77
|
+
const toolsParam = tools.length > 0 ? tools : undefined;
|
|
78
|
+
return await this.openai.chat.completions.create({
|
|
79
|
+
model: this.model,
|
|
80
|
+
messages: messages,
|
|
81
|
+
tools: toolsParam,
|
|
82
|
+
temperature: this.temperature
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|