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.
@@ -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-B8uwjNT9.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-B5Qw98nG.css">
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(app)] : []),
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 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
3
+ import * as lark from '@larksuiteoapi/node-sdk';
7
4
  export class FeishuChannel extends BaseChannel {
8
- constructor(app) {
5
+ constructor() {
9
6
  super();
10
- this.app = app;
11
7
  this.name = 'feishu';
12
- this.tenantAccessToken = '';
13
- this.tokenExpire = 0;
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
- // 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' });
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
- 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
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
- 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
- }
53
+ console.log('Feishu Channel Started (WebSocket Mode)');
67
54
  }
68
55
  catch (error) {
69
- console.error('Error fetching Feishu token:', 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 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
- }
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
- const tools = skillRegistry.getToolsDefinition();
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
- const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
19
- memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
22
+ runMaintenanceNow();
20
23
  });
21
24
  // Archive session memories every 3 minutes
22
- cron.schedule('*/3 * * * *', () => {
23
- memoryManager.runSessionArchiving();
25
+ archiveTask = cron.schedule('*/3 * * * *', () => {
26
+ runSessionArchivingNow();
24
27
  });
25
28
  };
26
- export const runMaintenanceNow = () => {
27
- console.log('[Scheduler] Manually triggering maintenance...');
28
- const sessionMaxAgeDays = config.scheduler?.sessionRetentionDays ?? 5;
29
- memoryManager.runMaintenance({ sessionMaxAgeDays, vectorMaxAgeDays: 15 });
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
  };
@@ -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 triggered.' });
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
- // Projects CRUD
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
- const session = await memory.getSession(sessionId);
690
+ let session = await memory.getSession(sessionId);
558
691
  if (!session) {
559
- console.error(`[EventBus] Session not found: ${sessionId}`);
560
- throw new Error('Session not found. Please create or select a session first.');
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);
@@ -1,4 +1,8 @@
1
1
  export class BaseSkill {
2
+ constructor() {
3
+ this.enabled = true;
4
+ this.keywords = [];
5
+ }
2
6
  validate(params) {
3
7
  // 这里可以添加基于 JSON Schema 的验证逻辑
4
8
  return true;
@@ -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
- return this.skills.get(name);
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
- getToolsDefinition() {
24
- return this.getAllSkills().map(skill => skill.toJSON());
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.80",
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": "模型提供商",