@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/assets/{index-C2Iik5Ac.js → index-CTsndL0V.js} +56 -56
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/claude-sdk.js +5 -3
- package/server/database/db.js +1 -1
- package/server/index.js +7 -0
- package/server/routes/mcp-proxy.js +166 -0
- package/server/routes/mcp-repos.js +6 -3
- package/server/routes/mcp.js +49 -1
- package/server/routes/skill-suite.js +1 -4
- package/server/services/system-agent-repo.js +2 -4
- package/server/services/system-mcp-repo.js +2 -5
- package/server/services/system-repo.js +2 -5
- package/server/utils/aes-encrypt.js +45 -0
- package/server/utils/mcp-proxy-header.js +39 -0
- package/shared/brand.js +21 -0
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-
|
|
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
package/server/claude-sdk.js
CHANGED
|
@@ -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
|
|
package/server/database/db.js
CHANGED
|
@@ -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] =
|
|
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,
|
|
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]:
|
|
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) {
|
package/server/routes/mcp.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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` +
|