@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/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-CTUPAZem.js"></script>
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-BAFclCJK.css">
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.65",
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.72",
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",
@@ -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 = 'You are an AI assistant running inside AgentHub, a platform that supports diverse work tasks beyond coding.';
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
  };
@@ -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 {
@@ -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;
@@ -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 total limit
72
+ // Apply default limits
72
73
  const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
73
- if (defaultTotalLimit !== null) {
74
- userDb.updateUserLimits(user.id, parseFloat(defaultTotalLimit), null);
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);
@@ -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
- // Auto-update submitter's installed agent version
938
- try {
939
- const submitterUser = userDb.getUserById(submission.user_id);
940
- if (submitterUser?.uuid) {
941
- const config = await loadProjectConfig(submitterUser.uuid);
942
- let updated = false;
943
- for (const [key, entry] of Object.entries(config)) {
944
- if (entry.agentInfo?.isAgent && entry.agentInfo.agentName === submission.agent_name) {
945
- entry.agentInfo.installedVersion = newVersion;
946
- entry.agentInfo.installedAt = new Date().toISOString();
947
- updated = true;
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
- if (updated) {
951
- await saveProjectConfig(config, submitterUser.uuid);
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 });
@@ -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 total limit for regular users only
120
+ // Apply default limits for regular users only
121
121
  if (role === 'user') {
122
122
  const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
123
- if (defaultTotalLimit !== null) {
124
- userDb.updateUserLimits(user.id, parseFloat(defaultTotalLimit), null);
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
 
@@ -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: 'AgentHub 登录验证码',
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;">AgentHub</h1>
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: `您的 AgentHub 登录验证码是:${code},有效期 5 分钟。如果您没有请求此验证码,请忽略此邮件。`,
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: '🤖 AgentHub' },
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> 绑定 agenthub 账号
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
- '请先完成账号绑定:\n\n1. 登录 AgentHub 网页:http://10.0.1.133:6175/\n2. 进入 **设置 → 账户**,点击「生成绑定 Token」\n3. 复制命令后在此发送:`/auth <token>`'
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, '暂无项目,请在 AgentHub 中创建项目。');
302
+ await this._reply(chatId, messageId, `暂无项目,请在 ${PRODUCT_NAME} 中创建项目。`);
302
303
  return true;
303
304
  }
304
305