@ian2018cs/agenthub 0.1.65 → 0.1.67
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-BjMqwOUf.css +32 -0
- package/dist/assets/index-DLZUEIt1.js +184 -0
- package/dist/index.html +4 -4
- package/package.json +4 -3
- package/server/claude-sdk.js +20 -3
- package/server/database/db.js +31 -0
- package/server/index.js +17 -1
- package/server/projects.js +5 -0
- package/server/routes/admin.js +37 -8
- package/server/routes/agents.js +145 -18
- package/server/routes/auth.js +8 -3
- package/server/services/email.js +4 -3
- package/server/services/feishu/card-builder.js +3 -1
- package/server/services/feishu/command-handler.js +4 -3
- package/server/services/feishu/feishu-engine.js +3 -2
- package/server/services/image-storage.js +13 -0
- package/server/services/system-agent-repo.js +8 -1
- package/shared/brand.js +27 -0
- package/dist/assets/index-BAFclCJK.css +0 -32
- package/dist/assets/index-CTUPAZem.js +0 -184
package/dist/index.html
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
8
8
|
<title>AgentHub</title>
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
<!-- PWA Manifest -->
|
|
11
11
|
<link rel="manifest" href="/manifest.json" />
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
<!-- iOS Safari PWA Meta Tags -->
|
|
14
14
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
15
15
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-DLZUEIt1.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-Bv0Nkan8.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-sVRjxPVQ.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-CjscLcYM.js">
|
|
35
35
|
<link rel="modulepreload" crossorigin href="/assets/vendor-syntax-BKENXTeY.js">
|
|
36
36
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CvdiG4-n.js">
|
|
37
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
37
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BjMqwOUf.css">
|
|
38
38
|
</head>
|
|
39
39
|
<body>
|
|
40
40
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian2018cs/agenthub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "A web-based UI for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"build": "vite build",
|
|
35
35
|
"preview": "vite preview",
|
|
36
36
|
"start": "npm run build && npm run server",
|
|
37
|
-
"release": "./release.sh"
|
|
37
|
+
"release": "./release.sh",
|
|
38
|
+
"rebrand": "node rebrand.js"
|
|
38
39
|
},
|
|
39
40
|
"keywords": [
|
|
40
41
|
"claude coode",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"access": "public"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
54
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.74",
|
|
54
55
|
"@codemirror/lang-css": "^6.3.1",
|
|
55
56
|
"@codemirror/lang-html": "^6.4.9",
|
|
56
57
|
"@codemirror/lang-javascript": "^6.2.4",
|
package/server/claude-sdk.js
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
* - WebSocket message streaming
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
15
|
+
import { query, renameSession } from '@anthropic-ai/claude-agent-sdk';
|
|
16
16
|
// Used to mint unique approval request IDs when randomUUID is not available.
|
|
17
17
|
// This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
|
|
18
18
|
import crypto from 'crypto';
|
|
19
19
|
import { promises as fs } from 'fs';
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { CLAUDE_MODELS } from '../shared/modelConstants.js';
|
|
22
|
+
import { PRODUCT_SYSTEM_DESC } from '../shared/brand.js';
|
|
22
23
|
import { getUserPaths } from './services/user-directories.js';
|
|
23
24
|
import { usageDb } from './database/db.js';
|
|
24
25
|
import { calculateCost, normalizeModelName } from './services/pricing.js';
|
|
@@ -205,7 +206,7 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
205
206
|
console.log(`Using model: ${sdkOptions.model}`);
|
|
206
207
|
|
|
207
208
|
// Map system prompt configuration
|
|
208
|
-
const baseAppend =
|
|
209
|
+
const baseAppend = PRODUCT_SYSTEM_DESC;
|
|
209
210
|
const extraAppend = options.appendSystemPrompt ? `\n\n${options.appendSystemPrompt}` : '';
|
|
210
211
|
sdkOptions.systemPrompt = {
|
|
211
212
|
type: 'preset',
|
|
@@ -612,7 +613,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
612
613
|
|
|
613
614
|
// Wait for the UI; if the SDK cancels, notify the UI so it can dismiss the banner.
|
|
614
615
|
// This does not retry or resurface the prompt; it just reflects the cancellation.
|
|
616
|
+
// AskUserQuestion needs more time for the user to type a response.
|
|
617
|
+
const approvalTimeoutMs = toolName === 'AskUserQuestion' ? 3 * 60 * 1000 : TOOL_APPROVAL_TIMEOUT_MS;
|
|
615
618
|
const decision = await waitForToolApproval(requestId, {
|
|
619
|
+
timeoutMs: approvalTimeoutMs,
|
|
616
620
|
signal: context?.signal,
|
|
617
621
|
onCancel: (reason) => {
|
|
618
622
|
ws.send({
|
|
@@ -892,11 +896,24 @@ function getActiveClaudeSDKSessions() {
|
|
|
892
896
|
return getAllSessions();
|
|
893
897
|
}
|
|
894
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Rename a session for a specific user
|
|
901
|
+
* @param {string} sessionId - Session UUID
|
|
902
|
+
* @param {string} title - New session title
|
|
903
|
+
* @param {string} userUuid - User UUID for directory isolation
|
|
904
|
+
*/
|
|
905
|
+
async function renameSessionForUser(sessionId, title, userUuid) {
|
|
906
|
+
const userPaths = getUserPaths(userUuid);
|
|
907
|
+
process.env.CLAUDE_CONFIG_DIR = userPaths.claudeDir;
|
|
908
|
+
await renameSession(sessionId, title);
|
|
909
|
+
}
|
|
910
|
+
|
|
895
911
|
// Export public API
|
|
896
912
|
export {
|
|
897
913
|
queryClaudeSDK,
|
|
898
914
|
abortClaudeSDKSession,
|
|
899
915
|
isClaudeSDKSessionActive,
|
|
900
916
|
getActiveClaudeSDKSessions,
|
|
901
|
-
resolveToolApproval
|
|
917
|
+
resolveToolApproval,
|
|
918
|
+
renameSessionForUser
|
|
902
919
|
};
|
package/server/database/db.js
CHANGED
|
@@ -1143,6 +1143,12 @@ const usageDb = {
|
|
|
1143
1143
|
} catch (err) {
|
|
1144
1144
|
throw err;
|
|
1145
1145
|
}
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
// 删除用户所有用量记录(用户删除时调用)
|
|
1149
|
+
deleteByUserUuid: (userUuid) => {
|
|
1150
|
+
db.prepare('DELETE FROM usage_records WHERE user_uuid = ?').run(userUuid);
|
|
1151
|
+
db.prepare('DELETE FROM usage_daily_summary WHERE user_uuid = ?').run(userUuid);
|
|
1146
1152
|
}
|
|
1147
1153
|
};
|
|
1148
1154
|
|
|
@@ -1311,6 +1317,16 @@ const feishuDb = {
|
|
|
1311
1317
|
DELETE FROM feishu_processed_messages
|
|
1312
1318
|
WHERE processed_at < datetime('now', '-48 hours')
|
|
1313
1319
|
`).run();
|
|
1320
|
+
},
|
|
1321
|
+
|
|
1322
|
+
// 删除用户所有飞书相关数据(用户删除时调用)
|
|
1323
|
+
deleteByUserUuid: (userUuid) => {
|
|
1324
|
+
// 先查出所有绑定的 feishu_open_id,清理 session_state
|
|
1325
|
+
const bindings = db.prepare('SELECT feishu_open_id FROM feishu_bindings WHERE user_uuid = ?').all(userUuid);
|
|
1326
|
+
for (const b of bindings) {
|
|
1327
|
+
db.prepare('DELETE FROM feishu_session_state WHERE feishu_open_id = ?').run(b.feishu_open_id);
|
|
1328
|
+
}
|
|
1329
|
+
db.prepare('DELETE FROM feishu_bindings WHERE user_uuid = ?').run(userUuid);
|
|
1314
1330
|
}
|
|
1315
1331
|
};
|
|
1316
1332
|
|
|
@@ -1383,6 +1399,16 @@ const imageDb = {
|
|
|
1383
1399
|
WHERE user_uuid = ? AND file_hash = ? AND id NOT IN (${placeholders})
|
|
1384
1400
|
`).get(userUuid, fileHash, ...excludeIds);
|
|
1385
1401
|
return row.count;
|
|
1402
|
+
},
|
|
1403
|
+
|
|
1404
|
+
// 查询用户所有图片记录(用于删除文件)
|
|
1405
|
+
getImagesByUserUuid: (userUuid) => {
|
|
1406
|
+
return db.prepare('SELECT file_hash, file_ext FROM message_images WHERE user_uuid = ?').all(userUuid);
|
|
1407
|
+
},
|
|
1408
|
+
|
|
1409
|
+
// 删除用户所有图片 DB 记录(用户删除时调用)
|
|
1410
|
+
deleteByUserUuid: (userUuid) => {
|
|
1411
|
+
db.prepare('DELETE FROM message_images WHERE user_uuid = ?').run(userUuid);
|
|
1386
1412
|
}
|
|
1387
1413
|
};
|
|
1388
1414
|
|
|
@@ -1442,6 +1468,11 @@ const agentSubmissionDb = {
|
|
|
1442
1468
|
return db.prepare(`
|
|
1443
1469
|
SELECT DISTINCT agent_name FROM agent_submissions WHERE user_id = ? AND status = 'approved'
|
|
1444
1470
|
`).all(userId).map(row => row.agent_name);
|
|
1471
|
+
},
|
|
1472
|
+
|
|
1473
|
+
// 删除用户所有 Agent 提交记录(用户删除时调用)
|
|
1474
|
+
deleteByUserId: (userId) => {
|
|
1475
|
+
db.prepare('DELETE FROM agent_submissions WHERE user_id = ?').run(userId);
|
|
1445
1476
|
}
|
|
1446
1477
|
};
|
|
1447
1478
|
|
package/server/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import fetch from 'node-fetch';
|
|
|
43
43
|
import mime from 'mime-types';
|
|
44
44
|
|
|
45
45
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, updateProjectLastActivity } from './projects.js';
|
|
46
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
|
|
46
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, renameSessionForUser } from './claude-sdk.js';
|
|
47
47
|
import authRoutes from './routes/auth.js';
|
|
48
48
|
import mcpRoutes from './routes/mcp.js';
|
|
49
49
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
@@ -439,6 +439,22 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
439
439
|
}
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
+
// Rename session endpoint
|
|
443
|
+
app.put('/api/projects/:projectName/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
|
444
|
+
try {
|
|
445
|
+
const { sessionId } = req.params;
|
|
446
|
+
const { title } = req.body;
|
|
447
|
+
if (!title || !title.trim()) {
|
|
448
|
+
return res.status(400).json({ error: 'Title is required' });
|
|
449
|
+
}
|
|
450
|
+
await renameSessionForUser(sessionId, title.trim(), req.user.uuid);
|
|
451
|
+
res.json({ success: true });
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
|
454
|
+
res.status(500).json({ error: error.message });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
442
458
|
// Delete project endpoint
|
|
443
459
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
444
460
|
try {
|
package/server/projects.js
CHANGED
|
@@ -536,6 +536,11 @@ async function parseJsonlSessions(filePath) {
|
|
|
536
536
|
session.summary = entry.summary;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
// Update summary from custom-title entries (user-set via renameSession SDK)
|
|
540
|
+
if (entry.type === 'custom-title' && entry.customTitle) {
|
|
541
|
+
session.summary = entry.customTitle;
|
|
542
|
+
}
|
|
543
|
+
|
|
539
544
|
// Track last user and assistant messages (skip system messages)
|
|
540
545
|
if (entry.message?.role === 'user' && entry.message?.content) {
|
|
541
546
|
const content = entry.message.content;
|
package/server/routes/admin.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import bcrypt from 'bcrypt';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import { userDb, domainWhitelistDb, settingsDb } from '../database/db.js';
|
|
4
|
+
import { userDb, domainWhitelistDb, settingsDb, usageDb, feishuDb, imageDb, agentSubmissionDb } from '../database/db.js';
|
|
5
5
|
import { authenticateToken } from '../middleware/auth.js';
|
|
6
6
|
import { deleteUserDirectories, initUserDirectories } from '../services/user-directories.js';
|
|
7
7
|
import { getDefaultPermissions, ALL_FEATURE_PERMISSIONS } from '../services/feature-permissions.js';
|
|
8
|
+
import { deleteUserImageDir } from '../services/image-storage.js';
|
|
8
9
|
|
|
9
10
|
const router = express.Router();
|
|
10
11
|
|
|
@@ -68,10 +69,15 @@ router.post('/users', async (req, res) => {
|
|
|
68
69
|
const uuid = uuidv4();
|
|
69
70
|
const user = userDb.createUserFull(username, passwordHash, uuid, 'user');
|
|
70
71
|
|
|
71
|
-
// Apply default
|
|
72
|
+
// Apply default limits
|
|
72
73
|
const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const defaultDailyLimit = settingsDb.get('default_daily_limit_usd');
|
|
75
|
+
if (defaultTotalLimit !== null || defaultDailyLimit !== null) {
|
|
76
|
+
userDb.updateUserLimits(
|
|
77
|
+
user.id,
|
|
78
|
+
defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null,
|
|
79
|
+
defaultDailyLimit !== null ? parseFloat(defaultDailyLimit) : null
|
|
80
|
+
);
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
// Initialize user directories
|
|
@@ -150,11 +156,19 @@ router.delete('/users/:id', async (req, res) => {
|
|
|
150
156
|
return res.status(403).json({ error: '无权删除管理员账户' });
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
// Delete user directories
|
|
159
|
+
// Delete user directories (config + projects)
|
|
154
160
|
if (user.uuid) {
|
|
155
161
|
await deleteUserDirectories(user.uuid);
|
|
162
|
+
// 删除聊天图片文件目录
|
|
163
|
+
await deleteUserImageDir(user.uuid);
|
|
156
164
|
}
|
|
157
165
|
|
|
166
|
+
// 删除数据库中所有关联数据
|
|
167
|
+
usageDb.deleteByUserUuid(user.uuid); // usage_records + usage_daily_summary
|
|
168
|
+
feishuDb.deleteByUserUuid(user.uuid); // feishu_bindings + feishu_session_state
|
|
169
|
+
imageDb.deleteByUserUuid(user.uuid); // message_images
|
|
170
|
+
agentSubmissionDb.deleteByUserId(user.id); // agent_submissions
|
|
171
|
+
|
|
158
172
|
// Delete from database
|
|
159
173
|
userDb.deleteUserById(id);
|
|
160
174
|
|
|
@@ -382,8 +396,10 @@ router.delete('/email-domains/:id', (req, res) => {
|
|
|
382
396
|
router.get('/settings/default-limits', (req, res) => {
|
|
383
397
|
try {
|
|
384
398
|
const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
|
|
399
|
+
const defaultDailyLimit = settingsDb.get('default_daily_limit_usd');
|
|
385
400
|
res.json({
|
|
386
|
-
default_total_limit_usd: defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null
|
|
401
|
+
default_total_limit_usd: defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null,
|
|
402
|
+
default_daily_limit_usd: defaultDailyLimit !== null ? parseFloat(defaultDailyLimit) : null,
|
|
387
403
|
});
|
|
388
404
|
} catch (error) {
|
|
389
405
|
console.error('Error fetching default limits:', error);
|
|
@@ -394,7 +410,7 @@ router.get('/settings/default-limits', (req, res) => {
|
|
|
394
410
|
// Update default spending limits for new users
|
|
395
411
|
router.put('/settings/default-limits', (req, res) => {
|
|
396
412
|
try {
|
|
397
|
-
const { default_total_limit_usd } = req.body;
|
|
413
|
+
const { default_total_limit_usd, default_daily_limit_usd } = req.body;
|
|
398
414
|
|
|
399
415
|
if (default_total_limit_usd !== null && default_total_limit_usd !== undefined) {
|
|
400
416
|
if (typeof default_total_limit_usd !== 'number' || default_total_limit_usd < 0) {
|
|
@@ -402,16 +418,29 @@ router.put('/settings/default-limits', (req, res) => {
|
|
|
402
418
|
}
|
|
403
419
|
}
|
|
404
420
|
|
|
421
|
+
if (default_daily_limit_usd !== null && default_daily_limit_usd !== undefined) {
|
|
422
|
+
if (typeof default_daily_limit_usd !== 'number' || default_daily_limit_usd < 0) {
|
|
423
|
+
return res.status(400).json({ error: '默认日限制必须是正数或为空' });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
405
427
|
if (default_total_limit_usd === null || default_total_limit_usd === undefined) {
|
|
406
428
|
settingsDb.delete('default_total_limit_usd');
|
|
407
429
|
} else {
|
|
408
430
|
settingsDb.set('default_total_limit_usd', String(default_total_limit_usd), req.user.id);
|
|
409
431
|
}
|
|
410
432
|
|
|
433
|
+
if (default_daily_limit_usd === null || default_daily_limit_usd === undefined) {
|
|
434
|
+
settingsDb.delete('default_daily_limit_usd');
|
|
435
|
+
} else {
|
|
436
|
+
settingsDb.set('default_daily_limit_usd', String(default_daily_limit_usd), req.user.id);
|
|
437
|
+
}
|
|
438
|
+
|
|
411
439
|
res.json({
|
|
412
440
|
success: true,
|
|
413
441
|
message: '默认限额已更新',
|
|
414
|
-
default_total_limit_usd: default_total_limit_usd ?? null
|
|
442
|
+
default_total_limit_usd: default_total_limit_usd ?? null,
|
|
443
|
+
default_daily_limit_usd: default_daily_limit_usd ?? null,
|
|
415
444
|
});
|
|
416
445
|
} catch (error) {
|
|
417
446
|
console.error('Error updating default limits:', error);
|
package/server/routes/agents.js
CHANGED
|
@@ -14,6 +14,26 @@ const router = express.Router();
|
|
|
14
14
|
|
|
15
15
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Recursively list all files under a directory, returning relative paths.
|
|
19
|
+
* Excludes agent.yaml (metadata file, never copied to user projects).
|
|
20
|
+
*/
|
|
21
|
+
async function listAgentFiles(dir, base = dir) {
|
|
22
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
23
|
+
const files = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.name === 'agent.yaml') continue;
|
|
26
|
+
const fullPath = path.join(dir, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
const sub = await listAgentFiles(fullPath, base);
|
|
29
|
+
files.push(...sub);
|
|
30
|
+
} else {
|
|
31
|
+
files.push(path.relative(base, fullPath));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
function isSshUrl(url) {
|
|
18
38
|
return /^[a-zA-Z0-9_-]+@[a-zA-Z0-9.-]+:.+$/.test(url);
|
|
19
39
|
}
|
|
@@ -419,6 +439,7 @@ router.post('/install', async (req, res) => {
|
|
|
419
439
|
|
|
420
440
|
// Copy all agent files to project directory (excluding agent.yaml metadata)
|
|
421
441
|
// This includes CLAUDE.md and any referenced files in subdirectories
|
|
442
|
+
let installedFiles = [];
|
|
422
443
|
try {
|
|
423
444
|
const agentEntries = await fs.readdir(agent.path, { withFileTypes: true });
|
|
424
445
|
for (const entry of agentEntries) {
|
|
@@ -437,6 +458,7 @@ router.post('/install', async (req, res) => {
|
|
|
437
458
|
}
|
|
438
459
|
}
|
|
439
460
|
}
|
|
461
|
+
installedFiles = await listAgentFiles(agent.path);
|
|
440
462
|
} catch (err) {
|
|
441
463
|
console.error('[AgentInstall] Failed to copy agent files:', err.message);
|
|
442
464
|
}
|
|
@@ -477,7 +499,8 @@ router.post('/install', async (req, res) => {
|
|
|
477
499
|
installedVersion: agent.version,
|
|
478
500
|
installedAt: new Date().toISOString(),
|
|
479
501
|
isAgent: true,
|
|
480
|
-
claudeMdHash
|
|
502
|
+
claudeMdHash,
|
|
503
|
+
installedFiles
|
|
481
504
|
}
|
|
482
505
|
};
|
|
483
506
|
await saveProjectConfig(config, userUuid);
|
|
@@ -893,6 +916,8 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
893
916
|
return res.status(403).json({ error: 'Admin access required' });
|
|
894
917
|
}
|
|
895
918
|
|
|
919
|
+
const { updateAllUsers = false } = req.body;
|
|
920
|
+
|
|
896
921
|
const submission = agentSubmissionDb.getById(req.params.id);
|
|
897
922
|
if (!submission) return res.status(404).json({ error: 'Submission not found' });
|
|
898
923
|
if (submission.status !== 'pending') {
|
|
@@ -934,32 +959,134 @@ router.post('/submissions/:id/approve', async (req, res) => {
|
|
|
934
959
|
reviewedBy: reviewerId
|
|
935
960
|
});
|
|
936
961
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
962
|
+
let updatedUsersCount = 0;
|
|
963
|
+
|
|
964
|
+
if (updateAllUsers) {
|
|
965
|
+
// Force-update all users who have this agent installed
|
|
966
|
+
try {
|
|
967
|
+
const freshAgents = await scanAgents();
|
|
968
|
+
const publishedAgent = freshAgents.find(a =>
|
|
969
|
+
a.dirName === submission.agent_name || a.name === submission.agent_name
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const allUsers = userDb.getAllUsers();
|
|
973
|
+
for (const user of allUsers) {
|
|
974
|
+
if (!user.uuid) continue;
|
|
975
|
+
try {
|
|
976
|
+
const config = await loadProjectConfig(user.uuid);
|
|
977
|
+
const userPaths = getUserPaths(user.uuid);
|
|
978
|
+
let userUpdated = false;
|
|
979
|
+
|
|
980
|
+
for (const [, entry] of Object.entries(config)) {
|
|
981
|
+
if (!entry.agentInfo?.isAgent || entry.agentInfo.agentName !== submission.agent_name) continue;
|
|
982
|
+
|
|
983
|
+
const projectDir = path.join(userPaths.projectsDir, submission.agent_name);
|
|
984
|
+
let newClaudeMdHash = entry.agentInfo.claudeMdHash;
|
|
985
|
+
let newInstalledFiles = entry.agentInfo.installedFiles;
|
|
986
|
+
|
|
987
|
+
// Force-copy agent files from repo to user's project directory
|
|
988
|
+
if (publishedAgent?.path) {
|
|
989
|
+
try {
|
|
990
|
+
// Compute new file list from the published agent
|
|
991
|
+
const newFileList = await listAgentFiles(publishedAgent.path);
|
|
992
|
+
|
|
993
|
+
// Remove files that were in the old version but are not in the new version.
|
|
994
|
+
// Only remove files we know were installed by the agent (tracked in installedFiles).
|
|
995
|
+
// This preserves any files the user created themselves.
|
|
996
|
+
const oldFileList = entry.agentInfo.installedFiles || [];
|
|
997
|
+
const filesToRemove = oldFileList.filter(f => !newFileList.includes(f));
|
|
998
|
+
for (const relPath of filesToRemove) {
|
|
999
|
+
await fs.rm(path.join(projectDir, relPath), { force: true }).catch(() => {});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Clean up directories that became empty after file removal.
|
|
1003
|
+
// Collect unique parent dirs, sort deepest-first so nested empties are handled.
|
|
1004
|
+
if (filesToRemove.length > 0) {
|
|
1005
|
+
const parentDirs = [...new Set(
|
|
1006
|
+
filesToRemove.map(f => path.dirname(f)).filter(d => d !== '.')
|
|
1007
|
+
)].sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
|
1008
|
+
for (const relDir of parentDirs) {
|
|
1009
|
+
try {
|
|
1010
|
+
const remaining = await fs.readdir(path.join(projectDir, relDir));
|
|
1011
|
+
if (remaining.length === 0) {
|
|
1012
|
+
await fs.rmdir(path.join(projectDir, relDir));
|
|
1013
|
+
}
|
|
1014
|
+
} catch {}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Copy new agent files (overwriting tracked agent files)
|
|
1019
|
+
const agentEntries = await fs.readdir(publishedAgent.path, { withFileTypes: true });
|
|
1020
|
+
for (const fileEntry of agentEntries) {
|
|
1021
|
+
if (fileEntry.name === 'agent.yaml') continue;
|
|
1022
|
+
const src = path.join(publishedAgent.path, fileEntry.name);
|
|
1023
|
+
const dst = path.join(projectDir, fileEntry.name);
|
|
1024
|
+
if (fileEntry.isDirectory()) {
|
|
1025
|
+
await fs.cp(src, dst, { recursive: true, force: true });
|
|
1026
|
+
} else {
|
|
1027
|
+
await fs.copyFile(src, dst);
|
|
1028
|
+
if (fileEntry.name === 'CLAUDE.md') {
|
|
1029
|
+
try {
|
|
1030
|
+
const content = await fs.readFile(dst, 'utf-8');
|
|
1031
|
+
newClaudeMdHash = createHash('sha256').update(content).digest('hex');
|
|
1032
|
+
} catch {}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
newInstalledFiles = newFileList;
|
|
1038
|
+
} catch (copyErr) {
|
|
1039
|
+
console.error(`[AgentApprove] Failed to copy files for user ${user.uuid}:`, copyErr.message);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
entry.agentInfo.installedVersion = newVersion;
|
|
1044
|
+
entry.agentInfo.installedAt = new Date().toISOString();
|
|
1045
|
+
entry.agentInfo.claudeMdHash = newClaudeMdHash;
|
|
1046
|
+
entry.agentInfo.installedFiles = newInstalledFiles;
|
|
1047
|
+
userUpdated = true;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (userUpdated) {
|
|
1051
|
+
await saveProjectConfig(config, user.uuid);
|
|
1052
|
+
updatedUsersCount++;
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
console.error(`[AgentApprove] Failed to update agent for user ${user.uuid}:`, err.message);
|
|
948
1056
|
}
|
|
949
1057
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1058
|
+
console.log(`[AgentApprove] Force-updated ${updatedUsersCount} users for agent "${submission.agent_name}" v${newVersion}`);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
console.error('[AgentApprove] Failed to update all users:', err.message);
|
|
1061
|
+
}
|
|
1062
|
+
} else {
|
|
1063
|
+
// Auto-update only the submitter's installed agent version
|
|
1064
|
+
try {
|
|
1065
|
+
const submitterUser = userDb.getUserById(submission.user_id);
|
|
1066
|
+
if (submitterUser?.uuid) {
|
|
1067
|
+
const config = await loadProjectConfig(submitterUser.uuid);
|
|
1068
|
+
let updated = false;
|
|
1069
|
+
for (const [, entry] of Object.entries(config)) {
|
|
1070
|
+
if (entry.agentInfo?.isAgent && entry.agentInfo.agentName === submission.agent_name) {
|
|
1071
|
+
entry.agentInfo.installedVersion = newVersion;
|
|
1072
|
+
entry.agentInfo.installedAt = new Date().toISOString();
|
|
1073
|
+
updated = true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (updated) {
|
|
1077
|
+
await saveProjectConfig(config, submitterUser.uuid);
|
|
1078
|
+
}
|
|
952
1079
|
}
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
// Non-critical: don't fail the approval if auto-update fails
|
|
1082
|
+
console.error('[AgentApprove] Failed to auto-update submitter config:', err.message);
|
|
953
1083
|
}
|
|
954
|
-
} catch (err) {
|
|
955
|
-
// Non-critical: don't fail the approval if auto-update fails
|
|
956
|
-
console.error('[AgentApprove] Failed to auto-update submitter config:', err.message);
|
|
957
1084
|
}
|
|
958
1085
|
|
|
959
1086
|
// Cleanup extracted dir
|
|
960
1087
|
fs.rm(extractDir, { recursive: true, force: true }).catch(() => {});
|
|
961
1088
|
|
|
962
|
-
res.json({ success: true, version: newVersion });
|
|
1089
|
+
res.json({ success: true, version: newVersion, updatedUsersCount });
|
|
963
1090
|
} catch (error) {
|
|
964
1091
|
console.error('Error approving submission:', error);
|
|
965
1092
|
res.status(500).json({ error: 'Failed to approve submission', details: error.message });
|
package/server/routes/auth.js
CHANGED
|
@@ -117,11 +117,16 @@ router.post('/verify-code', async (req, res) => {
|
|
|
117
117
|
|
|
118
118
|
user = userDb.createUserWithEmail(email, uuid, role);
|
|
119
119
|
|
|
120
|
-
// Apply default
|
|
120
|
+
// Apply default limits for regular users only
|
|
121
121
|
if (role === 'user') {
|
|
122
122
|
const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
const defaultDailyLimit = settingsDb.get('default_daily_limit_usd');
|
|
124
|
+
if (defaultTotalLimit !== null || defaultDailyLimit !== null) {
|
|
125
|
+
userDb.updateUserLimits(
|
|
126
|
+
user.id,
|
|
127
|
+
defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null,
|
|
128
|
+
defaultDailyLimit !== null ? parseFloat(defaultDailyLimit) : null
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
|
package/server/services/email.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import nodemailer from 'nodemailer';
|
|
2
|
+
import { PRODUCT_NAME } from '../../shared/brand.js';
|
|
2
3
|
|
|
3
4
|
// SMTP configuration from environment variables
|
|
4
5
|
const getSmtpConfig = () => {
|
|
@@ -49,11 +50,11 @@ export const sendVerificationCode = async (email, code) => {
|
|
|
49
50
|
const mailOptions = {
|
|
50
51
|
from: fromAddress,
|
|
51
52
|
to: email,
|
|
52
|
-
subject:
|
|
53
|
+
subject: `${PRODUCT_NAME} 登录验证码`,
|
|
53
54
|
html: `
|
|
54
55
|
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
55
56
|
<div style="text-align: center; margin-bottom: 30px;">
|
|
56
|
-
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0;"
|
|
57
|
+
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0;">${PRODUCT_NAME}</h1>
|
|
57
58
|
</div>
|
|
58
59
|
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 30px; text-align: center;">
|
|
59
60
|
<h2 style="color: #1a1a1a; font-size: 20px; margin: 0 0 20px 0;">您的验证码</h2>
|
|
@@ -71,7 +72,7 @@ export const sendVerificationCode = async (email, code) => {
|
|
|
71
72
|
</div>
|
|
72
73
|
</div>
|
|
73
74
|
`,
|
|
74
|
-
text: `您的
|
|
75
|
+
text: `您的 ${PRODUCT_NAME} 登录验证码是:${code},有效期 5 分钟。如果您没有请求此验证码,请忽略此邮件。`,
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
return transport.sendMail(mailOptions);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* 所有卡片均通过 JSON 直接构建(interactive 类型),无需飞书卡片模板 ID。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { PRODUCT_FEISHU_CARD_TITLE } from '../../../shared/brand.js';
|
|
8
|
+
|
|
7
9
|
const MODE_LABELS = {
|
|
8
10
|
default: '默认模式',
|
|
9
11
|
acceptEdits: '接受编辑',
|
|
@@ -667,7 +669,7 @@ function buildHelpCardDirect(boundEmail, state) {
|
|
|
667
669
|
schema: '2.0',
|
|
668
670
|
config: { update_multi: true },
|
|
669
671
|
header: {
|
|
670
|
-
title: { tag: 'plain_text', content:
|
|
672
|
+
title: { tag: 'plain_text', content: PRODUCT_FEISHU_CARD_TITLE },
|
|
671
673
|
template: 'blue',
|
|
672
674
|
padding: '12px 8px 12px 8px',
|
|
673
675
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* command-handler.js — 飞书斜杠命令处理
|
|
3
3
|
*
|
|
4
4
|
* 支持命令:
|
|
5
|
-
* /auth <token> 绑定
|
|
5
|
+
* /auth <token> 绑定 AgentHub 账号
|
|
6
6
|
* /unbind 解除绑定
|
|
7
7
|
* /new 新建 Claude 会话
|
|
8
8
|
* /list 列出当前项目的会话
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import jwt from 'jsonwebtoken';
|
|
24
24
|
import { feishuDb } from './feishu-db.js';
|
|
25
|
+
import { PRODUCT_NAME, feishuBindingGuide } from '../../../shared/brand.js';
|
|
25
26
|
import { userDb } from '../../database/db.js';
|
|
26
27
|
import { abortClaudeSDKSession } from '../../claude-sdk.js';
|
|
27
28
|
import { getProjects, getSessions } from '../../projects.js';
|
|
@@ -87,7 +88,7 @@ export class CommandHandler {
|
|
|
87
88
|
async _handleAuth(feishuOpenId, chatId, messageId, token) {
|
|
88
89
|
if (!token) {
|
|
89
90
|
await this._reply(chatId, messageId,
|
|
90
|
-
|
|
91
|
+
feishuBindingGuide()
|
|
91
92
|
);
|
|
92
93
|
return true;
|
|
93
94
|
}
|
|
@@ -298,7 +299,7 @@ export class CommandHandler {
|
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
if (projects.length === 0) {
|
|
301
|
-
await this._reply(chatId, messageId,
|
|
302
|
+
await this._reply(chatId, messageId, `暂无项目,请在 ${PRODUCT_NAME} 中创建项目。`);
|
|
302
303
|
return true;
|
|
303
304
|
}
|
|
304
305
|
|