@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.
- package/dist/assets/index-BHRvJZSD.js +152 -0
- package/dist/assets/index-BhhJnwtA.css +32 -0
- package/dist/assets/{vendor-icons-B1SOlwgw.js → vendor-icons-KP5LHo3O.js} +91 -71
- package/dist/index.html +3 -3
- package/package.json +2 -2
- package/server/database/db.js +59 -11
- package/server/database/init.sql +1 -1
- package/server/routes/admin.js +67 -5
- package/server/routes/auth.js +4 -3
- package/server/routes/usage.js +1 -1
- package/server/services/feishu/sdk-bridge.js +12 -9
- package/dist/assets/index-C7OUtEPY.js +0 -152
- package/dist/assets/index-DJIdQPR_.css +0 -32
package/server/database/db.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|
package/server/database/init.sql
CHANGED
|
@@ -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,
|
package/server/routes/admin.js
CHANGED
|
@@ -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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
package/server/routes/auth.js
CHANGED
|
@@ -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 ? '
|
|
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
|
|
120
|
-
if (role
|
|
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) {
|
package/server/routes/usage.js
CHANGED
|
@@ -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
|
|
418
|
-
if (
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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 || {};
|