chaimi-keep-mcp 3.1.47-beta.4 → 3.1.47-beta.6

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/README.md CHANGED
@@ -87,6 +87,13 @@ export MCP_PROMPT_URL="你的Prompt服务地址"
87
87
 
88
88
  > 默认使用柴米AI记账官方云服务,一般无需修改。
89
89
 
90
+ ## Changelog
91
+
92
+ ### v3.1.47-beta.5 (2026-04-22)
93
+ - **修复** 输入长度限制 - 添加硬性长度检查,防止超大字符串攻击
94
+ - **修复** 错误信息泄露 - 生产环境只显示友好提示,不暴露内部错误
95
+ - **修复** JSON.stringify DoS - 添加安全序列化,防止循环引用和超大数据
96
+
90
97
  ## 常见问题
91
98
 
92
99
  **Q: 提示 "command not found: chaimi-keep-mcp"**
package/SKILL.md CHANGED
@@ -290,8 +290,8 @@ Agent **直接使用当前时间**作为消费时间,无需进行时间语义
290
290
 
291
291
  ```
292
292
  ✅ 【商品名/店名】¥【金额】
293
- 已录入【柴米AI记账】
294
- 【建议增加】自定义消费的内容,让记账信息更全面准确。
293
+ 已录入【柴米AI记账】
294
+ 【建议增加】自定义消费的内容,让记账信息更全面准确。
295
295
  ✅ 【友好结束语】
296
296
 
297
297
  消费洞察:【洞察内容】(可选)
@@ -314,9 +314,8 @@ chaimi-keep-mcp: v【版本号】
314
314
  **真实示例:**
315
315
  ```
316
316
  ✅ 【牛肉】¥24
317
- 已录入【柴米AI记账】
318
- 华润万家超市,2026年4月16日购买
319
-
317
+ 已录入【柴米AI记账】
318
+ 华润万家超市,2026年4月16日购买
320
319
  ✅ 好好吃饭哦~
321
320
 
322
321
  消费洞察:这是你本周第19次餐饮消费,要不要看看本周餐饮花了多少钱?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.1.47-beta.4",
3
+ "version": "3.1.47-beta.6",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -30,6 +30,9 @@ const MCP_VERSION = packageJson.version;
30
30
  // 导入 OAuth 模块
31
31
  const { OAuthManager, FileTokenStorage } = require('./oauth.js');
32
32
 
33
+ // 导入校验工具
34
+ const { validateDate } = require('./utils/validators.js');
35
+
33
36
  // API 签名密钥(用于验证请求来自合法 MCP Server)
34
37
  const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
35
38
 
@@ -795,38 +798,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
795
798
 
796
799
  // P1: 日期合理性检查
797
800
  if (processedArgs.date) {
798
- const inputDate = new Date(processedArgs.date);
799
- const now = new Date();
800
- const sixtyYearsAgo = new Date();
801
- sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
802
-
803
- if (isNaN(inputDate.getTime())) {
804
- result = {
805
- success: false,
806
- error: '日期格式无效',
807
- code: 400
808
- };
809
- userMessage = '❌ 记账失败:日期格式无效,请使用毫秒级时间戳(13位数字,如:xxxxxxxxxxxxx)';
810
- break;
811
- }
812
-
813
- if (inputDate > now) {
814
- result = {
815
- success: false,
816
- error: '日期不能是未来时间',
817
- code: 400
818
- };
819
- userMessage = '❌ 记账失败:日期不能是未来时间';
820
- break;
821
- }
822
-
823
- if (inputDate < sixtyYearsAgo) {
801
+ const validation = validateDate(processedArgs.date, '消费日期');
802
+ if (!validation.valid) {
824
803
  result = {
825
804
  success: false,
826
- error: '日期不能是60年前',
805
+ error: validation.error,
827
806
  code: 400
828
807
  };
829
- userMessage = '❌ 记账失败:日期不能是60年前';
808
+ userMessage = validation.message;
830
809
  break;
831
810
  }
832
811
  }
@@ -958,38 +937,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
958
937
 
959
938
  // P1: 日期合理性检查
960
939
  if (processedArgs.date) {
961
- const inputDate = new Date(processedArgs.date);
962
- const now = new Date();
963
- const sixtyYearsAgo = new Date();
964
- sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
965
-
966
- if (isNaN(inputDate.getTime())) {
967
- result = {
968
- success: false,
969
- error: '日期格式无效',
970
- code: 400
971
- };
972
- userMessage = '❌ 保存失败:日期格式无效,请使用毫秒级时间戳(13位数字,如:xxxxxxxxxxxxx)';
973
- break;
974
- }
975
-
976
- if (inputDate > now) {
940
+ const validation = validateDate(processedArgs.date, '小票日期');
941
+ if (!validation.valid) {
977
942
  result = {
978
943
  success: false,
979
- error: '日期不能是未来时间',
944
+ error: validation.error,
980
945
  code: 400
981
946
  };
982
- userMessage = '❌ 保存失败:日期不能是未来时间,请检查小票日期是否正确';
983
- break;
984
- }
985
-
986
- if (inputDate < sixtyYearsAgo) {
987
- result = {
988
- success: false,
989
- error: '日期不能是60年前',
990
- code: 400
991
- };
992
- userMessage = '❌ 保存失败:日期不能是60年前,请检查小票日期是否正确';
947
+ userMessage = validation.message + ',请检查小票日期是否正确';
993
948
  break;
994
949
  }
995
950
  }
@@ -1329,6 +1284,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1329
1284
  break;
1330
1285
  }
1331
1286
 
1287
+ // P1: 日期合理性检查
1288
+ if (processedArgs.date) {
1289
+ const validation = validateDate(processedArgs.date, '收入日期');
1290
+ if (!validation.valid) {
1291
+ result = {
1292
+ success: false,
1293
+ error: validation.error,
1294
+ code: 400
1295
+ };
1296
+ userMessage = validation.message;
1297
+ break;
1298
+ }
1299
+ }
1300
+
1332
1301
  const mcpParams = convertParams('save_income', processedArgs);
1333
1302
  result = await callMcpHubWithLogging('addIncome', mcpParams, token, traceId, startTime, osInfo, agentType, apiProvider);
1334
1303
 
@@ -1427,18 +1396,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1427
1396
  userMessage = `❌ 操作失败:${result.error || '未知错误'}`;
1428
1397
  }
1429
1398
 
1430
- // 构建完整的返回内容:userMessage + 完整的 result 数据
1431
- const fullResponse = {
1432
- userMessage: userMessage,
1433
- result: result,
1434
- mcpVersion: MCP_VERSION
1435
- };
1436
-
1437
1399
  return {
1438
1400
  content: [
1439
1401
  {
1440
1402
  type: 'text',
1441
- text: `${userMessage}\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}\n\n## 完整数据\n\`\`\`json\n${JSON.stringify(result, null, 2)}\n\`\`\``,
1403
+ text: `${userMessage}\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}\n\n## 完整数据\n\`\`\`json\n${safeStringify(result)}\n\`\`\``,
1442
1404
  },
1443
1405
  ],
1444
1406
  };
@@ -1545,11 +1507,24 @@ ${'='.repeat(60)}
1545
1507
  };
1546
1508
  }
1547
1509
 
1510
+ // 区分开发/生产环境,避免泄露内部错误信息
1511
+ const errorMessage = process.env.NODE_ENV === 'development'
1512
+ ? `Error: ${error.message}`
1513
+ : '操作失败,请稍后重试或联系客服';
1514
+
1515
+ // 记录完整错误到日志(用于调试)
1516
+ console.error('Tool execution error:', {
1517
+ tool: request.params.name,
1518
+ error: error.message,
1519
+ stack: error.stack,
1520
+ timestamp: new Date().toISOString()
1521
+ });
1522
+
1548
1523
  return {
1549
1524
  content: [
1550
1525
  {
1551
1526
  type: 'text',
1552
- text: `Error: ${error.message}`,
1527
+ text: errorMessage,
1553
1528
  },
1554
1529
  ],
1555
1530
  isError: true,
@@ -1557,8 +1532,31 @@ ${'='.repeat(60)}
1557
1532
  }
1558
1533
  });
1559
1534
 
1535
+ // 安全的 JSON 序列化函数,防止循环引用和超大数据导致内存溢出
1536
+ function safeStringify(obj, maxLength = 100000) {
1537
+ try {
1538
+ const str = JSON.stringify(obj, null, 2);
1539
+ if (str.length > maxLength) {
1540
+ return JSON.stringify({
1541
+ ...obj,
1542
+ data: '[数据过大,已省略]',
1543
+ _note: `完整数据大小:${str.length} 字符`
1544
+ }, null, 2);
1545
+ }
1546
+ return str;
1547
+ } catch (error) {
1548
+ return JSON.stringify({ error: '数据序列化失败' });
1549
+ }
1550
+ }
1551
+
1560
1552
  function sanitizeString(str, maxLength = 200) {
1561
1553
  if (!str || typeof str !== 'string') return '';
1554
+
1555
+ // 硬性限制:输入不能超过 maxLength 的 10 倍,防止超大字符串导致内存溢出
1556
+ if (str.length > maxLength * 10) {
1557
+ throw new Error(`输入过长,最大允许 ${maxLength} 字符`);
1558
+ }
1559
+
1562
1560
  return str.replace(/<[^>]*>/g, '').substring(0, maxLength);
1563
1561
  }
1564
1562