@ian2018cs/agenthub 0.1.30 → 0.1.31

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.
@@ -210,6 +210,43 @@ const runMigrations = () => {
210
210
  )
211
211
  `);
212
212
 
213
+ // Create Feishu integration tables
214
+ db.exec(`
215
+ CREATE TABLE IF NOT EXISTS feishu_bindings (
216
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
217
+ feishu_open_id TEXT NOT NULL UNIQUE,
218
+ user_uuid TEXT NOT NULL,
219
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
220
+ FOREIGN KEY (user_uuid) REFERENCES users(uuid)
221
+ )
222
+ `);
223
+ db.exec('CREATE INDEX IF NOT EXISTS idx_feishu_open_id ON feishu_bindings(feishu_open_id)');
224
+
225
+ db.exec(`
226
+ CREATE TABLE IF NOT EXISTS feishu_session_state (
227
+ feishu_open_id TEXT PRIMARY KEY,
228
+ claude_session_id TEXT,
229
+ cwd TEXT NOT NULL DEFAULT '',
230
+ permission_mode TEXT NOT NULL DEFAULT 'default',
231
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
232
+ )
233
+ `);
234
+
235
+ // 为已有 feishu_session_state 表补充 chat_id 列(存储 p2p 会话的 chat_id,供菜单事件主动发消息使用)
236
+ try {
237
+ db.exec('ALTER TABLE feishu_session_state ADD COLUMN chat_id TEXT');
238
+ } catch (_) {
239
+ // 列已存在,忽略
240
+ }
241
+
242
+ // 飞书已处理消息 ID 去重表(持久化,防止服务重启后重复处理)
243
+ db.exec(`
244
+ CREATE TABLE IF NOT EXISTS feishu_processed_messages (
245
+ message_id TEXT PRIMARY KEY,
246
+ processed_at DATETIME DEFAULT CURRENT_TIMESTAMP
247
+ )
248
+ `);
249
+
213
250
  console.log('Database migrations completed successfully');
214
251
  } catch (error) {
215
252
  console.error('Error running migrations:', error.message);
@@ -1046,6 +1083,76 @@ const settingsDb = {
1046
1083
  }
1047
1084
  };
1048
1085
 
1086
+ const feishuDb = {
1087
+ getBinding: (feishuOpenId) => {
1088
+ return db.prepare('SELECT * FROM feishu_bindings WHERE feishu_open_id = ?').get(feishuOpenId) || null;
1089
+ },
1090
+
1091
+ createBinding: (feishuOpenId, userUuid) => {
1092
+ db.prepare(`
1093
+ INSERT INTO feishu_bindings (feishu_open_id, user_uuid)
1094
+ VALUES (?, ?)
1095
+ ON CONFLICT(feishu_open_id) DO UPDATE SET user_uuid = excluded.user_uuid
1096
+ `).run(feishuOpenId, userUuid);
1097
+ },
1098
+
1099
+ removeBinding: (feishuOpenId) => {
1100
+ db.prepare('DELETE FROM feishu_bindings WHERE feishu_open_id = ?').run(feishuOpenId);
1101
+ },
1102
+
1103
+ getSessionState: (feishuOpenId) => {
1104
+ return db.prepare('SELECT * FROM feishu_session_state WHERE feishu_open_id = ?').get(feishuOpenId) || null;
1105
+ },
1106
+
1107
+ updateSessionState: (feishuOpenId, updates) => {
1108
+ const state = db.prepare('SELECT * FROM feishu_session_state WHERE feishu_open_id = ?').get(feishuOpenId);
1109
+ if (state) {
1110
+ const fields = [];
1111
+ const values = [];
1112
+ if ('claude_session_id' in updates) { fields.push('claude_session_id = ?'); values.push(updates.claude_session_id); }
1113
+ if ('cwd' in updates) { fields.push('cwd = ?'); values.push(updates.cwd); }
1114
+ if ('permission_mode' in updates) { fields.push('permission_mode = ?'); values.push(updates.permission_mode); }
1115
+ if ('chat_id' in updates) { fields.push('chat_id = ?'); values.push(updates.chat_id); }
1116
+ fields.push('updated_at = CURRENT_TIMESTAMP');
1117
+ values.push(feishuOpenId);
1118
+ db.prepare(`UPDATE feishu_session_state SET ${fields.join(', ')} WHERE feishu_open_id = ?`).run(...values);
1119
+ } else {
1120
+ db.prepare(`
1121
+ INSERT INTO feishu_session_state (feishu_open_id, claude_session_id, cwd, permission_mode, chat_id)
1122
+ VALUES (?, ?, ?, ?, ?)
1123
+ `).run(
1124
+ feishuOpenId,
1125
+ updates.claude_session_id ?? null,
1126
+ updates.cwd ?? '',
1127
+ updates.permission_mode ?? 'default',
1128
+ updates.chat_id ?? null
1129
+ );
1130
+ }
1131
+ },
1132
+
1133
+ clearSession: (feishuOpenId) => {
1134
+ db.prepare('UPDATE feishu_session_state SET claude_session_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE feishu_open_id = ?').run(feishuOpenId);
1135
+ },
1136
+
1137
+ // 消息去重:检查消息是否已处理
1138
+ isProcessed: (messageId) => {
1139
+ const row = db.prepare('SELECT 1 FROM feishu_processed_messages WHERE message_id = ?').get(messageId);
1140
+ return !!row;
1141
+ },
1142
+
1143
+ // 消息去重:标记消息为已处理,并清理 48 小时前的旧记录
1144
+ markProcessed: (messageId) => {
1145
+ db.prepare(`
1146
+ INSERT OR IGNORE INTO feishu_processed_messages (message_id) VALUES (?)
1147
+ `).run(messageId);
1148
+ // 清理 48 小时前的记录,防止表无限增长
1149
+ db.prepare(`
1150
+ DELETE FROM feishu_processed_messages
1151
+ WHERE processed_at < datetime('now', '-48 hours')
1152
+ `).run();
1153
+ }
1154
+ };
1155
+
1049
1156
  export {
1050
1157
  db,
1051
1158
  initializeDatabase,
@@ -1053,5 +1160,6 @@ export {
1053
1160
  usageDb,
1054
1161
  verificationDb,
1055
1162
  domainWhitelistDb,
1056
- settingsDb
1163
+ settingsDb,
1164
+ feishuDb
1057
1165
  };
package/server/index.js CHANGED
@@ -58,6 +58,7 @@ import { initializeDatabase, userDb } from './database/db.js';
58
58
  import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
59
59
  import { getUserPaths, initCodexDirectories, initGeminiDirectories } from './services/user-directories.js';
60
60
  import { startUsageScanner } from './services/usage-scanner.js';
61
+ import { startFeishuService, stopFeishuService } from './services/feishu/index.js';
61
62
 
62
63
  // File system watcher for projects folder - per user
63
64
  const userWatchers = new Map(); // Map<userUuid, { watcher, clients: Set<ws> }>
@@ -2246,6 +2247,15 @@ async function startServer() {
2246
2247
  // Start usage scanner service
2247
2248
  startUsageScanner();
2248
2249
 
2250
+ // Start Feishu integration service (if configured)
2251
+ if (process.env.FEISHU_APP_ID && process.env.FEISHU_APP_SECRET) {
2252
+ startFeishuService().catch(err =>
2253
+ console.error('[Feishu] Service startup error:', err.message)
2254
+ );
2255
+ } else {
2256
+ console.log('[Feishu] Skipped: FEISHU_APP_ID or FEISHU_APP_SECRET not set');
2257
+ }
2258
+
2249
2259
  // Migrate existing users: ensure codex directories and config files exist
2250
2260
  try {
2251
2261
  const allUsers = userDb.getAllUsers();
@@ -812,6 +812,12 @@ async function deleteProject(projectName, userUuid, options = {}) {
812
812
  const sessionDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);
813
813
 
814
814
  try {
815
+ // If deleteFolder requested, resolve the actual path BEFORE removing config
816
+ let actualProjectDir = null;
817
+ if (deleteFolder) {
818
+ actualProjectDir = await extractProjectDirectory(projectName, userUuid);
819
+ }
820
+
815
821
  // Remove the Claude sessions directory
816
822
  try {
817
823
  await fs.rm(sessionDir, { recursive: true, force: true });
@@ -827,16 +833,13 @@ async function deleteProject(projectName, userUuid, options = {}) {
827
833
  await saveProjectConfig(config, userUuid);
828
834
 
829
835
  // Optionally delete the actual project folder
830
- if (deleteFolder) {
831
- const actualProjectDir = await extractProjectDirectory(projectName, userUuid);
832
- if (actualProjectDir) {
833
- try {
834
- await fs.rm(actualProjectDir, { recursive: true, force: true });
835
- } catch (err) {
836
- if (err.code !== 'ENOENT') {
837
- console.error(`Error deleting project folder ${actualProjectDir}:`, err);
838
- // Non-fatal: sessions already deleted, log and continue
839
- }
836
+ if (deleteFolder && actualProjectDir) {
837
+ try {
838
+ await fs.rm(actualProjectDir, { recursive: true, force: true });
839
+ } catch (err) {
840
+ if (err.code !== 'ENOENT') {
841
+ console.error(`Error deleting project folder ${actualProjectDir}:`, err);
842
+ // Non-fatal: sessions already deleted, log and continue
840
843
  }
841
844
  }
842
845
  }
@@ -1,8 +1,9 @@
1
1
  import express from 'express';
2
2
  import bcrypt from 'bcrypt';
3
+ import jwt from 'jsonwebtoken';
3
4
  import { v4 as uuidv4 } from 'uuid';
4
5
  import { userDb, verificationDb, domainWhitelistDb, usageDb, settingsDb } from '../database/db.js';
5
- import { generateToken, authenticateToken } from '../middleware/auth.js';
6
+ import { generateToken, authenticateToken, JWT_SECRET } from '../middleware/auth.js';
6
7
  import { initUserDirectories } from '../services/user-directories.js';
7
8
  import { initBuiltinSkills } from '../services/builtin-skills.js';
8
9
  import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
@@ -281,4 +282,20 @@ router.get('/limit-status', authenticateToken, (req, res) => {
281
282
  }
282
283
  });
283
284
 
285
+ // Generate a short-lived token for binding a Feishu account
286
+ // The user opens the web UI, calls this endpoint, then sends /auth <token> in Feishu private chat
287
+ router.post('/feishu-bind-token', authenticateToken, (req, res) => {
288
+ try {
289
+ const token = jwt.sign(
290
+ { userId: req.user.id, uuid: req.user.uuid, email: req.user.email },
291
+ JWT_SECRET,
292
+ { expiresIn: '24h' }
293
+ );
294
+ res.json({ success: true, token, expiresIn: '24h' });
295
+ } catch (error) {
296
+ console.error('Error generating Feishu bind token:', error);
297
+ res.status(500).json({ error: '生成绑定 Token 失败' });
298
+ }
299
+ });
300
+
284
301
  export default router;
@@ -465,12 +465,14 @@ router.delete('/:name', async (req, res) => {
465
465
 
466
466
  // Check the symlink target to determine source
467
467
  let realPath = null;
468
+ let isSymlink = false;
468
469
  let isImported = false;
469
470
  let isBuiltin = false;
470
471
 
471
472
  try {
472
473
  const stat = await fs.lstat(linkPath);
473
- if (stat.isSymbolicLink()) {
474
+ isSymlink = stat.isSymbolicLink();
475
+ if (isSymlink) {
474
476
  realPath = await fs.realpath(linkPath);
475
477
  isImported = realPath.includes('/skills-import/');
476
478
  isBuiltin = isBuiltinSkillPath(realPath);
@@ -479,8 +481,13 @@ router.delete('/:name', async (req, res) => {
479
481
  return res.status(404).json({ error: 'Skill not found' });
480
482
  }
481
483
 
482
- // Remove symlink
483
- await fs.unlink(linkPath);
484
+ // Remove symlink or directory
485
+ if (isSymlink) {
486
+ await fs.unlink(linkPath);
487
+ } else {
488
+ // Real directory (local skill) — remove recursively
489
+ await fs.rm(linkPath, { recursive: true, force: true });
490
+ }
484
491
 
485
492
  // If imported, also delete the actual files
486
493
  if (isImported && realPath) {