xiaozuoassistant 0.1.80 → 0.1.82
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 +7 -0
- package/config.json +2 -2
- package/dist/client/assets/{browser-ponyfill-BVTKMYN8.js → browser-ponyfill-DG-Mloyd.js} +1 -1
- package/dist/client/assets/index-BBXxfEv9.css +1 -0
- package/dist/client/assets/index-Cpq9njn6.js +201 -0
- package/dist/client/index.html +2 -2
- package/dist/client/locales/zh/translation.json +2 -2
- package/dist/server/channels/create-channels.js +1 -1
- package/dist/server/channels/feishu.js +55 -64
- package/dist/server/core/brain.js +4 -3
- package/dist/server/core/memories/short-term.js +7 -1
- package/dist/server/core/scheduler.js +66 -11
- package/dist/server/index.js +174 -7
- package/dist/server/skills/base-skill.js +4 -0
- package/dist/server/skills/registry.js +28 -3
- package/package.json +2 -1
- package/public/locales/zh/translation.json +2 -2
- package/dist/client/assets/index-B5Qw98nG.css +0 -1
- package/dist/client/assets/index-B8uwjNT9.js +0 -186
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍇</text></svg>" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>xiaozuoAssistant</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Cpq9njn6.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BBXxfEv9.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"you": "你",
|
|
10
10
|
"assistant": "xiaozuoAssistant",
|
|
11
11
|
"thinking": "思考中...",
|
|
12
|
-
"inputPlaceholder": "给 xiaozuoAssistant
|
|
12
|
+
"inputPlaceholder": "给 xiaozuoAssistant 发送消息 (Cmd/Ctrl + Enter 发送)...",
|
|
13
13
|
"disclaimer": "xiaozuoAssistant 可能会犯错。请核对重要信息。",
|
|
14
14
|
"resetSession": "重置会话",
|
|
15
15
|
"confirmReset": "确定要重置当前会话记忆吗?这将清空所有消息历史。"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tabs": {
|
|
47
47
|
"general": "常规",
|
|
48
48
|
"identity": "用户身份管理",
|
|
49
|
-
"scheduler": "
|
|
49
|
+
"scheduler": "定时任务"
|
|
50
50
|
},
|
|
51
51
|
"general": {
|
|
52
52
|
"provider": "模型提供商",
|
|
@@ -9,7 +9,7 @@ export function createChannels({ app, io, config, pluginManager }) {
|
|
|
9
9
|
new TerminalChannel(),
|
|
10
10
|
new WebChannel(io),
|
|
11
11
|
...(config.channels?.telegram?.token ? [new TelegramChannel()] : []),
|
|
12
|
-
...(config.channels?.feishu?.appId ? [new FeishuChannel(
|
|
12
|
+
...(config.channels?.feishu?.appId ? [new FeishuChannel()] : []),
|
|
13
13
|
...(config.channels?.dingtalk?.clientId ? [new DingTalkChannel(app)] : []),
|
|
14
14
|
...(config.channels?.wechat?.enabled ? [new WechatChannel()] : []),
|
|
15
15
|
...pluginManager.getChannels()
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { BaseChannel } from './base-channel.js';
|
|
2
2
|
import { config } from '../config/loader.js';
|
|
3
|
-
import
|
|
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
|
|
3
|
+
import * as lark from '@larksuiteoapi/node-sdk';
|
|
7
4
|
export class FeishuChannel extends BaseChannel {
|
|
8
|
-
constructor(
|
|
5
|
+
constructor() {
|
|
9
6
|
super();
|
|
10
|
-
this.app = app;
|
|
11
7
|
this.name = 'feishu';
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
8
|
+
this.client = null;
|
|
9
|
+
this.wsClient = null;
|
|
14
10
|
this.appId = config.channels?.feishu?.appId;
|
|
15
11
|
this.appSecret = config.channels?.feishu?.appSecret;
|
|
16
12
|
}
|
|
@@ -19,73 +15,68 @@ export class FeishuChannel extends BaseChannel {
|
|
|
19
15
|
console.log('Feishu credentials not configured, skipping Feishu channel.');
|
|
20
16
|
return;
|
|
21
17
|
}
|
|
22
|
-
//
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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' });
|
|
18
|
+
// Initialize Lark Client for API calls
|
|
19
|
+
this.client = new lark.Client({
|
|
20
|
+
appId: this.appId,
|
|
21
|
+
appSecret: this.appSecret,
|
|
22
|
+
appType: lark.AppType.SelfBuild,
|
|
23
|
+
domain: lark.Domain.Feishu,
|
|
24
|
+
});
|
|
25
|
+
// Initialize WebSocket Client for receiving events
|
|
26
|
+
this.wsClient = new lark.WSClient({
|
|
27
|
+
appId: this.appId,
|
|
28
|
+
appSecret: this.appSecret,
|
|
43
29
|
});
|
|
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
30
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
31
|
+
await this.wsClient.start({
|
|
32
|
+
eventDispatcher: new lark.EventDispatcher({}).register({
|
|
33
|
+
'im.message.receive_v1': async (data) => {
|
|
34
|
+
const event = data.message;
|
|
35
|
+
if (!event || !data.sender)
|
|
36
|
+
return;
|
|
37
|
+
// Ignore bot messages
|
|
38
|
+
if (data.sender.sender_type !== 'user')
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const content = JSON.parse(event.content).text;
|
|
42
|
+
// Use chat_id for group chats, open_id/user_id for p2p?
|
|
43
|
+
// Actually message.chat_id is universal for where the message comes from.
|
|
44
|
+
const sessionId = `feishu:${event.chat_id}`;
|
|
45
|
+
this.emitMessage(sessionId, content);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.error('[Feishu] Failed to parse message content:', e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})
|
|
57
52
|
});
|
|
58
|
-
|
|
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
|
-
}
|
|
53
|
+
console.log('Feishu Channel Started (WebSocket Mode)');
|
|
67
54
|
}
|
|
68
55
|
catch (error) {
|
|
69
|
-
console.error('
|
|
70
|
-
return '';
|
|
56
|
+
console.error('[Feishu] Failed to start WebSocket client:', error);
|
|
71
57
|
}
|
|
72
58
|
}
|
|
59
|
+
async stop() {
|
|
60
|
+
// There is no explicit stop method for WSClient in the current SDK version exposed clearly,
|
|
61
|
+
// but typically it persists until process exit.
|
|
62
|
+
// If needed, we can just set clients to null.
|
|
63
|
+
this.wsClient = null;
|
|
64
|
+
this.client = null;
|
|
65
|
+
}
|
|
73
66
|
async send(sessionId, message) {
|
|
74
|
-
if (!sessionId.startsWith('feishu:'))
|
|
67
|
+
if (!sessionId.startsWith('feishu:') || !this.client)
|
|
75
68
|
return;
|
|
76
69
|
const chatId = sessionId.split(':')[1];
|
|
77
|
-
const token = await this.getTenantAccessToken();
|
|
78
|
-
if (!token)
|
|
79
|
-
return;
|
|
80
70
|
try {
|
|
81
|
-
await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
await this.client.im.message.create({
|
|
72
|
+
params: {
|
|
73
|
+
receive_id_type: 'chat_id',
|
|
74
|
+
},
|
|
75
|
+
data: {
|
|
76
|
+
receive_id: chatId,
|
|
77
|
+
msg_type: 'text',
|
|
78
|
+
content: JSON.stringify({ text: message }),
|
|
79
|
+
},
|
|
89
80
|
});
|
|
90
81
|
}
|
|
91
82
|
catch (error) {
|
|
@@ -62,7 +62,7 @@ export class Brain {
|
|
|
62
62
|
try {
|
|
63
63
|
if (process.env.DEBUG)
|
|
64
64
|
console.log('[Brain] Calling LLM...');
|
|
65
|
-
let response = await this.callLLM(messages);
|
|
65
|
+
let response = await this.callLLM(messages, newMessage);
|
|
66
66
|
// Log only the content snippet to avoid flooding logs with full JSON
|
|
67
67
|
const contentSnippet = response.choices[0].message.content ? response.choices[0].message.content.substring(0, 100) + '...' : 'No content';
|
|
68
68
|
if (process.env.DEBUG)
|
|
@@ -199,8 +199,9 @@ Format: [{"title": "...", "content": "..."}]` },
|
|
|
199
199
|
return [];
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
async callLLM(messages) {
|
|
203
|
-
|
|
202
|
+
async callLLM(messages, userQuery) {
|
|
203
|
+
// 根据用户当前输入,获取过滤后的 Tools
|
|
204
|
+
const tools = skillRegistry.getToolsDefinition(userQuery);
|
|
204
205
|
// OpenAI SDK expects tools to be undefined if empty array, or valid tools array
|
|
205
206
|
const toolsParam = tools.length > 0 ? tools : undefined;
|
|
206
207
|
const maxRetries = config.llm.maxRetries ?? 2;
|
|
@@ -173,7 +173,13 @@ export class ShortTermMemory {
|
|
|
173
173
|
}
|
|
174
174
|
async createSession(input) {
|
|
175
175
|
await this.ensureReady();
|
|
176
|
-
const id = uuidv4();
|
|
176
|
+
const id = input?.id || uuidv4();
|
|
177
|
+
if (this.sessions.has(id)) {
|
|
178
|
+
// If session already exists, return it (idempotent for external channels)
|
|
179
|
+
const session = await this.getSession(id);
|
|
180
|
+
if (session)
|
|
181
|
+
return session;
|
|
182
|
+
}
|
|
177
183
|
console.log(`[ShortTermMemory:${this.instanceId}] Creating session: ${id}`);
|
|
178
184
|
const now = Date.now();
|
|
179
185
|
const meta = {
|
|
@@ -2,29 +2,84 @@ import cron from 'node-cron';
|
|
|
2
2
|
import { memoryManager } from './memories/manager.js';
|
|
3
3
|
import { config } from '../config/loader.js';
|
|
4
4
|
let cronTask = null;
|
|
5
|
+
let archiveTask = null;
|
|
6
|
+
let isMaintenanceRunning = false;
|
|
7
|
+
let isArchiveRunning = false;
|
|
5
8
|
export const initScheduler = () => {
|
|
6
9
|
// Stop existing task if any
|
|
7
10
|
if (cronTask) {
|
|
8
11
|
cronTask.stop();
|
|
9
12
|
cronTask = null;
|
|
10
13
|
}
|
|
14
|
+
if (archiveTask) {
|
|
15
|
+
archiveTask.stop();
|
|
16
|
+
archiveTask = null;
|
|
17
|
+
}
|
|
11
18
|
// Default: Every day at midnight
|
|
12
19
|
const cronSchedule = config.scheduler?.memoryMaintenanceCron || '0 0 * * *';
|
|
13
20
|
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
21
|
cronTask = cron.schedule(cronSchedule, () => {
|
|
18
|
-
|
|
19
|
-
memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
|
|
22
|
+
runMaintenanceNow();
|
|
20
23
|
});
|
|
21
24
|
// Archive session memories every 3 minutes
|
|
22
|
-
cron.schedule('*/3 * * * *', () => {
|
|
23
|
-
|
|
25
|
+
archiveTask = cron.schedule('*/3 * * * *', () => {
|
|
26
|
+
runSessionArchivingNow();
|
|
24
27
|
});
|
|
25
28
|
};
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
export const getSchedulerStatus = () => {
|
|
30
|
+
return {
|
|
31
|
+
maintenance: {
|
|
32
|
+
status: cronTask ? 'running' : 'stopped',
|
|
33
|
+
schedule: config.scheduler?.memoryMaintenanceCron || '0 0 * * *',
|
|
34
|
+
isRunningNow: isMaintenanceRunning
|
|
35
|
+
},
|
|
36
|
+
archiving: {
|
|
37
|
+
status: archiveTask ? 'running' : 'stopped',
|
|
38
|
+
schedule: '*/3 * * * *',
|
|
39
|
+
isRunningNow: isArchiveRunning
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export const startScheduler = () => {
|
|
44
|
+
initScheduler();
|
|
45
|
+
};
|
|
46
|
+
export const stopScheduler = () => {
|
|
47
|
+
if (cronTask) {
|
|
48
|
+
cronTask.stop();
|
|
49
|
+
cronTask = null;
|
|
50
|
+
}
|
|
51
|
+
if (archiveTask) {
|
|
52
|
+
archiveTask.stop();
|
|
53
|
+
archiveTask = null;
|
|
54
|
+
}
|
|
55
|
+
console.log('[Scheduler] All scheduled tasks stopped.');
|
|
56
|
+
};
|
|
57
|
+
export const runMaintenanceNow = async () => {
|
|
58
|
+
if (isMaintenanceRunning) {
|
|
59
|
+
console.log('[Scheduler] Maintenance is already running, skipping.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
isMaintenanceRunning = true;
|
|
63
|
+
try {
|
|
64
|
+
console.log('[Scheduler] Manually triggering maintenance...');
|
|
65
|
+
const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
|
|
66
|
+
await memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
isMaintenanceRunning = false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
export const runSessionArchivingNow = async () => {
|
|
73
|
+
if (isArchiveRunning) {
|
|
74
|
+
console.log('[Scheduler] Archiving is already running, skipping.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
isArchiveRunning = true;
|
|
78
|
+
try {
|
|
79
|
+
console.log('[Scheduler] Manually triggering session archiving...');
|
|
80
|
+
await memoryManager.runSessionArchiving();
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
isArchiveRunning = false;
|
|
84
|
+
}
|
|
30
85
|
};
|
package/dist/server/index.js
CHANGED
|
@@ -9,13 +9,15 @@ import path from 'path';
|
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import fs from 'fs'; // Ensure fs is imported for sync operations like existsSync
|
|
11
11
|
import fsPromises from 'fs/promises'; // For async file operations if needed
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import axios from 'axios';
|
|
12
14
|
import { memory } from './core/memory.js';
|
|
13
15
|
import { skillRegistry } from './skills/registry.js';
|
|
14
16
|
import { SystemTimeSkill } from './skills/system-time.js';
|
|
15
17
|
import { SearchSkill } from './skills/search.js';
|
|
16
18
|
import { ListDirectorySkill, ReadFileSkill, WriteFileSkill, DeleteFileSkill } from './skills/file-system.js';
|
|
17
19
|
import { PluginManager } from './core/plugin-manager.js';
|
|
18
|
-
import { initScheduler, runMaintenanceNow } from './core/scheduler.js';
|
|
20
|
+
import { initScheduler, runMaintenanceNow, getSchedulerStatus, startScheduler, stopScheduler } from './core/scheduler.js';
|
|
19
21
|
import { agentManager } from './core/agents/manager.js';
|
|
20
22
|
import { createOfficeAgent } from './agents/office.js';
|
|
21
23
|
import { DelegateSkill } from './skills/delegate.js';
|
|
@@ -348,17 +350,148 @@ app.post('/api/sessions/:id/archive', async (req, res) => {
|
|
|
348
350
|
res.status(500).json({ status: 'error', message: e.message });
|
|
349
351
|
}
|
|
350
352
|
});
|
|
353
|
+
// 获取定时任务状态
|
|
354
|
+
app.get('/api/scheduler/status', (req, res) => {
|
|
355
|
+
res.json(getSchedulerStatus());
|
|
356
|
+
});
|
|
357
|
+
// 启动定时任务
|
|
358
|
+
app.post('/api/scheduler/start', (req, res) => {
|
|
359
|
+
startScheduler();
|
|
360
|
+
res.json({ status: 'success', message: 'Scheduler started.', data: getSchedulerStatus() });
|
|
361
|
+
});
|
|
362
|
+
// 停止定时任务
|
|
363
|
+
app.post('/api/scheduler/stop', (req, res) => {
|
|
364
|
+
stopScheduler();
|
|
365
|
+
res.json({ status: 'success', message: 'Scheduler stopped.', data: getSchedulerStatus() });
|
|
366
|
+
});
|
|
351
367
|
// 手动触发记忆维护
|
|
352
368
|
app.post('/api/scheduler/run', async (req, res) => {
|
|
353
369
|
try {
|
|
354
|
-
runMaintenanceNow();
|
|
355
|
-
res.json({ status: 'success', message: 'Memory maintenance
|
|
370
|
+
await runMaintenanceNow();
|
|
371
|
+
res.json({ status: 'success', message: 'Memory maintenance completed.' });
|
|
356
372
|
}
|
|
357
373
|
catch (e) {
|
|
358
374
|
res.status(500).json({ status: 'error', message: e.message });
|
|
359
375
|
}
|
|
360
376
|
});
|
|
361
|
-
//
|
|
377
|
+
// 预设第三方插件的元数据
|
|
378
|
+
const thirdPartySkillsMeta = {
|
|
379
|
+
'wps_office': {
|
|
380
|
+
id: 'wps_office',
|
|
381
|
+
name: 'WPS Office 办公助手',
|
|
382
|
+
description: '授权 AI 读取和编辑本地 WPS 文档、表格和演示文稿。',
|
|
383
|
+
enabled: false,
|
|
384
|
+
isBuiltIn: false
|
|
385
|
+
},
|
|
386
|
+
'lark_docs': {
|
|
387
|
+
id: 'lark_docs',
|
|
388
|
+
name: '飞书文档 (Lark Docs) 助手',
|
|
389
|
+
description: '授权 AI 读取您的飞书云文档知识库,并在对话中进行总结和答疑。',
|
|
390
|
+
enabled: false,
|
|
391
|
+
isBuiltIn: false
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
// Skills CRUD & Management
|
|
395
|
+
app.get('/api/skills', (req, res) => {
|
|
396
|
+
try {
|
|
397
|
+
const skills = skillRegistry.getAllSkills().map(s => ({
|
|
398
|
+
id: s.name, // 使用 name 作为唯一标识
|
|
399
|
+
name: s.name,
|
|
400
|
+
description: s.description,
|
|
401
|
+
enabled: s.enabled, // 从 Registry 获取真实状态
|
|
402
|
+
isBuiltIn: true
|
|
403
|
+
}));
|
|
404
|
+
// 添加外部预设插件状态
|
|
405
|
+
skills.push(thirdPartySkillsMeta.wps_office);
|
|
406
|
+
skills.push(thirdPartySkillsMeta.lark_docs);
|
|
407
|
+
res.json(skills);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
res.status(500).json({ error: e.message });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
// 存储 OAuth 状态以防止 CSRF 攻击
|
|
414
|
+
const oauthStates = new Map();
|
|
415
|
+
// 模拟的 Token 存储 (实际应存入 SQLite 或加密文件)
|
|
416
|
+
const oauthTokens = new Map();
|
|
417
|
+
// Mock OAuth 授权接口
|
|
418
|
+
app.get('/api/auth/:provider/authorize', (req, res) => {
|
|
419
|
+
const provider = req.params.provider;
|
|
420
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
421
|
+
oauthStates.set(state, { provider, expiresAt: Date.now() + 10 * 60 * 1000 });
|
|
422
|
+
let authUrl = '';
|
|
423
|
+
const redirectUri = encodeURIComponent(`http://localhost:3001/api/auth/${provider}/callback`);
|
|
424
|
+
if (provider === 'lark') {
|
|
425
|
+
const appId = config.channels?.feishu?.appId || 'cli_a939363208f8dbb4'; // 使用已有配置或默认
|
|
426
|
+
authUrl = `https://open.feishu.cn/open-apis/authen/v1/user_auth_page_beta?app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
|
|
427
|
+
}
|
|
428
|
+
else if (provider === 'wps') {
|
|
429
|
+
// WPS OAuth 2.0 文档参考:https://open.wps.cn/docs/office/auth
|
|
430
|
+
const appId = 'your_wps_app_id'; // 需要在 WPS 开放平台申请
|
|
431
|
+
authUrl = `https://openapi.wps.cn/oauthapi/v2/authorize?response_type=code&app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
return res.status(400).send('Unsupported provider');
|
|
435
|
+
}
|
|
436
|
+
res.redirect(authUrl);
|
|
437
|
+
});
|
|
438
|
+
// OAuth 回调接口 (处理真实的 Code 换 Token)
|
|
439
|
+
app.get('/api/auth/:provider/callback', async (req, res) => {
|
|
440
|
+
const provider = req.params.provider;
|
|
441
|
+
const { code, state } = req.query;
|
|
442
|
+
if (!code || !state) {
|
|
443
|
+
return res.status(400).send('Missing code or state');
|
|
444
|
+
}
|
|
445
|
+
// 验证 State
|
|
446
|
+
const stateData = oauthStates.get(state);
|
|
447
|
+
if (!stateData || stateData.provider !== provider || stateData.expiresAt < Date.now()) {
|
|
448
|
+
return res.status(400).send('Invalid or expired state');
|
|
449
|
+
}
|
|
450
|
+
oauthStates.delete(state);
|
|
451
|
+
try {
|
|
452
|
+
let tokenData;
|
|
453
|
+
if (provider === 'lark') {
|
|
454
|
+
const appId = config.channels?.feishu?.appId || 'cli_a939363208f8dbb4';
|
|
455
|
+
const appSecret = config.channels?.feishu?.appSecret || 'e6oamY93lKQt9JFkbdWmGC6ULjNKAU62';
|
|
456
|
+
// 1. 获取 app_access_token
|
|
457
|
+
const appTokenRes = await axios.post('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
|
|
458
|
+
app_id: appId,
|
|
459
|
+
app_secret: appSecret
|
|
460
|
+
});
|
|
461
|
+
const appAccessToken = appTokenRes.data.app_access_token;
|
|
462
|
+
// 2. 用 code 换取 user_access_token
|
|
463
|
+
const userTokenRes = await axios.post('https://open.feishu.cn/open-apis/authen/v1/oidc/access_token', {
|
|
464
|
+
grant_type: 'authorization_code',
|
|
465
|
+
code: code
|
|
466
|
+
}, {
|
|
467
|
+
headers: { 'Authorization': `Bearer ${appAccessToken}` }
|
|
468
|
+
});
|
|
469
|
+
tokenData = userTokenRes.data.data;
|
|
470
|
+
oauthTokens.set('lark', tokenData); // 实际应持久化
|
|
471
|
+
}
|
|
472
|
+
else if (provider === 'wps') {
|
|
473
|
+
const appId = 'your_wps_app_id';
|
|
474
|
+
const appSecret = 'your_wps_app_secret';
|
|
475
|
+
// WPS 用 code 换 token
|
|
476
|
+
const tokenRes = await axios.post('https://openapi.wps.cn/oauthapi/v2/token', {
|
|
477
|
+
app_id: appId,
|
|
478
|
+
app_secret: appSecret,
|
|
479
|
+
grant_type: 'authorization_code',
|
|
480
|
+
code: code
|
|
481
|
+
});
|
|
482
|
+
tokenData = tokenRes.data;
|
|
483
|
+
oauthTokens.set('wps', tokenData); // 实际应持久化
|
|
484
|
+
}
|
|
485
|
+
console.log(`[OAuth] Successfully authenticated with ${provider}`);
|
|
486
|
+
// 授权成功,重定向回前端并带上成功标志
|
|
487
|
+
// 注意:实际项目中前端地址可能不同,这里假设本地开发环境
|
|
488
|
+
res.redirect(`http://localhost:5173/settings?auth_success=true&provider=${provider}`);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
console.error(`[OAuth] Failed to exchange token for ${provider}:`, error.response?.data || error.message);
|
|
492
|
+
res.redirect(`http://localhost:5173/settings?auth_success=false&error=token_exchange_failed`);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
362
495
|
app.get('/api/projects', async (req, res) => {
|
|
363
496
|
try {
|
|
364
497
|
const list = memory.structured.listProjects();
|
|
@@ -554,10 +687,44 @@ eventBus.onEvent('message', async (event) => {
|
|
|
554
687
|
const content = payload.content;
|
|
555
688
|
try {
|
|
556
689
|
console.log(`[EventBus] Processing message for session: ${sessionId}`);
|
|
557
|
-
|
|
690
|
+
let session = await memory.getSession(sessionId);
|
|
558
691
|
if (!session) {
|
|
559
|
-
|
|
560
|
-
|
|
692
|
+
// 对于第三方 Channel (如 Feishu),如果 Session 不存在,自动创建
|
|
693
|
+
if (channelName === 'feishu' || channelName === 'telegram' || channelName === 'dingtalk' || channelName === 'wechat') {
|
|
694
|
+
console.log(`[EventBus] Auto-creating session for ${channelName}: ${sessionId}`);
|
|
695
|
+
// 使用传入的 sessionId 作为 id,避免生成新的 uuid
|
|
696
|
+
// 注意:memory.createSession 默认生成 UUID,我们需要支持指定 ID
|
|
697
|
+
// 但 memory.createSession 目前不支持指定 ID,它调用 shortTerm.createSession
|
|
698
|
+
// 所以我们需要扩展 createSession 或者使用一种特殊方式
|
|
699
|
+
// 这里采用一种更简单的策略:如果是第三方渠道,我们允许 ShortTermMemory 懒加载/自动创建
|
|
700
|
+
// 或者在这里先创建一个标准 Session,然后映射?
|
|
701
|
+
// 不,sessionId 必须保持一致 (e.g. feishu:chat_id) 才能在后续消息中找到上下文
|
|
702
|
+
// 让我们修改 ShortTermMemory 的 createSession 逻辑,或者直接在这里 hack
|
|
703
|
+
// 但最好的方式是让 memory.createSession 支持自定义 ID
|
|
704
|
+
// 临时方案:如果 memory 支持 ensureSession
|
|
705
|
+
// 实际上,我们应该修改 MemoryManager 和 ShortTermMemory 支持 `createSession({ id: ... })`
|
|
706
|
+
// 由于现在无法直接修改底层接口,我们尝试调用 createSession 并忽略返回的 ID?不对。
|
|
707
|
+
// 正确的做法:
|
|
708
|
+
// 第三方 Channel 的 SessionID 通常是固定的 (如 `feishu:123`)。
|
|
709
|
+
// 我们需要 ShortTermMemory 支持 `getOrCreateSession(id)`
|
|
710
|
+
// 暂时先用 createSession 创建一个带 alias 的 session,
|
|
711
|
+
// 但这样 sessionId 会变,导致无法匹配。
|
|
712
|
+
// 关键点:EventBus 接收到的 sessionId 是 `feishu:xxx`。
|
|
713
|
+
// 我们必须确保数据库里有一个 ID 为 `feishu:xxx` 的 session。
|
|
714
|
+
// 现有的 `createSession` 会生成 UUID。
|
|
715
|
+
// 让我们修改 memory.createSession 及其底层实现来支持自定义 ID。
|
|
716
|
+
// 现在先报错,提示需要修改 ShortTermMemory。
|
|
717
|
+
// 但为了不中断流程,我们假设 ShortTermMemory.createSession 能够接受 id 参数
|
|
718
|
+
// 实际上,查看 memory.ts,createSession 调用 shortTerm.createSession
|
|
719
|
+
// 查看 short-term.ts (需要 read),createSession 确实生成了 uuid。
|
|
720
|
+
// 为了快速修复,我们在 index.ts 里做不到,必须去修改 ShortTermMemory。
|
|
721
|
+
// 假设我们已经修改了(下一步操作),这里我们调用一个新的方法
|
|
722
|
+
session = await memory.createSession({ id: sessionId, alias: `${channelName} Session` });
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.error(`[EventBus] Session not found: ${sessionId}`);
|
|
726
|
+
throw new Error('Session not found. Please create or select a session first.');
|
|
727
|
+
}
|
|
561
728
|
}
|
|
562
729
|
await memory.addMessage(sessionId, { role: 'user', content, timestamp: Date.now() });
|
|
563
730
|
const run = await memory.createRun(sessionId, content);
|
|
@@ -14,14 +14,39 @@ class SkillRegistry {
|
|
|
14
14
|
}
|
|
15
15
|
this.skills.set(skill.name, skill);
|
|
16
16
|
}
|
|
17
|
+
unregister(skillName) {
|
|
18
|
+
this.skills.delete(skillName);
|
|
19
|
+
}
|
|
20
|
+
enableSkill(skillName) {
|
|
21
|
+
const skill = this.skills.get(skillName);
|
|
22
|
+
if (skill)
|
|
23
|
+
skill.enabled = true;
|
|
24
|
+
}
|
|
25
|
+
disableSkill(skillName) {
|
|
26
|
+
const skill = this.skills.get(skillName);
|
|
27
|
+
if (skill)
|
|
28
|
+
skill.enabled = false;
|
|
29
|
+
}
|
|
17
30
|
getSkill(name) {
|
|
18
|
-
|
|
31
|
+
const skill = this.skills.get(name);
|
|
32
|
+
return skill?.enabled ? skill : undefined;
|
|
19
33
|
}
|
|
20
34
|
getAllSkills() {
|
|
21
35
|
return Array.from(this.skills.values());
|
|
22
36
|
}
|
|
23
|
-
|
|
24
|
-
return this.getAllSkills().
|
|
37
|
+
getEnabledSkills() {
|
|
38
|
+
return this.getAllSkills().filter(s => s.enabled);
|
|
39
|
+
}
|
|
40
|
+
getToolsDefinition(query) {
|
|
41
|
+
let activeSkills = this.getEnabledSkills();
|
|
42
|
+
// 如果有提供查询语句,可以基于 keywords 进行初步过滤,防止一次性发送过多 Tool
|
|
43
|
+
if (query) {
|
|
44
|
+
// 这里可以实现简单的关键词匹配,或者基于 LLM Router 决定
|
|
45
|
+
// 目前为了不影响基础功能,如果 query 存在,我们暂不严格过滤,
|
|
46
|
+
// 但为将来根据 intent 选择 skills 留出接口
|
|
47
|
+
// activeSkills = activeSkills.filter(s => s.keywords.some(k => query.includes(k)));
|
|
48
|
+
}
|
|
49
|
+
return activeSkills.map(skill => skill.toJSON());
|
|
25
50
|
}
|
|
26
51
|
}
|
|
27
52
|
export const skillRegistry = SkillRegistry.getInstance();
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "xiaozuoassistant",
|
|
3
3
|
"private": false,
|
|
4
4
|
"description": "Your personal, locally-hosted AI assistant for office productivity.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.82",
|
|
6
6
|
"author": "mantle.lau",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@lancedb/lancedb": "0.22.3",
|
|
46
46
|
"@lancedb/lancedb-darwin-x64": "0.22.3",
|
|
47
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
47
48
|
"@types/node-cron": "^3.0.11",
|
|
48
49
|
"apache-arrow": "18.1.0",
|
|
49
50
|
"axios": "^1.13.6",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"you": "你",
|
|
10
10
|
"assistant": "xiaozuoAssistant",
|
|
11
11
|
"thinking": "思考中...",
|
|
12
|
-
"inputPlaceholder": "给 xiaozuoAssistant
|
|
12
|
+
"inputPlaceholder": "给 xiaozuoAssistant 发送消息 (Cmd/Ctrl + Enter 发送)...",
|
|
13
13
|
"disclaimer": "xiaozuoAssistant 可能会犯错。请核对重要信息。",
|
|
14
14
|
"resetSession": "重置会话",
|
|
15
15
|
"confirmReset": "确定要重置当前会话记忆吗?这将清空所有消息历史。"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"tabs": {
|
|
47
47
|
"general": "常规",
|
|
48
48
|
"identity": "用户身份管理",
|
|
49
|
-
"scheduler": "
|
|
49
|
+
"scheduler": "定时任务"
|
|
50
50
|
},
|
|
51
51
|
"general": {
|
|
52
52
|
"provider": "模型提供商",
|