xiaozuoassistant 0.2.42 → 0.2.44

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.
@@ -80,8 +80,59 @@ export class FeishuChannel extends BaseChannel {
80
80
  if (sender.sender_type !== 'user') {
81
81
  return;
82
82
  }
83
+ // 在群聊中,只有当机器人被 @ 时才响应
84
+ if (event.chat_type === 'group') {
85
+ const mentions = event.mentions || [];
86
+ // 检查是否包含对本机器人的 @,如果有,mentions 数组中会有一项是针对 bot 的
87
+ // 飞书 SDK 会将消息文本中的 @ 解析到 mentions 数组
88
+ // 对于机器人自己,如果没有被 @,则直接忽略
89
+ const isMentioned = mentions.some((m) => !m.id || m.id.open_id === undefined);
90
+ // 注意:在飞书事件中,如果 @ 的是机器人自己,通常没有具体的 user_id,或者带有特定的标识。
91
+ // 更严谨的做法是:如果 message 内容中包含 "@_user_1" 并且 mentions 里有机器人的标识。
92
+ // 简单且通用的判定是:群聊中如果不包含 mentions,肯定没被@
93
+ if (mentions.length === 0) {
94
+ return; // 群聊中未被@,忽略
95
+ }
96
+ // 进一步:我们需要检查 mentions 里面是不是真的@了自己
97
+ // 飞书返回的 mentions 结构通常包含 key 和 id,如果@了机器人,通常 id 里有 bot_id 或没有普通用户id
98
+ // 但考虑到我们可能没有当前机器人的 bot_id 缓存,只要在群里有人 @,我们就看 content 里是否有对应占位符
99
+ // 为防止误回别人@其他机器人的消息,我们可以简单处理:
100
+ // 如果 content 解析出来的文本在去掉了 @ 标签后有内容,且触发了我们的服务。
101
+ // 更好的办法:如果 `mentions` 里有人,我们就假设是@了本机器人(因为通常没配其他机器人的时候)
102
+ // 更精确:飞书给机器人的事件回调,只有在机器人被@或者在单聊时,才会触发!
103
+ // !!!等一下!!!
104
+ // 实际上,飞书开放平台规则:机器人如果在群里,只有被 @ 时,飞书服务器才会把消息推送给长连接!
105
+ // 如果没被 @,飞书根本不会发事件过来!
106
+ // 但是,如果机器人在后台申请了“获取群内所有消息”的高级权限,那么每条消息都会推过来。
107
+ // 为了防止刷屏,我们检查 event.mentions 是否包含机器人。
108
+ // 我们通过检查 mentions 数组里是否包含一个 name 为本机器人的项,或者 id.open_id 为空的项
109
+ const isMentionedSelf = mentions.some((m) => {
110
+ // 飞书中@机器人的 mention 对象通常缺少某些具体的 user_id 字段,或者 name 匹配
111
+ return m.name === botName || !m.id || Object.keys(m.id).length === 0 || m.is_self === true;
112
+ });
113
+ // 如果有 mentions 但是没检测到自己,也有可能是获取所有消息权限带来的,忽略它
114
+ // 但是飞书当前版本的事件其实有个隐式的坑:mentions 里的 key 是 "@_user_1",我们需要把它从内容里剔除
115
+ }
83
116
  try {
84
- const content = JSON.parse(event.content).text;
117
+ let content = JSON.parse(event.content).text;
118
+ // 清理飞书消息中的 @ 占位符 (例如 "@_user_1")
119
+ if (event.mentions && event.mentions.length > 0) {
120
+ // 如果是群聊且未被@,且我们拿到了消息(说明有获取全群消息的权限),我们必须拦截掉
121
+ if (event.chat_type === 'group') {
122
+ // 简单判断:如果文本中没有出现 @ 相关的占位符,且这是群聊,说明不是对机器人说的
123
+ if (!content.includes('@_user_')) {
124
+ return;
125
+ }
126
+ }
127
+ // 将所有的 "@_user_x" 占位符从文本中删掉,以免干扰大模型
128
+ event.mentions.forEach((m) => {
129
+ content = content.replace(m.key, '').trim();
130
+ });
131
+ }
132
+ // 如果清理掉 @ 之后内容为空,说明用户只发了一个 "@机器人"
133
+ if (!content.trim()) {
134
+ return;
135
+ }
85
136
  // Use chat_id for group chats, open_id/user_id for p2p?
86
137
  // Actually message.chat_id is universal for where the message comes from.
87
138
  const sessionId = `feishu:${botName}:${event.chat_id}`;
@@ -206,7 +206,41 @@ app.delete('/api/sessions/:id', async (req, res) => {
206
206
  // 简单的目录列举接口
207
207
  app.post('/api/fs/list', async (req, res) => {
208
208
  try {
209
- const dirPath = req.body.path || process.cwd();
209
+ let dirPath = req.body.path;
210
+ // 如果没有提供路径,在 Windows 上默认返回驱动器列表;在 Mac/Linux 返回根目录或 Home
211
+ if (!dirPath) {
212
+ if (process.platform === 'win32') {
213
+ // Windows: 返回所有可用驱动器 (A-Z)
214
+ try {
215
+ // 使用 child_process 执行 wmic 命令获取逻辑磁盘
216
+ const { execSync } = require('child_process');
217
+ const stdout = execSync('wmic logicaldisk get name').toString();
218
+ const drives = stdout.split('\n')
219
+ .map((line) => line.trim())
220
+ .filter((line) => line.length === 2 && line.endsWith(':'))
221
+ .map((drive) => drive + '\\');
222
+ if (drives.length > 0) {
223
+ return res.json({
224
+ current: 'PC',
225
+ parent: null,
226
+ items: drives.map((d) => ({ name: d, isDirectory: true, path: d }))
227
+ });
228
+ }
229
+ }
230
+ catch (e) {
231
+ // 如果 wmic 失败,退退为默认 C 盘
232
+ dirPath = 'C:\\';
233
+ }
234
+ }
235
+ else {
236
+ // Mac/Linux: 默认为用户主目录
237
+ dirPath = require('os').homedir();
238
+ }
239
+ }
240
+ // 处理特殊情况:当当前是 Windows 的驱动器列表页面时,如果点击了某个驱动器,这里就是正常路径
241
+ if (dirPath === 'PC') {
242
+ dirPath = 'C:\\';
243
+ }
210
244
  // Use fsPromises for async operations which returns proper types for withFileTypes
211
245
  const items = await fsPromises.readdir(dirPath, { withFileTypes: true });
212
246
  const result = items.map(item => ({
@@ -214,9 +248,15 @@ app.post('/api/fs/list', async (req, res) => {
214
248
  isDirectory: item.isDirectory(),
215
249
  path: path.join(dirPath, item.name)
216
250
  })).filter(item => item.isDirectory && !item.name.startsWith('.')); // 仅列出目录,忽略隐藏目录
251
+ // 在 Windows 上,计算父目录时需要特别处理根目录 (例如 C:\ 的父目录应该是驱动器列表 'PC')
252
+ let parentDir = path.dirname(dirPath);
253
+ if (process.platform === 'win32' && parentDir === dirPath) {
254
+ // 如果 dirname(C:\) 还是 C:\,说明到顶了,父级应该展示磁盘列表
255
+ parentDir = 'PC';
256
+ }
217
257
  res.json({
218
258
  current: dirPath,
219
- parent: path.dirname(dirPath),
259
+ parent: parentDir,
220
260
  items: result
221
261
  });
222
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xiaozuoassistant",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "description": "A local-first personal AI assistant with multi-channel support and enhanced memory.",
5
5
  "author": "mantle.lau",
6
6
  "license": "MIT",