@ian2018cs/agenthub 0.1.45 → 0.1.47

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.
@@ -247,6 +247,43 @@ const runMigrations = () => {
247
247
  )
248
248
  `);
249
249
 
250
+ // Migration: Add super_admin role (recreate users table with updated CHECK constraint)
251
+ const hasAnyUser = db.prepare('SELECT COUNT(*) as count FROM users').get().count > 0;
252
+ const hasSuperAdmin = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'super_admin'").get().count > 0;
253
+
254
+ if (hasAnyUser && !hasSuperAdmin) {
255
+ console.log('Running migration: Adding super_admin role and promoting first admin');
256
+ db.exec('PRAGMA foreign_keys = OFF');
257
+ db.transaction(() => {
258
+ db.exec(`CREATE TABLE users_new (
259
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
260
+ username TEXT UNIQUE,
261
+ password_hash TEXT,
262
+ email TEXT UNIQUE,
263
+ uuid TEXT,
264
+ role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user', 'super_admin')),
265
+ status TEXT DEFAULT 'active' CHECK(status IN ('active', 'disabled')),
266
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
267
+ last_login DATETIME,
268
+ is_active BOOLEAN DEFAULT 1,
269
+ git_name TEXT,
270
+ git_email TEXT,
271
+ has_completed_onboarding BOOLEAN DEFAULT 0,
272
+ total_limit_usd REAL DEFAULT NULL,
273
+ daily_limit_usd REAL DEFAULT NULL
274
+ )`);
275
+ db.exec('INSERT INTO users_new SELECT id,username,password_hash,email,uuid,role,status,created_at,last_login,is_active,git_name,git_email,has_completed_onboarding,total_limit_usd,daily_limit_usd FROM users');
276
+ db.exec('DROP TABLE users');
277
+ db.exec('ALTER TABLE users_new RENAME TO users');
278
+ db.exec("UPDATE users SET role='super_admin' WHERE id=(SELECT MIN(id) FROM users WHERE role='admin')");
279
+ })();
280
+ db.exec('PRAGMA foreign_keys = ON');
281
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_uuid ON users(uuid)');
282
+ db.exec('CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)');
283
+ db.exec('CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)');
284
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email)');
285
+ }
286
+
250
287
  // 聊天图片持久化关联表
251
288
  db.exec(`
252
289
  CREATE TABLE IF NOT EXISTS message_images (
@@ -438,6 +475,15 @@ const userDb = {
438
475
  }
439
476
  },
440
477
 
478
+ // Update user role
479
+ updateUserRole: (userId, role) => {
480
+ try {
481
+ db.prepare('UPDATE users SET role = ? WHERE id = ?').run(role, userId);
482
+ } catch (err) {
483
+ throw err;
484
+ }
485
+ },
486
+
441
487
  // Update user status
442
488
  updateUserStatus: (userId, status) => {
443
489
  try {
@@ -795,13 +841,13 @@ const usageDb = {
795
841
  try {
796
842
  return db.prepare(`
797
843
  SELECT
798
- user_uuid,
799
- SUM(total_cost_usd) as total_cost,
800
- SUM(request_count) as total_requests,
801
- SUM(session_count) as total_sessions,
802
- MAX(date) as last_active
803
- FROM usage_daily_summary
804
- GROUP BY user_uuid
844
+ uds.user_uuid,
845
+ SUM(uds.total_cost_usd) as total_cost,
846
+ SUM(uds.request_count) as total_requests,
847
+ (SELECT COUNT(DISTINCT ur.session_id) FROM usage_records ur WHERE ur.user_uuid = uds.user_uuid AND ur.session_id IS NOT NULL) as total_sessions,
848
+ MAX(uds.date) as last_active
849
+ FROM usage_daily_summary uds
850
+ GROUP BY uds.user_uuid
805
851
  ORDER BY total_cost DESC
806
852
  `).all();
807
853
  } catch (err) {
@@ -839,10 +885,10 @@ const usageDb = {
839
885
  SUM(total_input_tokens) as total_input_tokens,
840
886
  SUM(total_output_tokens) as total_output_tokens,
841
887
  SUM(request_count) as total_requests,
842
- SUM(session_count) as total_sessions
888
+ (SELECT COUNT(DISTINCT session_id) FROM usage_records WHERE user_uuid = ? AND session_id IS NOT NULL) as total_sessions
843
889
  FROM usage_daily_summary
844
890
  WHERE user_uuid = ?
845
- `).get(userUuid);
891
+ `).get(userUuid, userUuid);
846
892
  } catch (err) {
847
893
  throw err;
848
894
  }
@@ -859,6 +905,7 @@ const usageDb = {
859
905
  COUNT(*) as requests
860
906
  FROM usage_records
861
907
  WHERE user_uuid = ? AND created_at >= ? AND created_at <= ?
908
+ AND COALESCE(raw_model, model) != '<synthetic>'
862
909
  GROUP BY COALESCE(raw_model, model)
863
910
  ORDER BY cost DESC
864
911
  `).all(userUuid, startDate + 'T00:00:00', endDate + 'T23:59:59');
@@ -874,11 +921,11 @@ const usageDb = {
874
921
  SELECT
875
922
  SUM(total_cost_usd) as total_cost,
876
923
  SUM(request_count) as total_requests,
877
- SUM(session_count) as total_sessions,
924
+ (SELECT COUNT(DISTINCT session_id) FROM usage_records WHERE date(created_at) >= ? AND date(created_at) <= ? AND session_id IS NOT NULL) as total_sessions,
878
925
  COUNT(DISTINCT user_uuid) as active_users
879
926
  FROM usage_daily_summary
880
927
  WHERE date >= ? AND date <= ?
881
- `).get(startDate, endDate);
928
+ `).get(startDate, endDate, startDate, endDate);
882
929
 
883
930
  const dailyTrend = db.prepare(`
884
931
  SELECT
@@ -899,6 +946,7 @@ const usageDb = {
899
946
  COUNT(*) as requests
900
947
  FROM usage_records
901
948
  WHERE created_at >= ? AND created_at <= ?
949
+ AND COALESCE(raw_model, model) != '<synthetic>'
902
950
  GROUP BY COALESCE(raw_model, model)
903
951
  ORDER BY cost DESC
904
952
  `).all(startDate + 'T00:00:00', endDate + 'T23:59:59');
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS users (
11
11
  password_hash TEXT,
12
12
  email TEXT UNIQUE,
13
13
  uuid TEXT,
14
- role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user')),
14
+ role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user', 'super_admin')),
15
15
  status TEXT DEFAULT 'active' CHECK(status IN ('active', 'disabled')),
16
16
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
17
17
  last_login DATETIME,
@@ -7,14 +7,22 @@ import { deleteUserDirectories, initUserDirectories } from '../services/user-dir
7
7
 
8
8
  const router = express.Router();
9
9
 
10
- // Admin middleware
10
+ // Admin middleware - allows both admin and super_admin
11
11
  const requireAdmin = (req, res, next) => {
12
- if (req.user.role !== 'admin') {
12
+ if (req.user.role !== 'admin' && req.user.role !== 'super_admin') {
13
13
  return res.status(403).json({ error: 'Admin access required' });
14
14
  }
15
15
  next();
16
16
  };
17
17
 
18
+ // Super admin only middleware
19
+ const requireSuperAdmin = (req, res, next) => {
20
+ if (req.user.role !== 'super_admin') {
21
+ return res.status(403).json({ error: 'Super admin access required' });
22
+ }
23
+ next();
24
+ };
25
+
18
26
  // Apply auth and admin middleware to all routes
19
27
  router.use(authenticateToken);
20
28
  router.use(requireAdmin);
@@ -108,6 +116,11 @@ router.patch('/users/:id', (req, res) => {
108
116
  return res.status(404).json({ error: 'User not found' });
109
117
  }
110
118
 
119
+ // admin cannot operate on other admin/super_admin users
120
+ if (req.user.role === 'admin' && ['admin', 'super_admin'].includes(user.role)) {
121
+ return res.status(403).json({ error: '无权操作管理员账户' });
122
+ }
123
+
111
124
  userDb.updateUserStatus(id, status);
112
125
  res.json({ success: true, message: `User status updated to ${status}` });
113
126
  } catch (error) {
@@ -131,6 +144,11 @@ router.delete('/users/:id', async (req, res) => {
131
144
  return res.status(404).json({ error: 'User not found' });
132
145
  }
133
146
 
147
+ // admin cannot delete other admin/super_admin users
148
+ if (req.user.role === 'admin' && ['admin', 'super_admin'].includes(user.role)) {
149
+ return res.status(403).json({ error: '无权删除管理员账户' });
150
+ }
151
+
134
152
  // Delete user directories
135
153
  if (user.uuid) {
136
154
  await deleteUserDirectories(user.uuid);
@@ -167,6 +185,11 @@ router.patch('/users/:id/password', async (req, res) => {
167
185
  return res.status(404).json({ error: '用户不存在' });
168
186
  }
169
187
 
188
+ // admin cannot reset password of other admin/super_admin users
189
+ if (req.user.role === 'admin' && ['admin', 'super_admin'].includes(user.role)) {
190
+ return res.status(403).json({ error: '无权重置管理员密码' });
191
+ }
192
+
170
193
  // Can only reset password for password-login users
171
194
  if (!user.password_hash) {
172
195
  return res.status(400).json({ error: '该用户使用邮箱验证码登录,无法重置密码' });
@@ -186,6 +209,37 @@ router.patch('/users/:id/password', async (req, res) => {
186
209
  }
187
210
  });
188
211
 
212
+ // Update user role (super_admin only)
213
+ router.patch('/users/:id/role', requireSuperAdmin, (req, res) => {
214
+ try {
215
+ const { id } = req.params;
216
+ const { role } = req.body;
217
+
218
+ if (!['admin', 'user'].includes(role)) {
219
+ return res.status(400).json({ error: '只能设置为 admin 或 user' });
220
+ }
221
+
222
+ if (parseInt(id) === req.user.id) {
223
+ return res.status(400).json({ error: '不能修改自己的角色' });
224
+ }
225
+
226
+ const user = userDb.getUserById(id);
227
+ if (!user) {
228
+ return res.status(404).json({ error: 'User not found' });
229
+ }
230
+
231
+ if (user.role === 'super_admin') {
232
+ return res.status(403).json({ error: '不能修改超级管理员的角色' });
233
+ }
234
+
235
+ userDb.updateUserRole(id, role);
236
+ res.json({ success: true, message: '用户角色已更新' });
237
+ } catch (error) {
238
+ console.error('Error updating user role:', error);
239
+ res.status(500).json({ error: '更新用户角色失败' });
240
+ }
241
+ });
242
+
189
243
  // ==================== User Spending Limits ====================
190
244
 
191
245
  // Get user spending limits
@@ -227,9 +281,17 @@ router.patch('/users/:id/limits', (req, res) => {
227
281
  }
228
282
  }
229
283
 
230
- // Prevent modifying own limits
231
- if (parseInt(id) === req.user.id) {
232
- return res.status(400).json({ error: '不能修改自己的额度限制' });
284
+ const isSelf = parseInt(id) === req.user.id;
285
+
286
+ // admin can only set limits for themselves or for user-role accounts
287
+ if (req.user.role === 'admin' && !isSelf) {
288
+ const targetUser = userDb.getUserById(id);
289
+ if (!targetUser) {
290
+ return res.status(404).json({ error: 'User not found' });
291
+ }
292
+ if (['admin', 'super_admin'].includes(targetUser.role)) {
293
+ return res.status(403).json({ error: '无权修改管理员账户的限额' });
294
+ }
233
295
  }
234
296
 
235
297
  const user = userDb.getUserById(id);
@@ -111,13 +111,13 @@ router.post('/verify-code', async (req, res) => {
111
111
  // If user doesn't exist, create new user (registration)
112
112
  if (!user) {
113
113
  const userCount = userDb.getUserCount();
114
- const role = userCount === 0 ? 'admin' : 'user';
114
+ const role = userCount === 0 ? 'super_admin' : 'user';
115
115
  const uuid = uuidv4();
116
116
 
117
117
  user = userDb.createUserWithEmail(email, uuid, role);
118
118
 
119
- // Apply default total limit for non-admin users
120
- if (role !== 'admin') {
119
+ // Apply default total limit for regular users only
120
+ if (role === 'user') {
121
121
  const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
122
122
  if (defaultTotalLimit !== null) {
123
123
  userDb.updateUserLimits(user.id, parseFloat(defaultTotalLimit), null);
@@ -276,6 +276,7 @@ router.patch('/change-password', authenticateToken, async (req, res) => {
276
276
  // Get current user's spending limit status
277
277
  router.get('/limit-status', authenticateToken, (req, res) => {
278
278
  try {
279
+ if (req.user.role === 'super_admin') return res.json({ allowed: true });
279
280
  const status = usageDb.checkUserLimits(req.user.uuid);
280
281
  res.json(status);
281
282
  } catch (error) {
@@ -7,7 +7,7 @@ const router = express.Router();
7
7
 
8
8
  // Admin middleware
9
9
  const requireAdmin = (req, res, next) => {
10
- if (req.user.role !== 'admin') {
10
+ if (req.user.role !== 'admin' && req.user.role !== 'super_admin') {
11
11
  return res.status(403).json({ error: 'Admin access required' });
12
12
  }
13
13
  next();
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { queryClaudeSDK } from '../../claude-sdk.js';
19
- import { usageDb } from '../../database/db.js';
19
+ import { usageDb, userDb } from '../../database/db.js';
20
20
  import { feishuDb } from './feishu-db.js';
21
21
  import fs from 'fs/promises';
22
22
  import path from 'path';
@@ -413,14 +413,17 @@ async function runQuery({
413
413
  larkClient,
414
414
  pendingApprovals,
415
415
  }) {
416
- // 限额检查
417
- const limitStatus = usageDb.checkUserLimits(userUuid);
418
- if (!limitStatus.allowed) {
419
- const reason = limitStatus.reason === 'daily_limit_exceeded'
420
- ? `每日用量已达上限 ($${limitStatus.limit.toFixed(2)})`
421
- : `总用量已达上限 ($${limitStatus.limit.toFixed(2)})`;
422
- await larkClient.sendText(chatId, `⚠️ ${reason},请联系管理员。`);
423
- return;
416
+ // 限额检查(super_admin 豁免)
417
+ const feishuUser = userDb.getUserByUuid(userUuid);
418
+ if (feishuUser?.role !== 'super_admin') {
419
+ const limitStatus = usageDb.checkUserLimits(userUuid);
420
+ if (!limitStatus.allowed) {
421
+ const reason = limitStatus.reason === 'daily_limit_exceeded'
422
+ ? `每日用量已达上限 ($${limitStatus.limit.toFixed(2)})`
423
+ : `总用量已达上限 ($${limitStatus.limit.toFixed(2)})`;
424
+ await larkClient.sendText(chatId, `⚠️ ${reason},请联系管理员。`);
425
+ return;
426
+ }
424
427
  }
425
428
 
426
429
  const { claude_session_id, cwd, permission_mode } = state || {};