@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.
- package/dist/assets/index-BjRZpEx5.css +32 -0
- package/dist/assets/{index-C0my6OWo.js → index-jWEew-uM.js} +37 -37
- package/dist/assets/{vendor-icons-_JvlqdUe.js → vendor-icons-CosU8VI8.js} +69 -64
- package/dist/feishu-logo.svg +6 -0
- package/dist/index.html +3 -3
- package/package.json +4 -2
- package/server/claude-sdk.js +8 -5
- package/server/database/db.js +109 -1
- package/server/index.js +10 -0
- package/server/projects.js +13 -10
- package/server/routes/auth.js +18 -1
- package/server/routes/skills.js +10 -3
- package/server/services/feishu/card-builder.js +937 -0
- package/server/services/feishu/command-handler.js +492 -0
- package/server/services/feishu/feishu-db.js +7 -0
- package/server/services/feishu/feishu-engine.js +884 -0
- package/server/services/feishu/index.js +76 -0
- package/server/services/feishu/lark-client.js +398 -0
- package/server/services/feishu/sdk-bridge.js +475 -0
- package/server/services/feishu/speech.js +117 -0
- package/dist/assets/index-Bawp3dBD.css +0 -32
package/server/database/db.js
CHANGED
|
@@ -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();
|
package/server/projects.js
CHANGED
|
@@ -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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
}
|
package/server/routes/auth.js
CHANGED
|
@@ -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;
|
package/server/routes/skills.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|