@ian2018cs/agenthub 0.1.61 → 0.1.63
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 +2 -2
- package/dist/assets/index-Bih5jXn3.js +162 -0
- package/dist/assets/index-DSfWrrLR.css +32 -0
- package/dist/assets/{vendor-icons-BSeZkdSJ.js → vendor-icons-D0W5CcE4.js} +91 -71
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/claude-sdk.js +48 -15
- package/server/database/db.js +69 -5
- package/server/routes/admin.js +187 -0
- package/server/routes/agents.js +98 -5
- package/server/routes/auth.js +21 -1
- package/server/services/feature-permissions.js +92 -0
- package/server/services/tool-guard/index.js +2 -0
- package/dist/assets/index-B65OoZ2-.js +0 -162
- package/dist/assets/index-BW9F1hzB.css +0 -32
package/server/claude-sdk.js
CHANGED
|
@@ -232,13 +232,14 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
232
232
|
* @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
|
|
233
233
|
* @param {string} tempDir - Temp directory for cleanup
|
|
234
234
|
*/
|
|
235
|
-
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
|
|
235
|
+
function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, abortController = null) {
|
|
236
236
|
activeSessions.set(sessionId, {
|
|
237
237
|
instance: queryInstance,
|
|
238
238
|
startTime: Date.now(),
|
|
239
239
|
status: 'active',
|
|
240
240
|
tempImagePaths,
|
|
241
|
-
tempDir
|
|
241
|
+
tempDir,
|
|
242
|
+
abortController
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
245
|
|
|
@@ -511,6 +512,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
511
512
|
// Map CLI options to SDK format
|
|
512
513
|
const sdkOptions = mapCliOptionsToSDK(options);
|
|
513
514
|
|
|
515
|
+
// Create AbortController for session cancellation
|
|
516
|
+
const abortController = new AbortController();
|
|
517
|
+
sdkOptions.abortController = abortController;
|
|
518
|
+
|
|
514
519
|
// Load MCP configuration
|
|
515
520
|
const mcpServers = await loadMcpConfig(options.cwd, userUuid);
|
|
516
521
|
if (mcpServers) {
|
|
@@ -651,7 +656,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
651
656
|
|
|
652
657
|
// Track the query instance for abort capability
|
|
653
658
|
if (capturedSessionId) {
|
|
654
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
|
659
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
655
660
|
}
|
|
656
661
|
|
|
657
662
|
// Process streaming messages
|
|
@@ -661,7 +666,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
661
666
|
if (message.session_id && !capturedSessionId) {
|
|
662
667
|
|
|
663
668
|
capturedSessionId = message.session_id;
|
|
664
|
-
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
|
669
|
+
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, abortController);
|
|
665
670
|
|
|
666
671
|
// Set session ID on writer
|
|
667
672
|
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
@@ -775,17 +780,33 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
775
780
|
// Clean up temporary image files
|
|
776
781
|
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
777
782
|
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
783
|
+
// Only send completion event if not aborted — when the session is aborted
|
|
784
|
+
// via abortClaudeSDKSession(), that function already sends 'session-aborted'
|
|
785
|
+
// to the frontend, so we must not send a duplicate 'claude-complete'.
|
|
786
|
+
if (!abortController.signal.aborted) {
|
|
787
|
+
console.log('Streaming complete, sending claude-complete event');
|
|
788
|
+
ws.send({
|
|
789
|
+
type: 'claude-complete',
|
|
790
|
+
sessionId: capturedSessionId,
|
|
791
|
+
exitCode: 0,
|
|
792
|
+
isNewSession: !sessionId && !!command
|
|
793
|
+
});
|
|
794
|
+
console.log('claude-complete event sent');
|
|
795
|
+
} else {
|
|
796
|
+
console.log('Session was aborted, skipping claude-complete event');
|
|
797
|
+
}
|
|
787
798
|
|
|
788
799
|
} catch (error) {
|
|
800
|
+
// If the session was aborted, this is expected — do not treat as an error.
|
|
801
|
+
if (abortController.signal.aborted) {
|
|
802
|
+
console.log('Session aborted, ignoring post-abort error:', error.message);
|
|
803
|
+
if (capturedSessionId) {
|
|
804
|
+
removeSession(capturedSessionId);
|
|
805
|
+
}
|
|
806
|
+
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
789
810
|
console.error('SDK query error:', error);
|
|
790
811
|
|
|
791
812
|
// Clean up session on error
|
|
@@ -822,8 +843,20 @@ async function abortClaudeSDKSession(sessionId) {
|
|
|
822
843
|
try {
|
|
823
844
|
console.log(`Aborting SDK session: ${sessionId}`);
|
|
824
845
|
|
|
825
|
-
//
|
|
826
|
-
|
|
846
|
+
// Signal abort via AbortController (stops SDK internal operations)
|
|
847
|
+
if (session.abortController) {
|
|
848
|
+
session.abortController.abort();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Forcefully close the query and terminate the underlying process.
|
|
852
|
+
// close() is the correct method for aborting a running query — it kills the
|
|
853
|
+
// CLI subprocess and cleans up all resources (MCP transports, pending requests).
|
|
854
|
+
// interrupt() only works in streaming-input mode and is not reliable here.
|
|
855
|
+
try {
|
|
856
|
+
session.instance.close();
|
|
857
|
+
} catch (closeError) {
|
|
858
|
+
console.error(`Error closing session ${sessionId}:`, closeError);
|
|
859
|
+
}
|
|
827
860
|
|
|
828
861
|
// Update session status
|
|
829
862
|
session.status = 'aborted';
|
package/server/database/db.js
CHANGED
|
@@ -190,6 +190,18 @@ const runMigrations = () => {
|
|
|
190
190
|
db.exec('ALTER TABLE users ADD COLUMN daily_limit_usd REAL DEFAULT NULL');
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// Add feature_permissions column for per-user feature access control
|
|
194
|
+
if (!columnNames.includes('feature_permissions')) {
|
|
195
|
+
console.log('Running migration: Adding feature_permissions column');
|
|
196
|
+
db.exec('ALTER TABLE users ADD COLUMN feature_permissions TEXT DEFAULT NULL');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add visible_agents column for per-user agent visibility control
|
|
200
|
+
if (!columnNames.includes('visible_agents')) {
|
|
201
|
+
console.log('Running migration: Adding visible_agents column');
|
|
202
|
+
db.exec('ALTER TABLE users ADD COLUMN visible_agents TEXT DEFAULT NULL');
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
// Add raw_model column to usage_records for tracking actual model IDs
|
|
194
206
|
const usageTableInfo = db.prepare("PRAGMA table_info(usage_records)").all();
|
|
195
207
|
const usageColumnNames = usageTableInfo.map(col => col.name);
|
|
@@ -314,6 +326,11 @@ const runMigrations = () => {
|
|
|
314
326
|
db.exec('CREATE INDEX IF NOT EXISTS idx_agent_submissions_user ON agent_submissions(user_id)');
|
|
315
327
|
db.exec('CREATE INDEX IF NOT EXISTS idx_agent_submissions_status ON agent_submissions(status)');
|
|
316
328
|
|
|
329
|
+
// Migration: add update_notes column
|
|
330
|
+
try {
|
|
331
|
+
db.exec('ALTER TABLE agent_submissions ADD COLUMN update_notes TEXT');
|
|
332
|
+
} catch (_) { /* 列已存在 */ }
|
|
333
|
+
|
|
317
334
|
// 聊天图片持久化关联表
|
|
318
335
|
db.exec(`
|
|
319
336
|
CREATE TABLE IF NOT EXISTS message_images (
|
|
@@ -498,7 +515,7 @@ const userDb = {
|
|
|
498
515
|
getAllUsers: () => {
|
|
499
516
|
try {
|
|
500
517
|
return db.prepare(
|
|
501
|
-
'SELECT id, username, email, uuid, role, status, created_at, last_login, total_limit_usd, daily_limit_usd FROM users ORDER BY created_at DESC'
|
|
518
|
+
'SELECT id, username, email, uuid, role, status, created_at, last_login, total_limit_usd, daily_limit_usd, feature_permissions, visible_agents FROM users ORDER BY created_at DESC'
|
|
502
519
|
).all();
|
|
503
520
|
} catch (err) {
|
|
504
521
|
throw err;
|
|
@@ -599,6 +616,46 @@ const userDb = {
|
|
|
599
616
|
} catch (err) {
|
|
600
617
|
throw err;
|
|
601
618
|
}
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
// Get user feature permissions (raw JSON string)
|
|
622
|
+
getFeaturePermissions: (userId) => {
|
|
623
|
+
try {
|
|
624
|
+
const row = db.prepare('SELECT feature_permissions FROM users WHERE id = ?').get(userId);
|
|
625
|
+
return row?.feature_permissions || null;
|
|
626
|
+
} catch (err) {
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
// Update user feature permissions (JSON string or null to reset to default)
|
|
632
|
+
updateFeaturePermissions: (userId, permissions) => {
|
|
633
|
+
try {
|
|
634
|
+
const value = permissions === null ? null : JSON.stringify(permissions);
|
|
635
|
+
db.prepare('UPDATE users SET feature_permissions = ? WHERE id = ?').run(value, userId);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
// Get user visible agents (raw JSON string)
|
|
642
|
+
getVisibleAgents: (userId) => {
|
|
643
|
+
try {
|
|
644
|
+
const row = db.prepare('SELECT visible_agents FROM users WHERE id = ?').get(userId);
|
|
645
|
+
return row?.visible_agents || null;
|
|
646
|
+
} catch (err) {
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
|
|
651
|
+
// Update user visible agents (JSON array or null to reset)
|
|
652
|
+
updateVisibleAgents: (userId, agents) => {
|
|
653
|
+
try {
|
|
654
|
+
const value = agents === null ? null : JSON.stringify(agents);
|
|
655
|
+
db.prepare('UPDATE users SET visible_agents = ? WHERE id = ?').run(value, userId);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
602
659
|
}
|
|
603
660
|
};
|
|
604
661
|
|
|
@@ -1331,11 +1388,11 @@ const imageDb = {
|
|
|
1331
1388
|
|
|
1332
1389
|
// Agent submissions database operations
|
|
1333
1390
|
const agentSubmissionDb = {
|
|
1334
|
-
create: ({ userId, agentName, displayName, description, zipPath }) => {
|
|
1391
|
+
create: ({ userId, agentName, displayName, description, zipPath, updateNotes }) => {
|
|
1335
1392
|
const result = db.prepare(`
|
|
1336
|
-
INSERT INTO agent_submissions (user_id, agent_name, display_name, description, zip_path)
|
|
1337
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1338
|
-
`).run(userId, agentName, displayName, description || null, zipPath);
|
|
1393
|
+
INSERT INTO agent_submissions (user_id, agent_name, display_name, description, zip_path, update_notes)
|
|
1394
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1395
|
+
`).run(userId, agentName, displayName, description || null, zipPath, updateNotes || null);
|
|
1339
1396
|
return result.lastInsertRowid;
|
|
1340
1397
|
},
|
|
1341
1398
|
|
|
@@ -1378,6 +1435,13 @@ const agentSubmissionDb = {
|
|
|
1378
1435
|
SET status = ?, version = ?, reject_reason = ?, reviewed_by = ?, reviewed_at = CURRENT_TIMESTAMP
|
|
1379
1436
|
WHERE id = ?
|
|
1380
1437
|
`).run(status, version, rejectReason, reviewedBy, id);
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
// Get approved agent names by user (for auto-visibility)
|
|
1441
|
+
getApprovedAgentNamesByUser: (userId) => {
|
|
1442
|
+
return db.prepare(`
|
|
1443
|
+
SELECT DISTINCT agent_name FROM agent_submissions WHERE user_id = ? AND status = 'approved'
|
|
1444
|
+
`).all(userId).map(row => row.agent_name);
|
|
1381
1445
|
}
|
|
1382
1446
|
};
|
|
1383
1447
|
|
package/server/routes/admin.js
CHANGED
|
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
4
4
|
import { userDb, domainWhitelistDb, settingsDb } from '../database/db.js';
|
|
5
5
|
import { authenticateToken } from '../middleware/auth.js';
|
|
6
6
|
import { deleteUserDirectories, initUserDirectories } from '../services/user-directories.js';
|
|
7
|
+
import { getDefaultPermissions, ALL_FEATURE_PERMISSIONS } from '../services/feature-permissions.js';
|
|
7
8
|
|
|
8
9
|
const router = express.Router();
|
|
9
10
|
|
|
@@ -418,4 +419,190 @@ router.put('/settings/default-limits', (req, res) => {
|
|
|
418
419
|
}
|
|
419
420
|
});
|
|
420
421
|
|
|
422
|
+
// ==================== Feature Permissions ====================
|
|
423
|
+
|
|
424
|
+
// Get user feature permissions (raw, not resolved)
|
|
425
|
+
router.get('/users/:id/permissions', (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { id } = req.params;
|
|
428
|
+
|
|
429
|
+
const user = userDb.getUserById(id);
|
|
430
|
+
if (!user) {
|
|
431
|
+
return res.status(404).json({ error: 'User not found' });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const raw = userDb.getFeaturePermissions(id);
|
|
435
|
+
let permissions = null;
|
|
436
|
+
if (raw) {
|
|
437
|
+
try { permissions = JSON.parse(raw); } catch (_) { /* ignore */ }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
res.json({ permissions });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error('Error fetching user permissions:', error);
|
|
443
|
+
res.status(500).json({ error: '获取用户权限失败' });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Update user feature permissions
|
|
448
|
+
router.patch('/users/:id/permissions', (req, res) => {
|
|
449
|
+
try {
|
|
450
|
+
const { id } = req.params;
|
|
451
|
+
const { permissions } = req.body;
|
|
452
|
+
|
|
453
|
+
const user = userDb.getUserById(id);
|
|
454
|
+
if (!user) {
|
|
455
|
+
return res.status(404).json({ error: 'User not found' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// admin / super_admin 免检,不应设置权限
|
|
459
|
+
if (['admin', 'super_admin'].includes(user.role)) {
|
|
460
|
+
return res.status(400).json({ error: '管理员始终拥有全部权限,无需配置' });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// admin 不能操作 admin/super_admin
|
|
464
|
+
if (req.user.role === 'admin' && ['admin', 'super_admin'].includes(user.role)) {
|
|
465
|
+
return res.status(403).json({ error: '无权修改管理员的权限' });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// permissions 为 null 表示重置为默认
|
|
469
|
+
if (permissions === null) {
|
|
470
|
+
userDb.updateFeaturePermissions(parseInt(id), null);
|
|
471
|
+
return res.json({ success: true, message: '用户权限已重置为默认' });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 校验 permissions 对象
|
|
475
|
+
if (typeof permissions !== 'object' || Array.isArray(permissions)) {
|
|
476
|
+
return res.status(400).json({ error: '权限格式无效' });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 只允许已知的权限 key
|
|
480
|
+
const validKeys = Object.keys(ALL_FEATURE_PERMISSIONS);
|
|
481
|
+
const filtered = {};
|
|
482
|
+
for (const key of validKeys) {
|
|
483
|
+
if (key in permissions) {
|
|
484
|
+
filtered[key] = !!permissions[key];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
userDb.updateFeaturePermissions(parseInt(id), filtered);
|
|
489
|
+
res.json({ success: true, message: '用户权限已更新', permissions: filtered });
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error('Error updating user permissions:', error);
|
|
492
|
+
res.status(500).json({ error: '更新用户权限失败' });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Get default feature permissions
|
|
497
|
+
router.get('/settings/default-permissions', (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
const permissions = getDefaultPermissions();
|
|
500
|
+
// 同时返回 DB 中存储的原始值,以便前端区分是否已设置
|
|
501
|
+
const raw = settingsDb.get('default_feature_permissions');
|
|
502
|
+
res.json({ permissions, hasCustom: raw !== null });
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error('Error fetching default permissions:', error);
|
|
505
|
+
res.status(500).json({ error: '获取默认权限设置失败' });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Update default feature permissions
|
|
510
|
+
router.put('/settings/default-permissions', (req, res) => {
|
|
511
|
+
try {
|
|
512
|
+
const { permissions } = req.body;
|
|
513
|
+
|
|
514
|
+
// permissions 为 null 表示清除自定义,回退到环境变量/硬编码
|
|
515
|
+
if (permissions === null) {
|
|
516
|
+
settingsDb.delete('default_feature_permissions');
|
|
517
|
+
return res.json({ success: true, message: '默认权限已重置', permissions: getDefaultPermissions() });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (typeof permissions !== 'object' || Array.isArray(permissions)) {
|
|
521
|
+
return res.status(400).json({ error: '权限格式无效' });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 只允许已知的权限 key
|
|
525
|
+
const validKeys = Object.keys(ALL_FEATURE_PERMISSIONS);
|
|
526
|
+
const filtered = {};
|
|
527
|
+
for (const key of validKeys) {
|
|
528
|
+
if (key in permissions) {
|
|
529
|
+
filtered[key] = !!permissions[key];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
settingsDb.set('default_feature_permissions', JSON.stringify(filtered), req.user.id);
|
|
534
|
+
res.json({ success: true, message: '默认权限已更新', permissions: { ...ALL_FEATURE_PERMISSIONS, ...filtered } });
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.error('Error updating default permissions:', error);
|
|
537
|
+
res.status(500).json({ error: '更新默认权限设置失败' });
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ─── Agent Visibility ───────────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
// GET /api/admin/users/:id/visible-agents - Get user's visible agents config
|
|
544
|
+
router.get('/users/:id/visible-agents', (req, res) => {
|
|
545
|
+
try {
|
|
546
|
+
const { id } = req.params;
|
|
547
|
+
const user = userDb.getUserById(id);
|
|
548
|
+
if (!user) {
|
|
549
|
+
return res.status(404).json({ error: 'User not found' });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const raw = userDb.getVisibleAgents(id);
|
|
553
|
+
let agents = null;
|
|
554
|
+
if (raw) {
|
|
555
|
+
try { agents = JSON.parse(raw); } catch (_) { /* ignore */ }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
res.json({ agents });
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error('Error getting user visible agents:', error);
|
|
561
|
+
res.status(500).json({ error: '获取用户 Agent 可见性失败' });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// PATCH /api/admin/users/:id/visible-agents - Update user's visible agents
|
|
566
|
+
router.patch('/users/:id/visible-agents', (req, res) => {
|
|
567
|
+
try {
|
|
568
|
+
const { id } = req.params;
|
|
569
|
+
const { agents } = req.body;
|
|
570
|
+
|
|
571
|
+
const user = userDb.getUserById(id);
|
|
572
|
+
if (!user) {
|
|
573
|
+
return res.status(404).json({ error: 'User not found' });
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// admin/super_admin always see all agents, no need to configure
|
|
577
|
+
if (['admin', 'super_admin'].includes(user.role)) {
|
|
578
|
+
return res.status(400).json({ error: '管理员始终可见全部 Agent,无需配置' });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// admin cannot modify admin/super_admin
|
|
582
|
+
if (req.user.role === 'admin' && ['admin', 'super_admin'].includes(user.role)) {
|
|
583
|
+
return res.status(403).json({ error: '无权修改管理员的 Agent 可见性' });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// null = reset to default (no agents visible)
|
|
587
|
+
if (agents === null) {
|
|
588
|
+
userDb.updateVisibleAgents(parseInt(id), null);
|
|
589
|
+
return res.json({ success: true, message: 'Agent 可见性已重置为默认' });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Validate agents is an array
|
|
593
|
+
if (!Array.isArray(agents)) {
|
|
594
|
+
return res.status(400).json({ error: 'agents 必须是数组' });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Only allow non-empty strings
|
|
598
|
+
const filtered = agents.filter(a => typeof a === 'string' && a.length > 0);
|
|
599
|
+
|
|
600
|
+
userDb.updateVisibleAgents(parseInt(id), filtered);
|
|
601
|
+
res.json({ success: true, message: 'Agent 可见性已更新', agents: filtered });
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error('Error updating user visible agents:', error);
|
|
604
|
+
res.status(500).json({ error: '更新用户 Agent 可见性失败' });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
421
608
|
export default router;
|
package/server/routes/agents.js
CHANGED
|
@@ -7,7 +7,7 @@ import AdmZip from 'adm-zip';
|
|
|
7
7
|
import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
|
|
8
8
|
import { scanAgents, ensureAgentRepo, incrementPatchVersion, publishAgentToRepo } from '../services/system-agent-repo.js';
|
|
9
9
|
import { addProjectManually, loadProjectConfig, saveProjectConfig } from '../projects.js';
|
|
10
|
-
import { agentSubmissionDb } from '../database/db.js';
|
|
10
|
+
import { agentSubmissionDb, userDb } from '../database/db.js';
|
|
11
11
|
import { chatCompletion } from '../services/llm.js';
|
|
12
12
|
|
|
13
13
|
const router = express.Router();
|
|
@@ -292,10 +292,44 @@ async function installMcp(mcpName, repoUrl, userUuid) {
|
|
|
292
292
|
* GET /api/agents
|
|
293
293
|
* List all available agents from the system agent repo
|
|
294
294
|
*/
|
|
295
|
-
router.get('/', async (
|
|
295
|
+
router.get('/', async (req, res) => {
|
|
296
296
|
try {
|
|
297
297
|
const agents = await scanAgents();
|
|
298
|
-
|
|
298
|
+
|
|
299
|
+
// admin/super_admin see all agents
|
|
300
|
+
const userRole = req.user?.role;
|
|
301
|
+
if (userRole === 'admin' || userRole === 'super_admin') {
|
|
302
|
+
return res.json({ agents });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Regular user: filter by visibility whitelist + own approved submissions
|
|
306
|
+
const userId = req.user?.id;
|
|
307
|
+
const visibleSet = new Set();
|
|
308
|
+
|
|
309
|
+
// 1. Admin-configured visible agents
|
|
310
|
+
const raw = userId ? userDb.getVisibleAgents(userId) : null;
|
|
311
|
+
if (raw) {
|
|
312
|
+
try {
|
|
313
|
+
const list = JSON.parse(raw);
|
|
314
|
+
if (Array.isArray(list)) {
|
|
315
|
+
list.forEach(name => visibleSet.add(name));
|
|
316
|
+
}
|
|
317
|
+
} catch (_) { /* ignore */ }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 2. User's own approved submissions (auto-visible)
|
|
321
|
+
if (userId) {
|
|
322
|
+
const approvedNames = agentSubmissionDb.getApprovedAgentNamesByUser(userId);
|
|
323
|
+
approvedNames.forEach(name => visibleSet.add(name));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// If no visible agents at all, return empty list
|
|
327
|
+
if (visibleSet.size === 0) {
|
|
328
|
+
return res.json({ agents: [] });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const filtered = agents.filter(a => visibleSet.has(a.dirName) || visibleSet.has(a.name));
|
|
332
|
+
res.json({ agents: filtered });
|
|
299
333
|
} catch (error) {
|
|
300
334
|
console.error('Error listing agents:', error);
|
|
301
335
|
res.status(500).json({ error: 'Failed to list agents', details: error.message });
|
|
@@ -517,6 +551,42 @@ router.get('/preview', async (req, res) => {
|
|
|
517
551
|
}
|
|
518
552
|
});
|
|
519
553
|
|
|
554
|
+
/**
|
|
555
|
+
* GET /api/agents/check-name/:name
|
|
556
|
+
* Check if an agent name already exists in the repo, and if there's a pending conflict.
|
|
557
|
+
* Returns existing agent config for pre-fill and pending conflict info.
|
|
558
|
+
*/
|
|
559
|
+
router.get('/check-name/:name', async (req, res) => {
|
|
560
|
+
try {
|
|
561
|
+
const { name } = req.params;
|
|
562
|
+
const userId = req.user?.id;
|
|
563
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
564
|
+
|
|
565
|
+
// 1. Check if agent exists in the repo
|
|
566
|
+
const agents = await scanAgents();
|
|
567
|
+
const existing = agents.find(a => a.dirName === name || a.name === name);
|
|
568
|
+
|
|
569
|
+
// 2. Check for pending submissions with the same name from other users
|
|
570
|
+
const pendingConflict = agentSubmissionDb.listAll('pending')
|
|
571
|
+
.find(s => s.agent_name === name && s.user_id !== userId);
|
|
572
|
+
|
|
573
|
+
res.json({
|
|
574
|
+
existing: existing ? {
|
|
575
|
+
display_name: existing.display_name,
|
|
576
|
+
description: existing.description,
|
|
577
|
+
version: existing.version
|
|
578
|
+
} : null,
|
|
579
|
+
pendingConflict: pendingConflict ? {
|
|
580
|
+
username: pendingConflict.username || pendingConflict.email,
|
|
581
|
+
submitted_at: pendingConflict.submitted_at
|
|
582
|
+
} : null
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error('Error checking agent name:', error);
|
|
586
|
+
res.status(500).json({ error: 'Failed to check agent name', details: error.message });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
520
590
|
/**
|
|
521
591
|
* POST /api/agents/generate-description
|
|
522
592
|
* Use LLM to generate an Agent description based on CLAUDE.md + selected skills/MCPs.
|
|
@@ -588,7 +658,7 @@ router.post('/submit', async (req, res) => {
|
|
|
588
658
|
const userId = req.user?.id;
|
|
589
659
|
if (!userUuid || !userId) return res.status(401).json({ error: 'User authentication required' });
|
|
590
660
|
|
|
591
|
-
const { agentName, displayName, description, projectKey, agentYaml, refFiles = [] } = req.body;
|
|
661
|
+
const { agentName, displayName, description, projectKey, agentYaml, refFiles = [], updateNotes } = req.body;
|
|
592
662
|
if (!agentName) return res.status(400).json({ error: 'agentName is required' });
|
|
593
663
|
if (!displayName) return res.status(400).json({ error: 'displayName is required' });
|
|
594
664
|
if (!projectKey) return res.status(400).json({ error: 'projectKey is required' });
|
|
@@ -644,7 +714,8 @@ router.post('/submit', async (req, res) => {
|
|
|
644
714
|
agentName,
|
|
645
715
|
displayName,
|
|
646
716
|
description: description || '',
|
|
647
|
-
zipPath
|
|
717
|
+
zipPath,
|
|
718
|
+
updateNotes: updateNotes || null
|
|
648
719
|
});
|
|
649
720
|
|
|
650
721
|
res.json({
|
|
@@ -793,6 +864,28 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
793
864
|
reviewedBy: reviewerId
|
|
794
865
|
});
|
|
795
866
|
|
|
867
|
+
// Auto-update submitter's installed agent version
|
|
868
|
+
try {
|
|
869
|
+
const submitterUser = userDb.getUserById(submission.user_id);
|
|
870
|
+
if (submitterUser?.uuid) {
|
|
871
|
+
const config = await loadProjectConfig(submitterUser.uuid);
|
|
872
|
+
let updated = false;
|
|
873
|
+
for (const [key, entry] of Object.entries(config)) {
|
|
874
|
+
if (entry.agentInfo?.isAgent && entry.agentInfo.agentName === submission.agent_name) {
|
|
875
|
+
entry.agentInfo.installedVersion = newVersion;
|
|
876
|
+
entry.agentInfo.installedAt = new Date().toISOString();
|
|
877
|
+
updated = true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (updated) {
|
|
881
|
+
await saveProjectConfig(config, submitterUser.uuid);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
// Non-critical: don't fail the approval if auto-update fails
|
|
886
|
+
console.error('[AgentApprove] Failed to auto-update submitter config:', err.message);
|
|
887
|
+
}
|
|
888
|
+
|
|
796
889
|
// Cleanup extracted dir
|
|
797
890
|
fs.rm(extractDir, { recursive: true, force: true }).catch(() => {});
|
|
798
891
|
|
package/server/routes/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ import { initUserDirectories } from '../services/user-directories.js';
|
|
|
8
8
|
import { initSystemRepoForUser } from '../services/system-repo.js';
|
|
9
9
|
import { initSystemMcpRepoForUser } from '../services/system-mcp-repo.js';
|
|
10
10
|
import { sendVerificationCode, isSmtpConfigured } from '../services/email.js';
|
|
11
|
+
import { resolvePermissions } from '../services/feature-permissions.js';
|
|
11
12
|
|
|
12
13
|
const router = express.Router();
|
|
13
14
|
|
|
@@ -213,6 +214,11 @@ router.post('/login', async (req, res) => {
|
|
|
213
214
|
|
|
214
215
|
// Get current user (protected route)
|
|
215
216
|
router.get('/user', authenticateToken, (req, res) => {
|
|
217
|
+
// 查询用户的 feature_permissions 列
|
|
218
|
+
const rawPerms = userDb.getFeaturePermissions(req.user.id);
|
|
219
|
+
const userWithPerms = { ...req.user, feature_permissions: rawPerms };
|
|
220
|
+
const featurePermissions = resolvePermissions(userWithPerms);
|
|
221
|
+
|
|
216
222
|
res.json({
|
|
217
223
|
user: {
|
|
218
224
|
id: req.user.id,
|
|
@@ -220,7 +226,8 @@ router.get('/user', authenticateToken, (req, res) => {
|
|
|
220
226
|
email: req.user.email,
|
|
221
227
|
uuid: req.user.uuid,
|
|
222
228
|
role: req.user.role
|
|
223
|
-
}
|
|
229
|
+
},
|
|
230
|
+
featurePermissions
|
|
224
231
|
});
|
|
225
232
|
});
|
|
226
233
|
|
|
@@ -273,6 +280,19 @@ router.patch('/change-password', authenticateToken, async (req, res) => {
|
|
|
273
280
|
}
|
|
274
281
|
});
|
|
275
282
|
|
|
283
|
+
// Get current user's resolved feature permissions
|
|
284
|
+
router.get('/feature-permissions', authenticateToken, (req, res) => {
|
|
285
|
+
try {
|
|
286
|
+
const rawPerms = userDb.getFeaturePermissions(req.user.id);
|
|
287
|
+
const userWithPerms = { ...req.user, feature_permissions: rawPerms };
|
|
288
|
+
const featurePermissions = resolvePermissions(userWithPerms);
|
|
289
|
+
res.json({ featurePermissions });
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('Error fetching feature permissions:', error);
|
|
292
|
+
res.status(500).json({ error: '获取功能权限失败' });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
276
296
|
// Get current user's spending limit status
|
|
277
297
|
router.get('/limit-status', authenticateToken, (req, res) => {
|
|
278
298
|
try {
|