chaimi-keep-mcp 3.4.0 → 3.5.0-beta.1

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/.env.example ADDED
@@ -0,0 +1,24 @@
1
+ # 环境配置示例
2
+ # 将此文件复制为 .env 并填入实际值
3
+
4
+ # 环境选择:development 或 production
5
+ NODE_ENV=production
6
+
7
+ # API 签名密钥(生产环境建议使用强密钥)
8
+ CHAIMI_API_SECRET=your-chaimi-api-secret-here
9
+
10
+ # URL 加密密钥(用于解密云函数 URL)
11
+ URL_ENCRYPT_KEY=your-url-encrypt-key-here
12
+
13
+ # ====================
14
+ # 开发环境配置
15
+ # ====================
16
+ # 开发环境的云函数 URL(仅 NODE_ENV=development 时使用)
17
+ # MCP_HUB_URL=http://localhost:3000/mcpHub
18
+ # MCP_OAUTH_URL=http://localhost:3000/mcpOAuth
19
+ # MCP_PROMPT_URL=http://localhost:3000/mcpPrompt
20
+
21
+ # ====================
22
+ # 生产环境配置
23
+ # ====================
24
+ # 生产环境使用加密存储在代码中的 URL,无需在此配置
package/bin/cli.js CHANGED
@@ -21,12 +21,7 @@ const serverPath = path.join(__dirname, '..', 'server.js');
21
21
  const DEFAULT_CHAIMI_CONFIG = {
22
22
  command: 'chaimi-keep-mcp',
23
23
  description: '柴米AI记账 MCP Server - 支持 AI 工具直接记账',
24
- env: {
25
- MCP_OAUTH_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpOAuth',
26
- MCP_HUB_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp',
27
- MCP_PROMPT_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpPrompt'
28
- },
29
- // 首次授权时需要常驻运行,等待用户授权(3分钟轮询)
24
+ // 首次授权时需要常驻运行,等待用户授权(30分钟轮询)
30
25
  // 授权成功后进程自动退出,下次调用时 mcporter 会重新启动
31
26
  lifecycle: 'keep-alive'
32
27
  };
package/oauth.js CHANGED
@@ -505,7 +505,11 @@ class FileTokenStorage extends TokenStorage {
505
505
  agentName: agentName || '柴米AI助手',
506
506
  updatedAt: new Date().toISOString()
507
507
  };
508
- await this.fs.writeFile(agentNamePath, JSON.stringify(data, null, 2), 'utf8');
508
+ // 使用 JSON.stringify 并转义处理,确保中文正常显示(不转义为 \uXXXX)
509
+ const jsonStr = JSON.stringify(data, null, 2)
510
+ .replace(/\\u[\dA-F]{4}/gi, (match) =>
511
+ String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16)));
512
+ await this.fs.writeFile(agentNamePath, jsonStr, 'utf8');
509
513
  console.error('✅ Agent 名称已保存:', agentNamePath);
510
514
  } catch (err) {
511
515
  console.error('❌ 保存 Agent 名称失败:', err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.4.0",
3
+ "version": "3.5.0-beta.1",
4
4
  "description": "柴米AI记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -48,8 +48,11 @@ const { configManager } = require('./utils/config.js');
48
48
  // 导入校验工具
49
49
  const { validateDate } = require('./utils/validators.js');
50
50
 
51
- // API 签名密钥(用于验证请求来自合法 MCP Server)
52
- const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
51
+ // 导入密钥管理工具
52
+ const { getOrCreateSecretKey } = require('./utils/keyManager.js');
53
+
54
+ // 导入加密工具
55
+ const { encrypt, decrypt } = require('./utils/encryption.js');
53
56
 
54
57
  // ==================== 请求频率限制 ====================
55
58
 
@@ -138,36 +141,102 @@ function checkRateLimit(toolName) {
138
141
  return { allowed: true };
139
142
  }
140
143
 
141
- // URL 加密密钥
142
- const URL_ENCRYPT_KEY = 'chaimi-url-key-2024';
144
+ // 获取 API 密钥(从环境变量或自动生成)
145
+ let CHAIMI_API_SECRET = process.env.CHAIMI_API_SECRET;
146
+
147
+ // ==================== 多环境配置 ====================
143
148
 
144
- // 加密的云函数 URL
149
+ // 环境配置
150
+ const ENV = process.env.NODE_ENV || 'production';
151
+
152
+ // 加密的云函数 URL(开发和生产环境分开)
145
153
  const ENCRYPTED_URLS = {
146
- hub: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571320140b40044e05',
147
- oauth: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571327201c1901',
148
- prompt: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f5713381306001959'
154
+ development: {
155
+ hub: 'enc:v2:d397243fce051c611175302dea8a7a54:8f0bc1dc6966e9149e8d3ecae2467830:cf61c7a159154d15aa1502deec2519931090ff791be67e89fd4e3b20322b2bea8071be02d768ec10abdc50b2d3e76df6dc44a6d46d61c2743acdef428205cc0e3534c93d94857cc91985db5fd69745053df05a87',
156
+ oauth: 'enc:v2:bff470f73af1723d6e320d39df72b342:eefb409dff5a4713931caa3886f9159c:18d0a711f31a5ba7e068e43d8bcce6b8fc56513d44ff3d90ab006063461b44025387e52cb2ce5ffe471a8a8f4b993879dcaa485dd1e845c647be134595467c4a00efc736d15abfcaa8d8cf460365fbf597e0',
157
+ prompt: 'enc:v2:a058c504374087a36417280f0bc4288d:ed5fc65a697c1254e8aeb9faa7119ec8:b8831fd319c35bc01554546827ce31bd04d2039fb87f0b6a19b35850ec228b6988b2103b1cc45a7bd21b117b299c5095a44e1ee4a4c2adb04e24a401e1271aff912503e2abb9cec03764b774df53e60b0686d3'
158
+ },
159
+ production: {
160
+ hub: 'enc:v2:500682dfbd51aff69852abe39430da35:3d57b5ac7f798c6770209159b6dfd9fb:7b9733f27208809b761090f7628f3799aac12c957cdd45b3d512eb4f1f1c18f626afd0b73b12982500122b11e373e0a2a71f14cb6c5966cf98af9ae4b9e79fb575d1e2f42ec403690d5c7dcc519e9eaec21865eb',
161
+ oauth: 'enc:v2:30adcae20bc1452e8e8c7eff088e8b61:b8bedcc9985d1e10185f8ae5c845cc9c:1b2206380eddcfc4d00ed920f20f838bfc664e2350591666db5a5c2b044a88165e0e7f04c58e2d4f0764bb9872aeaf625a91f7a19ce6909264115e7ce70b35c0c081eb4170cac869d3f8b576b7b4dc41580e',
162
+ prompt: 'enc:v2:beba836814161d59df3a2220ebc500ef:6023bde15c74be6bec641044a3f854de:4421b9f985a1914c06868c8577e57e26e3fd4fe4392c2c94862322fd30258eccae68fce6fd56e9a35427a724d3c22b735e9b5a822d05be098ae5e0662ea41c786e4acea12e4d80b598b543d346d7ccbdc32623'
163
+ }
149
164
  };
150
165
 
151
- // 解密 URL 函数
166
+ // 解密 URL 函数(支持 AES-256-GCM)
152
167
  function decryptUrl(encrypted, key) {
153
- const hex = encrypted.replace('enc:v1:', '');
154
- let result = '';
155
- for (let i = 0; i < hex.length; i += 2) {
156
- const byte = parseInt(hex.substr(i, 2), 16);
157
- result += String.fromCharCode(byte ^ key.charCodeAt((i / 2) % key.length));
168
+ if (!encrypted) return '';
169
+
170
+ if (encrypted.startsWith('enc:v2:')) {
171
+ return decrypt(encrypted.replace('enc:v2:', ''), key);
172
+ }
173
+
174
+ if (encrypted.startsWith('enc:v1:')) {
175
+ const urlEncryptKey = key || 'chaimi-url-key-2024';
176
+ const hex = encrypted.replace('enc:v1:', '');
177
+ let result = '';
178
+ for (let i = 0; i < hex.length; i += 2) {
179
+ const byte = parseInt(hex.substr(i, 2), 16);
180
+ result += String.fromCharCode(byte ^ urlEncryptKey.charCodeAt((i / 2) % urlEncryptKey.length));
181
+ }
182
+ return result;
158
183
  }
159
- return result;
184
+
185
+ return encrypted;
160
186
  }
161
187
 
162
- // 配置 - 微信云函数(运行时解密)
163
- const MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(ENCRYPTED_URLS.hub, URL_ENCRYPT_KEY);
164
- const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(ENCRYPTED_URLS.prompt, URL_ENCRYPT_KEY);
165
- const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(ENCRYPTED_URLS.oauth, URL_ENCRYPT_KEY);
188
+ // ==================== URL 配置 ====================
166
189
 
167
- // Token 缓存
168
- let cachedToken = null;
190
+ let MCP_HUB_URL;
191
+ let MCP_PROMPT_URL;
192
+ let MCP_OAUTH_URL;
193
+ let tokenEncryptionKey;
194
+
195
+ // 初始化配置
196
+ async function initConfig() {
197
+ // 获取 URL 加密密钥
198
+ const urlEncryptKey = process.env.URL_ENCRYPT_KEY || 'd2e4168144a50c6d8d0c4692cd73331eb2bc1db0cd727afaf29ff9645d480e59';
199
+
200
+ // 获取 Token 加密密钥
201
+ tokenEncryptionKey = getOrCreateSecretKey();
202
+
203
+ // 获取 API 密钥
204
+ if (!CHAIMI_API_SECRET) {
205
+ CHAIMI_API_SECRET = process.env.CHAIMI_API_SECRET || 'chaimi-mcp-secret-2024';
206
+ }
207
+
208
+ // 配置 URL
209
+ const envUrls = ENCRYPTED_URLS[ENV] || ENCRYPTED_URLS.production;
210
+ MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(envUrls.hub, urlEncryptKey);
211
+ MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(envUrls.prompt, urlEncryptKey);
212
+ MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(envUrls.oauth, urlEncryptKey);
213
+
214
+ console.error(`✅ 已加载 ${ENV} 环境配置`);
215
+ }
216
+
217
+ // Token 缓存(加密存储)
218
+ let cachedEncryptedToken = null;
169
219
  let tokenExpireTime = 0;
170
220
 
221
+ // 获取解密的 Token
222
+ function getCachedToken() {
223
+ if (!cachedEncryptedToken || !tokenEncryptionKey) return null;
224
+ try {
225
+ return decrypt(cachedEncryptedToken, tokenEncryptionKey);
226
+ } catch (e) {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ // 设置加密的 Token
232
+ function setCachedToken(token) {
233
+ if (!token || !tokenEncryptionKey) {
234
+ cachedEncryptedToken = null;
235
+ return;
236
+ }
237
+ cachedEncryptedToken = encrypt(token, tokenEncryptionKey);
238
+ }
239
+
171
240
  // OAuth 管理器实例
172
241
  let oauthManager = null;
173
242
 
@@ -213,8 +282,8 @@ async function initOAuthManager() {
213
282
  // 检查 Token 是否过期(预留5分钟缓冲)
214
283
  const expiresAt = new Date(existingToken.expiresAt).getTime();
215
284
  if (expiresAt > Date.now() + 5 * 60 * 1000) {
216
- // Token 有效,设置授权状态
217
- cachedToken = existingToken.accessToken;
285
+ // Token 有效,设置授权状态(加密存储)
286
+ setCachedToken(existingToken.accessToken);
218
287
  tokenExpireTime = expiresAt;
219
288
  authState.isAuthorized = true;
220
289
  console.error('✅ 已从文件加载有效 Token,无需重新授权');
@@ -490,6 +559,7 @@ let lastRefreshTime = 0;
490
559
 
491
560
  async function getToken() {
492
561
  // 【优化1】优先检查内存中的 token 是否有效
562
+ const cachedToken = getCachedToken();
493
563
  if (cachedToken && tokenExpireTime > Date.now() + 5 * 60 * 1000) {
494
564
  if (Date.now() - lastRefreshTime < TOKEN_REFRESH_INTERVAL) {
495
565
  return cachedToken;
@@ -502,13 +572,13 @@ async function getToken() {
502
572
  if (existingToken && existingToken.accessToken && existingToken.expiresAt) {
503
573
  const expiresAt = new Date(existingToken.expiresAt).getTime();
504
574
  if (expiresAt > Date.now() + 5 * 60 * 1000) {
505
- // Token 有效,直接使用
506
- cachedToken = existingToken.accessToken;
575
+ // Token 有效,直接使用(加密存储)
576
+ setCachedToken(existingToken.accessToken);
507
577
  tokenExpireTime = expiresAt;
508
578
  authState.isAuthorized = true;
509
579
  lastRefreshTime = Date.now();
510
580
  console.error('✅ 已从文件加载有效 Token,无需重新授权');
511
- return cachedToken;
581
+ return getCachedToken();
512
582
  } else {
513
583
  console.error('⏰ 保存的 Token 已过期,需要重新授权');
514
584
  }
@@ -532,8 +602,8 @@ async function getToken() {
532
602
  try {
533
603
  const token = await oauthManager.pollForTokenOnce(savedAuthState.deviceCode);
534
604
  if (token) {
535
- // 授权成功
536
- cachedToken = token.accessToken;
605
+ // 授权成功(加密存储)
606
+ setCachedToken(token.accessToken);
537
607
  tokenExpireTime = token.expiresAt;
538
608
  authState.isAuthorized = true;
539
609
  await oauthManager.tokenStorage.save(token);
@@ -543,7 +613,7 @@ async function getToken() {
543
613
  await oauthManager.clearAuthState();
544
614
  console.error('✅ 授权成功!可以继续使用记账功能');
545
615
  // 返回特殊标记,让上层知道这是刚完成授权的情况
546
- throw new Error(`AUTH_JUST_COMPLETED:${cachedToken}`);
616
+ throw new Error(`AUTH_JUST_COMPLETED:${getCachedToken()}`);
547
617
  }
548
618
  // 用户还未授权,返回同一个验证码
549
619
  authState.deviceCode = savedAuthState.deviceCode;
@@ -573,11 +643,11 @@ async function getToken() {
573
643
 
574
644
  try {
575
645
  const oauthToken = await oauthManager.getValidToken();
576
- cachedToken = oauthToken.accessToken;
646
+ setCachedToken(oauthToken.accessToken);
577
647
  tokenExpireTime = oauthToken.expiresAt;
578
648
  lastRefreshTime = Date.now();
579
649
  await oauthManager.clearAuthState();
580
- return cachedToken;
650
+ return getCachedToken();
581
651
  } catch (err) {
582
652
  // Token 获取失败,可能需要重新授权
583
653
  authState.isAuthorized = false;
@@ -2363,6 +2433,7 @@ async function fetchAgentNameFromCloud(deviceCode) {
2363
2433
  }
2364
2434
 
2365
2435
  async function main() {
2436
+ await initConfig();
2366
2437
  await initOAuthManager();
2367
2438
 
2368
2439
  const transport = new StdioServerTransport();
@@ -2433,8 +2504,8 @@ async function pollForAuthInBackground(deviceCode, interval) {
2433
2504
  const token = await oauthManager.pollForTokenOnce(deviceCode);
2434
2505
 
2435
2506
  if (token) {
2436
- // 授权成功
2437
- cachedToken = token.accessToken;
2507
+ // 授权成功(加密存储)
2508
+ setCachedToken(token.accessToken);
2438
2509
  tokenExpireTime = token.expiresAt;
2439
2510
  authState.isAuthorized = true;
2440
2511
  authState.isWaiting = false;
@@ -0,0 +1,53 @@
1
+ const crypto = require('crypto');
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 16;
5
+ const TAG_LENGTH = 16;
6
+ const SALT = 'chaimi-mcp-salt-2026';
7
+
8
+ function encrypt(text, key) {
9
+ let derivedKey;
10
+ if (typeof key === 'string') {
11
+ derivedKey = crypto.scryptSync(key, SALT, 32);
12
+ } else {
13
+ derivedKey = key;
14
+ }
15
+
16
+ const iv = crypto.randomBytes(IV_LENGTH);
17
+ const cipher = crypto.createCipheriv(ALGORITHM, derivedKey, iv);
18
+
19
+ let encrypted = cipher.update(text, 'utf8', 'hex');
20
+ encrypted += cipher.final('hex');
21
+
22
+ const authTag = cipher.getAuthTag();
23
+
24
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
25
+ }
26
+
27
+ function decrypt(encryptedData, key) {
28
+ const parts = encryptedData.split(':');
29
+ if (parts.length !== 3) {
30
+ throw new Error('无效的加密格式');
31
+ }
32
+
33
+ let derivedKey;
34
+ if (typeof key === 'string') {
35
+ derivedKey = crypto.scryptSync(key, SALT, 32);
36
+ } else {
37
+ derivedKey = key;
38
+ }
39
+
40
+ const iv = Buffer.from(parts[0], 'hex');
41
+ const authTag = Buffer.from(parts[1], 'hex');
42
+ const encrypted = Buffer.from(parts[2], 'hex');
43
+
44
+ const decipher = crypto.createDecipheriv(ALGORITHM, derivedKey, iv);
45
+ decipher.setAuthTag(authTag);
46
+
47
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
48
+ decrypted += decipher.final('utf8');
49
+
50
+ return decrypted;
51
+ }
52
+
53
+ module.exports = { encrypt, decrypt };
@@ -0,0 +1,38 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const KEY_FILE = path.join(os.homedir(), '.chaimi-keep', '.mcp-secret-key');
7
+ const KEY_SIZE = 32; // 256 bits
8
+
9
+ function getOrCreateSecretKey() {
10
+ // 1. 优先使用环境变量
11
+ if (process.env.MCP_SECRET_KEY) {
12
+ return Buffer.from(process.env.MCP_SECRET_KEY, 'hex');
13
+ }
14
+
15
+ // 2. 尝试从文件加载
16
+ if (fs.existsSync(KEY_FILE)) {
17
+ const keyHex = fs.readFileSync(KEY_FILE, 'utf8').trim();
18
+ return Buffer.from(keyHex, 'hex');
19
+ }
20
+
21
+ // 3. 自动生成新密钥
22
+ const newKey = crypto.randomBytes(KEY_SIZE);
23
+ const keyHex = newKey.toString('hex');
24
+
25
+ // 确保目录存在
26
+ const keyDir = path.dirname(KEY_FILE);
27
+ if (!fs.existsSync(keyDir)) {
28
+ fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
29
+ }
30
+
31
+ // 写入文件,权限 0600
32
+ fs.writeFileSync(KEY_FILE, keyHex, { mode: 0o600 });
33
+
34
+ console.error('✅ 已自动生成安全密钥');
35
+ return newKey;
36
+ }
37
+
38
+ module.exports = { getOrCreateSecretKey, KEY_FILE };