@ian2018cs/agenthub 0.1.65 → 0.1.66

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-BmRD2CqB.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-BCjk5bkF.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.66",
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
  };
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;
@@ -68,10 +68,15 @@ router.post('/users', async (req, res) => {
68
68
  const uuid = uuidv4();
69
69
  const user = userDb.createUserFull(username, passwordHash, uuid, 'user');
70
70
 
71
- // Apply default total limit
71
+ // Apply default limits
72
72
  const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
73
- if (defaultTotalLimit !== null) {
74
- userDb.updateUserLimits(user.id, parseFloat(defaultTotalLimit), null);
73
+ const defaultDailyLimit = settingsDb.get('default_daily_limit_usd');
74
+ if (defaultTotalLimit !== null || defaultDailyLimit !== null) {
75
+ userDb.updateUserLimits(
76
+ user.id,
77
+ defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null,
78
+ defaultDailyLimit !== null ? parseFloat(defaultDailyLimit) : null
79
+ );
75
80
  }
76
81
 
77
82
  // Initialize user directories
@@ -382,8 +387,10 @@ router.delete('/email-domains/:id', (req, res) => {
382
387
  router.get('/settings/default-limits', (req, res) => {
383
388
  try {
384
389
  const defaultTotalLimit = settingsDb.get('default_total_limit_usd');
390
+ const defaultDailyLimit = settingsDb.get('default_daily_limit_usd');
385
391
  res.json({
386
- default_total_limit_usd: defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null
392
+ default_total_limit_usd: defaultTotalLimit !== null ? parseFloat(defaultTotalLimit) : null,
393
+ default_daily_limit_usd: defaultDailyLimit !== null ? parseFloat(defaultDailyLimit) : null,
387
394
  });
388
395
  } catch (error) {
389
396
  console.error('Error fetching default limits:', error);
@@ -394,7 +401,7 @@ router.get('/settings/default-limits', (req, res) => {
394
401
  // Update default spending limits for new users
395
402
  router.put('/settings/default-limits', (req, res) => {
396
403
  try {
397
- const { default_total_limit_usd } = req.body;
404
+ const { default_total_limit_usd, default_daily_limit_usd } = req.body;
398
405
 
399
406
  if (default_total_limit_usd !== null && default_total_limit_usd !== undefined) {
400
407
  if (typeof default_total_limit_usd !== 'number' || default_total_limit_usd < 0) {
@@ -402,16 +409,29 @@ router.put('/settings/default-limits', (req, res) => {
402
409
  }
403
410
  }
404
411
 
412
+ if (default_daily_limit_usd !== null && default_daily_limit_usd !== undefined) {
413
+ if (typeof default_daily_limit_usd !== 'number' || default_daily_limit_usd < 0) {
414
+ return res.status(400).json({ error: '默认日限制必须是正数或为空' });
415
+ }
416
+ }
417
+
405
418
  if (default_total_limit_usd === null || default_total_limit_usd === undefined) {
406
419
  settingsDb.delete('default_total_limit_usd');
407
420
  } else {
408
421
  settingsDb.set('default_total_limit_usd', String(default_total_limit_usd), req.user.id);
409
422
  }
410
423
 
424
+ if (default_daily_limit_usd === null || default_daily_limit_usd === undefined) {
425
+ settingsDb.delete('default_daily_limit_usd');
426
+ } else {
427
+ settingsDb.set('default_daily_limit_usd', String(default_daily_limit_usd), req.user.id);
428
+ }
429
+
411
430
  res.json({
412
431
  success: true,
413
432
  message: '默认限额已更新',
414
- default_total_limit_usd: default_total_limit_usd ?? null
433
+ default_total_limit_usd: default_total_limit_usd ?? null,
434
+ default_daily_limit_usd: default_daily_limit_usd ?? null,
415
435
  });
416
436
  } catch (error) {
417
437
  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
 
@@ -13,6 +13,7 @@ import { transcribeAudio } from './speech.js';
13
13
  import { CommandHandler } from './command-handler.js';
14
14
  import { runQuery } from './sdk-bridge.js';
15
15
  import { resolveToolApproval } from '../../claude-sdk.js';
16
+ import { feishuBindingGuide } from '../../../shared/brand.js';
16
17
  import {
17
18
  buildToolApprovalResultCardDirect,
18
19
  buildModeSelectCardDirect,
@@ -112,7 +113,7 @@ export class FeishuEngine {
112
113
  const binding = feishuDb.getBinding(feishuOpenId);
113
114
  if (!binding) {
114
115
  await this.larkClient.replyText(messageId,
115
- '👋 你好!请先完成账号绑定:\n\n1. 登录 AgentHub 网页:http://10.0.1.133:6175/\n2. 进入 **设置 → 账户**,点击「生成绑定 Token」\n3. 复制命令后在此发送:`/auth <token>`'
116
+ feishuBindingGuide()
116
117
  );
117
118
  return;
118
119
  }
@@ -222,7 +223,7 @@ export class FeishuEngine {
222
223
  const binding = feishuDb.getBinding(feishuOpenId);
223
224
  if (!binding && !commandText.toLowerCase().startsWith('/auth')) {
224
225
  await this.larkClient.sendTextToUser(feishuOpenId,
225
- '👋 你好!请先完成账号绑定:\n\n1. 登录 AgentHub 网页:http://10.0.1.133:6175/\n2. 进入 **设置 → 账户**,点击「生成绑定 Token」\n3. 复制命令后在此发送:`/auth <token>`'
226
+ feishuBindingGuide()
226
227
  );
227
228
  return;
228
229
  }
@@ -247,8 +247,15 @@ export async function publishAgentToRepo(agentName, extractedDir, newVersion, su
247
247
  throw err;
248
248
  }
249
249
 
250
- // 2. Copy all files recursively to repo (includes subdirectories for referenced files)
250
+ // 2. Replace agent directory entirely to ensure removed files are cleaned up.
251
+ // Delete the old directory first, then copy fresh from the submitted archive.
252
+ // git add will pick up both the new/modified files and the deletions.
251
253
  const agentRepoDir = path.join(repoPath, agentName);
254
+ try {
255
+ await fs.rm(agentRepoDir, { recursive: true, force: true });
256
+ } catch (err) {
257
+ console.error('[AgentRepo] Failed to remove old agent dir:', err.message);
258
+ }
252
259
  await copyDirRecursive(extractedDir, agentRepoDir);
253
260
 
254
261
  // 3. Update agent.yaml with new version and updated_at
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 产品品牌常量
3
+ * Single source of truth for all product name / tagline / copy strings.
4
+ * 改名时只需修改此文件。
5
+ */
6
+
7
+ export const PRODUCT_NAME = 'AgentHub';
8
+ export const PRODUCT_TAGLINE = '你的 AI 助手';
9
+
10
+ // Claude SDK system prompt 追加描述(英文)
11
+ export const PRODUCT_SYSTEM_DESC =
12
+ `You are an AI assistant running inside ${PRODUCT_NAME}, a platform that supports diverse work tasks beyond coding.`;
13
+
14
+ // 飞书帮助卡片标题(含 emoji)
15
+ export const PRODUCT_FEISHU_CARD_TITLE = `🤖 ${PRODUCT_NAME}`;
16
+
17
+ // 产品 Web 地址(飞书绑定引导文案使用),通过环境变量配置
18
+ export const PRODUCT_WEB_URL =
19
+ (typeof process !== 'undefined' && process.env?.PRODUCT_WEB_URL) ||
20
+ 'http://localhost:6175';
21
+
22
+ // 飞书绑定引导文案(出现 3 次,统一成函数避免漂移)
23
+ export const feishuBindingGuide = () =>
24
+ `👋 你好!请先完成账号绑定:\n\n` +
25
+ `1. 登录 ${PRODUCT_NAME} 网页:${PRODUCT_WEB_URL}/\n` +
26
+ `2. 进入 **设置 → 账户**,点击「生成绑定 Token」\n` +
27
+ `3. 复制命令后在此发送:\`/auth <token>\``;