@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.
@@ -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
- // Send completion event
779
- console.log('Streaming complete, sending claude-complete event');
780
- ws.send({
781
- type: 'claude-complete',
782
- sessionId: capturedSessionId,
783
- exitCode: 0,
784
- isNewSession: !sessionId && !!command
785
- });
786
- console.log('claude-complete event sent');
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
- // Call interrupt() on the query instance
826
- await session.instance.interrupt();
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';
@@ -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
 
@@ -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;
@@ -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 (_req, res) => {
295
+ router.get('/', async (req, res) => {
296
296
  try {
297
297
  const agents = await scanAgents();
298
- res.json({ agents });
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
 
@@ -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 {