@ian2018cs/agenthub 0.2.11 → 0.2.12

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
@@ -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-C2Iik5Ac.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-CTsndL0V.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-C_uEg43g.js">
30
30
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-B2saKA4-.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/vendor-utils-00TdZexr.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian2018cs/agenthub",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "A web-based UI for AI Agents",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -972,13 +972,15 @@ async function queryClaudeSDK(command, options = {}, ws) {
972
972
  // Clean up temporary image files on error
973
973
  await cleanupTempFiles(tempImagePaths, tempDir);
974
974
 
975
- // Send error to WebSocket
975
+ // Send error to WebSocket — do NOT re-throw, as that would cause the
976
+ // outer catch in index.js to send a second `{type:'error'}` message.
977
+ // React 18 batching can merge both WS messages into one render cycle,
978
+ // causing the frontend to only process the unhandled `error` type and
979
+ // miss the `claude-error` that resets isLoading.
976
980
  mutableWriter.send({
977
981
  type: 'claude-error',
978
982
  error: error.message
979
983
  });
980
-
981
- throw error;
982
984
  }
983
985
  }
984
986
 
@@ -456,7 +456,7 @@ const userDb = {
456
456
  // Get user by ID
457
457
  getUserById: (userId) => {
458
458
  try {
459
- const row = db.prepare('SELECT id, username, uuid, role, status, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
459
+ const row = db.prepare('SELECT id, username, email, uuid, role, status, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
460
460
  return row;
461
461
  } catch (err) {
462
462
  throw err;
package/server/index.js CHANGED
@@ -76,6 +76,7 @@ import builtinTools, {
76
76
  import authRoutes from './routes/auth.js';
77
77
  import mcpRoutes from './routes/mcp.js';
78
78
  import mcpUtilsRoutes from './routes/mcp-utils.js';
79
+ import mcpProxyRoutes from './routes/mcp-proxy.js';
79
80
  import commandsRoutes from './routes/commands.js';
80
81
  import projectsRoutes from './routes/projects.js';
81
82
  import adminRoutes from './routes/admin.js';
@@ -471,6 +472,12 @@ wss.on('close', () => {
471
472
  });
472
473
 
473
474
  app.use(cors());
475
+
476
+ // MCP reverse proxy must be mounted before body parsers so the raw request
477
+ // stream is not consumed before the proxy can forward it to the upstream.
478
+ // Identity is via x-proxy-user header; access is restricted to localhost only.
479
+ app.use('/api/mcp-proxy', mcpProxyRoutes);
480
+
474
481
  app.use(express.json({
475
482
  limit: '50mb',
476
483
  type: (req) => {
@@ -0,0 +1,166 @@
1
+ /**
2
+ * MCP HTTP/SSE 反向代理路由
3
+ *
4
+ * 从请求 headers 读取目标 URL 和用户身份,AES 加密用户邮箱后注入到转发请求中。
5
+ * 仅允许本地(localhost)访问。
6
+ */
7
+ import { Router } from 'express';
8
+ import fetch from 'node-fetch';
9
+ import { userDb } from '../database/db.js';
10
+ import { encryptEmail, isProxyEnabled } from '../utils/aes-encrypt.js';
11
+ import { PRODUCT_NAME } from '../../shared/brand.js';
12
+
13
+ const router = Router();
14
+ const TOKEN_HEADER = `x-${PRODUCT_NAME.toLowerCase()}-token`;
15
+
16
+ // 仅允许本地访问的 IP 白名单
17
+ const LOCALHOST_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
18
+
19
+ // 不转发给上游的 hop-by-hop / 内部 headers
20
+ const SKIP_HEADERS = new Set([
21
+ 'connection', 'keep-alive', 'host', 'authorization',
22
+ 'proxy-authenticate', 'proxy-authorization',
23
+ 'te', 'trailer', 'transfer-encoding', 'upgrade',
24
+ 'x-proxy-user', 'x-proxy-target'
25
+ ]);
26
+
27
+ async function handleProxy(req, res) {
28
+ // 1. 仅允许本地访问
29
+ if (!LOCALHOST_IPS.has(req.ip)) {
30
+ return res.status(403).json({ error: 'Proxy only accessible from localhost' });
31
+ }
32
+
33
+ // 2. 检查加密配置
34
+ if (!isProxyEnabled()) {
35
+ return res.status(503).json({ error: 'MCP proxy encryption not configured' });
36
+ }
37
+
38
+ // 3. 读取目标 URL
39
+ const proxyTarget = req.headers['x-proxy-target'];
40
+ if (!proxyTarget) {
41
+ return res.status(400).json({ error: 'Missing x-proxy-target header' });
42
+ }
43
+
44
+ // 4. 读取用户身份
45
+ const userUuid = req.headers['x-proxy-user'];
46
+ if (!userUuid) {
47
+ return res.status(401).json({ error: 'Missing x-proxy-user header' });
48
+ }
49
+
50
+ const user = userDb.getUserByUuid(userUuid);
51
+ if (!user?.email) {
52
+ return res.status(401).json({ error: 'Invalid user or missing email' });
53
+ }
54
+
55
+ // 5. AES 加密邮箱
56
+ let token;
57
+ try {
58
+ token = encryptEmail(user.email);
59
+ } catch (err) {
60
+ console.error('[MCP-Proxy] Encryption error:', err.message);
61
+ return res.status(503).json({ error: 'Proxy encryption failed' });
62
+ }
63
+
64
+ // 6. 构造目标 URL(追加 /api/mcp-proxy 之后的路径,避免多余尾部斜杠)
65
+ let targetUrl;
66
+ try {
67
+ const parsed = new URL(proxyTarget);
68
+ const remainingPath = req.params[0] || '';
69
+ if (remainingPath) {
70
+ // 拼接剩余路径,避免双斜杠
71
+ const basePath = parsed.pathname.replace(/\/$/, '');
72
+ const subPath = remainingPath.replace(/^\//, '');
73
+ parsed.pathname = `${basePath}/${subPath}`;
74
+ }
75
+ // 保留原始 query string
76
+ parsed.search = new URL(req.url, 'http://localhost').search;
77
+ targetUrl = parsed;
78
+ } catch (err) {
79
+ return res.status(400).json({ error: `Invalid proxy target URL: ${err.message}` });
80
+ }
81
+
82
+ // 7. 构造转发 headers
83
+ const forwardHeaders = {};
84
+ for (const [key, value] of Object.entries(req.headers)) {
85
+ if (!SKIP_HEADERS.has(key.toLowerCase())) {
86
+ forwardHeaders[key] = value;
87
+ }
88
+ }
89
+ forwardHeaders[TOKEN_HEADER] = token;
90
+ // console.log(`[MCP-Proxy] injected header: ${TOKEN_HEADER} = ${token}`);
91
+
92
+ // 8. 缓冲请求 body(避免 stream 在重定向时无法重用)
93
+ const isSSERequest = req.headers.accept?.includes('text/event-stream');
94
+ let requestBody = undefined;
95
+ if (!['GET', 'HEAD'].includes(req.method)) {
96
+ requestBody = await new Promise((resolve, reject) => {
97
+ const chunks = [];
98
+ req.on('data', chunk => chunks.push(chunk));
99
+ req.on('end', () => resolve(Buffer.concat(chunks)));
100
+ req.on('error', reject);
101
+ });
102
+ }
103
+
104
+ const startTime = Date.now();
105
+ try {
106
+ const upstreamRes = await fetch(targetUrl.toString(), {
107
+ method: req.method,
108
+ headers: forwardHeaders,
109
+ body: requestBody,
110
+ timeout: isSSERequest ? 0 : 30000,
111
+ });
112
+
113
+ const duration = Date.now() - startTime;
114
+ console.log(`[MCP-Proxy] ${req.method} → ${targetUrl.pathname} ${upstreamRes.status} ${duration}ms`);
115
+
116
+ // 9. 复制响应状态和 headers
117
+ res.status(upstreamRes.status);
118
+ const responseSkipHeaders = new Set(['connection', 'keep-alive', 'transfer-encoding']);
119
+ for (const [k, v] of upstreamRes.headers.entries()) {
120
+ if (!responseSkipHeaders.has(k.toLowerCase())) {
121
+ res.setHeader(k, v);
122
+ }
123
+ }
124
+
125
+ // 10. SSE 特殊处理
126
+ const isSSEResponse = upstreamRes.headers.get('content-type')?.includes('text/event-stream');
127
+ if (isSSEResponse) {
128
+ res.setHeader('Cache-Control', 'no-cache');
129
+ res.setHeader('Connection', 'keep-alive');
130
+ res.flushHeaders();
131
+ }
132
+
133
+ // 11. 流式管道转发(HTTP 和 SSE 统一处理)
134
+ upstreamRes.body.pipe(res);
135
+
136
+ req.on('close', () => {
137
+ upstreamRes.body.destroy();
138
+ });
139
+
140
+ upstreamRes.body.on('error', (err) => {
141
+ console.error('[MCP-Proxy] Upstream stream error:', err.message);
142
+ if (!res.headersSent) {
143
+ res.status(502).json({ error: 'Upstream stream error' });
144
+ } else {
145
+ res.end();
146
+ }
147
+ });
148
+ } catch (err) {
149
+ const duration = Date.now() - startTime;
150
+ console.error(`[MCP-Proxy] ${req.method} → ${targetUrl.pathname} FAILED ${duration}ms:`, err.message);
151
+
152
+ if (!res.headersSent) {
153
+ const status = err.type === 'request-timeout' ? 504 : 502;
154
+ const message = err.type === 'request-timeout'
155
+ ? 'Upstream timeout'
156
+ : `Failed to connect to upstream: ${err.message}`;
157
+ res.status(status).json({ error: message });
158
+ }
159
+ }
160
+ }
161
+
162
+ // 匹配 /api/mcp-proxy 和 /api/mcp-proxy/* 两种路径
163
+ router.all('/', handleProxy);
164
+ router.all('/*', handleProxy);
165
+
166
+ export default router;
@@ -5,6 +5,7 @@ import { spawn } from 'child_process';
5
5
  import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
6
6
  import { SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME } from '../services/system-mcp-repo.js';
7
7
  import { addToCodexConfig, removeFromCodexConfig } from '../services/codex-mcp.js';
8
+ import { applyProxyConfig } from '../utils/mcp-proxy-header.js';
8
9
 
9
10
  const router = express.Router();
10
11
 
@@ -478,6 +479,8 @@ router.post('/install', async (req, res) => {
478
479
 
479
480
  // Install each server entry
480
481
  for (const [serverName, serverConfig] of Object.entries(mcpJson)) {
482
+ // Auto-inject MCP proxy config for http/sse types
483
+ const finalConfig = applyProxyConfig(serverConfig, userUuid);
481
484
  try {
482
485
  if (scope === 'local' && projectPath) {
483
486
  // Bypass Claude CLI for local scope: directly write to .claude.json projects[projectPath].mcpServers
@@ -492,11 +495,11 @@ router.post('/install', async (req, res) => {
492
495
  if (!claudeConfig.projects) claudeConfig.projects = {};
493
496
  if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
494
497
  if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
495
- claudeConfig.projects[projectPath].mcpServers[serverName] = serverConfig;
498
+ claudeConfig.projects[projectPath].mcpServers[serverName] = finalConfig;
496
499
  await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
497
500
  installedServers.push(serverName);
498
501
  } else {
499
- await addMcpServerViaJson(serverName, serverConfig, scope, userPaths.claudeDir);
502
+ await addMcpServerViaJson(serverName, finalConfig, scope, userPaths.claudeDir);
500
503
  installedServers.push(serverName);
501
504
  }
502
505
  } catch (err) {
@@ -510,7 +513,7 @@ router.post('/install', async (req, res) => {
510
513
  } catch {
511
514
  claudeConfig = { hasCompletedOnboarding: true };
512
515
  }
513
- claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), [serverName]: serverConfig };
516
+ claudeConfig.mcpServers = { ...(claudeConfig.mcpServers || {}), [serverName]: finalConfig };
514
517
  await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
515
518
  installedServers.push(serverName);
516
519
  } catch (fallbackErr) {
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'url';
5
5
  import { dirname } from 'path';
6
6
  import { getUserPaths } from '../services/user-directories.js';
7
7
  import { addToCodexConfig, removeFromCodexConfig } from '../services/codex-mcp.js';
8
+ import { applyProxyConfig } from '../utils/mcp-proxy-header.js';
9
+ import { isProxyEnabled } from '../utils/aes-encrypt.js';
8
10
 
9
11
  const router = express.Router();
10
12
  const __filename = fileURLToPath(import.meta.url);
@@ -93,6 +95,9 @@ router.post('/cli/add', async (req, res) => {
93
95
  serverEntry = { command, ...(args?.length ? { args } : {}), ...(Object.keys(env).length ? { env } : {}) };
94
96
  }
95
97
 
98
+ // Auto-inject MCP proxy config for http/sse types
99
+ serverEntry = applyProxyConfig(serverEntry, userUuid);
100
+
96
101
  if (!claudeConfig.projects) claudeConfig.projects = {};
97
102
  if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
98
103
  if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
@@ -105,6 +110,26 @@ router.post('/cli/add', async (req, res) => {
105
110
 
106
111
  const { spawn } = await import('child_process');
107
112
 
113
+ // For http/sse types with proxy enabled, bypass CLI and write directly to .claude.json
114
+ // (Claude CLI doesn't support custom proxy headers like x-proxy-target / x-proxy-user)
115
+ if ((type === 'http' || type === 'sse') && isProxyEnabled()) {
116
+ const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
117
+ let claudeConfig = {};
118
+ try {
119
+ claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
120
+ } catch { /* start fresh if missing */ }
121
+
122
+ let serverEntry = { transport: type, url, ...(Object.keys(headers).length ? { headers } : {}) };
123
+ serverEntry = applyProxyConfig(serverEntry, userUuid);
124
+
125
+ if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
126
+ claudeConfig.mcpServers[name] = serverEntry;
127
+
128
+ await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
129
+ console.log(`[MCP] Wrote user-scoped MCP "${name}" with proxy config`);
130
+ return res.json({ success: true, message: `MCP server "${name}" added successfully` });
131
+ }
132
+
108
133
  let cliArgs = ['mcp', 'add', '--scope', scope];
109
134
 
110
135
  if (type === 'http') {
@@ -192,7 +217,7 @@ router.post('/cli/add-json', async (req, res) => {
192
217
  if (!claudeConfig.projects) claudeConfig.projects = {};
193
218
  if (!claudeConfig.projects[projectPath]) claudeConfig.projects[projectPath] = {};
194
219
  if (!claudeConfig.projects[projectPath].mcpServers) claudeConfig.projects[projectPath].mcpServers = {};
195
- claudeConfig.projects[projectPath].mcpServers[name] = parsedConfig;
220
+ claudeConfig.projects[projectPath].mcpServers[name] = applyProxyConfig(parsedConfig, userUuid);
196
221
 
197
222
  await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
198
223
  console.log(`[MCP] Wrote local-scoped MCP "${name}" (JSON) for project ${projectPath}`);
@@ -207,6 +232,29 @@ router.post('/cli/add-json', async (req, res) => {
207
232
 
208
233
  const { spawn } = await import('child_process');
209
234
 
235
+ // For http/sse types with proxy enabled, bypass CLI and write directly to .claude.json
236
+ if ((parsedConfig.type === 'http' || parsedConfig.type === 'sse') && isProxyEnabled()) {
237
+ const claudeJsonPath = path.join(userPaths.claudeDir, '.claude.json');
238
+ let claudeConfig = {};
239
+ try {
240
+ claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
241
+ } catch { /* start fresh if missing */ }
242
+
243
+ const proxiedConfig = applyProxyConfig(parsedConfig, userUuid);
244
+
245
+ if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
246
+ claudeConfig.mcpServers[name] = proxiedConfig;
247
+
248
+ await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
249
+ console.log(`[MCP] Wrote user-scoped MCP "${name}" (JSON) with proxy config`);
250
+
251
+ const codexConfigPath = path.join(userPaths.codexHomeDir, '.codex', 'config.toml');
252
+ addToCodexConfig(codexConfigPath, { [name]: proxiedConfig }).catch(err =>
253
+ console.error('[MCP] Failed to sync to Codex config.toml:', err.message)
254
+ );
255
+ return res.json({ success: true, message: `MCP server "${name}" added successfully via JSON` });
256
+ }
257
+
210
258
  const cliArgs = ['mcp', 'add-json', '--scope', scope, name, JSON.stringify(parsedConfig)];
211
259
  console.log('🔧 Running Claude CLI command:', 'claude', cliArgs[0], cliArgs[1], cliArgs[2], cliArgs[3], cliArgs[4]);
212
260
 
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
4
  import { spawn, exec } from 'child_process';
5
5
  import { getUserPaths, getPublicPaths } from '../services/user-directories.js';
6
+ import { LARK_CLI_REPO_URL, LARK_CLI_REPO_OWNER, LARK_CLI_REPO_NAME } from '../../shared/brand.js';
6
7
 
7
8
  const router = express.Router();
8
9
 
@@ -16,10 +17,6 @@ const LARK_CLI_SKILLS = [
16
17
  'lark-workflow-standup-report',
17
18
  ];
18
19
 
19
- const LARK_CLI_REPO_URL = 'https://github.com/larksuite/cli';
20
- const LARK_CLI_REPO_OWNER = 'larksuite';
21
- const LARK_CLI_REPO_NAME = 'cli';
22
-
23
20
  // ─── 认证策略表(便于未来扩展其他套装) ────────────────────────────────────
24
21
 
25
22
  const AUTH_STRATEGIES = {
@@ -2,10 +2,8 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { getPublicPaths } from './user-directories.js';
5
-
6
- export const SYSTEM_AGENT_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-agents.git';
7
- export const SYSTEM_AGENT_REPO_OWNER = 'mcp-server';
8
- export const SYSTEM_AGENT_REPO_NAME = 'hupoer-agents';
5
+ import { SYSTEM_AGENT_REPO_URL, SYSTEM_AGENT_REPO_OWNER, SYSTEM_AGENT_REPO_NAME } from '../../shared/brand.js';
6
+ export { SYSTEM_AGENT_REPO_URL, SYSTEM_AGENT_REPO_OWNER, SYSTEM_AGENT_REPO_NAME };
9
7
 
10
8
 
11
9
  function runGit(args, cwd = null) {
@@ -2,10 +2,8 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { getUserPaths, getPublicPaths } from './user-directories.js';
5
-
6
- const SYSTEM_MCP_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-mcps.git';
7
- const SYSTEM_MCP_REPO_OWNER = 'mcp-server';
8
- const SYSTEM_MCP_REPO_NAME = 'hupoer-mcps';
5
+ import { SYSTEM_MCP_REPO_URL, SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME } from '../../shared/brand.js';
6
+ export { SYSTEM_MCP_REPO_URL, SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME };
9
7
 
10
8
  function runGit(args, cwd) {
11
9
  return new Promise((resolve, reject) => {
@@ -98,4 +96,3 @@ export async function initSystemMcpRepoForUser(userUuid) {
98
96
  }
99
97
  }
100
98
 
101
- export { SYSTEM_MCP_REPO_OWNER, SYSTEM_MCP_REPO_NAME, SYSTEM_MCP_REPO_URL };
@@ -2,10 +2,8 @@ import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { getUserPaths, getPublicPaths } from './user-directories.js';
5
-
6
- const SYSTEM_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-skills.git';
7
- const SYSTEM_REPO_OWNER = 'mcp-server';
8
- const SYSTEM_REPO_NAME = 'hupoer-skills';
5
+ import { SYSTEM_REPO_URL, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME } from '../../shared/brand.js';
6
+ export { SYSTEM_REPO_URL, SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME };
9
7
 
10
8
  function runGit(args, cwd) {
11
9
  return new Promise((resolve, reject) => {
@@ -105,4 +103,3 @@ export async function initSystemRepoForUser(userUuid) {
105
103
  }
106
104
  }
107
105
 
108
- export { SYSTEM_REPO_OWNER, SYSTEM_REPO_NAME, SYSTEM_REPO_URL };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * AES-256-CBC 加密工具
3
+ * 用于 MCP 反向代理,加密用户邮箱作为身份令牌。
4
+ *
5
+ * 算法:AES-256-CBC
6
+ * 密钥派生:crypto.scryptSync(AES_KEY, AES_SALT, 32)
7
+ * IV:每次随机 16 字节
8
+ * 输出:base64(iv_16bytes + ciphertext)
9
+ */
10
+ import crypto from 'crypto';
11
+
12
+ const AES_KEY = process.env.MCP_PROXY_AES_KEY || '';
13
+ const AES_SALT = process.env.MCP_PROXY_AES_SALT || '';
14
+
15
+ // 模块加载时一次性派生密钥
16
+ let derivedKey = null;
17
+ if (AES_KEY && AES_SALT) {
18
+ try {
19
+ derivedKey = crypto.scryptSync(AES_KEY, AES_SALT, 32);
20
+ } catch (err) {
21
+ console.error('[MCP-Proxy] Failed to derive AES key:', err.message);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * 检查代理加密是否已配置(AES_KEY + AES_SALT 都存在)
27
+ */
28
+ export function isProxyEnabled() {
29
+ return derivedKey !== null;
30
+ }
31
+
32
+ /**
33
+ * AES-256-CBC 加密用户邮箱
34
+ * @param {string} email - 用户邮箱明文
35
+ * @returns {string} base64(iv + ciphertext)
36
+ */
37
+ export function encryptEmail(email) {
38
+ if (!derivedKey) {
39
+ throw new Error('MCP proxy encryption not configured (MCP_PROXY_AES_KEY and MCP_PROXY_AES_SALT required)');
40
+ }
41
+ const iv = crypto.randomBytes(16);
42
+ const cipher = crypto.createCipheriv('aes-256-cbc', derivedKey, iv);
43
+ const encrypted = Buffer.concat([cipher.update(email, 'utf8'), cipher.final()]);
44
+ return Buffer.concat([iv, encrypted]).toString('base64');
45
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * MCP 代理配置注入
3
+ * 在安装 http/sse 类型 MCP 时,自动注入代理 headers 并替换 URL。
4
+ */
5
+ import { isProxyEnabled } from './aes-encrypt.js';
6
+
7
+ const PROXY_BASE_PATH = '/api/mcp-proxy';
8
+
9
+ /**
10
+ * 如果代理已启用且配置是 http/sse 类型,自动注入代理 headers 并替换 URL。
11
+ * @param {object} serverConfig - MCP 服务器配置
12
+ * @param {string} userUuid - 用户 UUID
13
+ * @returns {object} 处理后的配置(可能是原样返回)
14
+ */
15
+ export function applyProxyConfig(serverConfig, userUuid) {
16
+ if (!isProxyEnabled()) return serverConfig;
17
+
18
+ const type = serverConfig.type || serverConfig.transport;
19
+ if (type !== 'http' && type !== 'sse') return serverConfig;
20
+
21
+ const originalUrl = serverConfig.url;
22
+ if (!originalUrl) return serverConfig;
23
+
24
+ // 已经是代理地址的不重复处理
25
+ if (originalUrl.includes(PROXY_BASE_PATH)) return serverConfig;
26
+
27
+ const port = process.env.PORT || 3001;
28
+ const proxyUrl = `http://localhost:${port}${PROXY_BASE_PATH}`;
29
+
30
+ return {
31
+ ...serverConfig,
32
+ url: proxyUrl,
33
+ headers: {
34
+ ...(serverConfig.headers || {}),
35
+ 'x-proxy-target': originalUrl,
36
+ 'x-proxy-user': userUuid
37
+ }
38
+ };
39
+ }
package/shared/brand.js CHANGED
@@ -31,6 +31,27 @@ export const CHAT_EXAMPLE_PROMPTS = [
31
31
  // 可选值: 'default'(默认模式)| 'acceptEdits'(接受编辑)| 'bypassPermissions'(跳过权限)| 'plan'(计划模式)
32
32
  export const DEFAULT_PERMISSION_MODE = 'default';
33
33
 
34
+ // ─── 内置仓库地址 ─────────────────────────────────────────────────────────────
35
+ // Skills 内置仓库
36
+ export const SYSTEM_REPO_URL = 'git@git.amberweather.com:mcp-server/hupoer-skills.git';
37
+ export const SYSTEM_REPO_OWNER = 'mcp-server';
38
+ export const SYSTEM_REPO_NAME = 'hupoer-skills';
39
+
40
+ // MCP 内置仓库
41
+ export const SYSTEM_MCP_REPO_URL = 'git@git.amberweather.com:mcp-server/agenthub-mcps.git';
42
+ export const SYSTEM_MCP_REPO_OWNER = 'mcp-server';
43
+ export const SYSTEM_MCP_REPO_NAME = 'agenthub-mcps';
44
+
45
+ // Agents 内置仓库
46
+ export const SYSTEM_AGENT_REPO_URL = 'git@git.amberweather.com:mcp-server/agenthub-agents.git';
47
+ export const SYSTEM_AGENT_REPO_OWNER = 'mcp-server';
48
+ export const SYSTEM_AGENT_REPO_NAME = 'agenthub-agents';
49
+
50
+ // Lark CLI 技能套装仓库
51
+ export const LARK_CLI_REPO_URL = 'https://github.com/larksuite/cli';
52
+ export const LARK_CLI_REPO_OWNER = 'larksuite';
53
+ export const LARK_CLI_REPO_NAME = 'cli';
54
+
34
55
  // 飞书绑定引导文案(出现 3 次,统一成函数避免漂移)
35
56
  export const feishuBindingGuide = () =>
36
57
  `👋 你好!请先完成账号绑定:\n\n` +