chaimi-keep-mcp 3.1.41 → 3.1.43

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +203 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.1.41",
3
+ "version": "3.1.43",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -483,11 +483,105 @@ function generateSignature(body, timestamp, secret) {
483
483
  .digest('hex');
484
484
  }
485
485
 
486
+ // MCP 调用日志记录函数
487
+ async function logMcpCall(logData) {
488
+ try {
489
+ // 异步发送日志到云函数,不阻塞主流程
490
+ const body = {
491
+ tool: 'logMcpCall',
492
+ params: logData,
493
+ };
494
+ const timestamp = Date.now().toString();
495
+ const signature = generateSignature(body, timestamp, CHAIMI_API_SECRET);
496
+
497
+ fetch(MCP_HUB_URL, {
498
+ method: 'POST',
499
+ headers: {
500
+ 'Content-Type': 'application/json',
501
+ 'Authorization': `Bearer ${await getToken()}`,
502
+ 'X-Chaimi-Signature': signature,
503
+ 'X-Chaimi-Timestamp': timestamp,
504
+ },
505
+ body: JSON.stringify(body),
506
+ }).catch(err => {
507
+ // 日志记录失败不影响主流程
508
+ console.error('MCP 调用日志记录失败:', err.message);
509
+ });
510
+ } catch (e) {
511
+ // 日志记录失败不影响主流程
512
+ console.error('MCP 调用日志记录异常:', e.message);
513
+ }
514
+ }
515
+
516
+ // 生成 traceId
517
+ function generateTraceId() {
518
+ return `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
519
+ }
520
+
521
+ // 获取操作系统信息
522
+ function getOSInfo() {
523
+ return {
524
+ osType: process.platform, // 'darwin', 'win32', 'linux'
525
+ osVersion: os.release() // '23.6.0', '10.0.19045'
526
+ };
527
+ }
528
+
529
+ // 调用 mcpHub 云函数(带日志和链路追踪)
530
+ async function callMcpHubWithLogging(tool, params, token, traceId, startTime, osInfo) {
531
+ // 记录即将调用云函数
532
+ logMcpCall({
533
+ traceId,
534
+ stage: 'cloud_calling',
535
+ toolName: tool,
536
+ timestamp: new Date().toISOString(),
537
+ logSource: 'mcp',
538
+ osType: osInfo?.osType,
539
+ osVersion: osInfo?.osVersion
540
+ });
541
+
542
+ try {
543
+ const result = await callMcpHub(tool, params, token, traceId, osInfo);
544
+
545
+ // 记录云函数调用成功
546
+ logMcpCall({
547
+ traceId,
548
+ stage: 'cloud_success',
549
+ toolName: tool,
550
+ duration: Date.now() - startTime,
551
+ timestamp: new Date().toISOString(),
552
+ logSource: 'mcp',
553
+ osType: osInfo?.osType,
554
+ osVersion: osInfo?.osVersion
555
+ });
556
+
557
+ return result;
558
+ } catch (cloudErr) {
559
+ // 记录云函数调用失败
560
+ logMcpCall({
561
+ traceId,
562
+ stage: 'cloud_failed',
563
+ toolName: tool,
564
+ error: cloudErr.message,
565
+ duration: Date.now() - startTime,
566
+ timestamp: new Date().toISOString(),
567
+ logSource: 'mcp',
568
+ osType: osInfo?.osType,
569
+ osVersion: osInfo?.osVersion
570
+ });
571
+ throw cloudErr;
572
+ }
573
+ }
574
+
486
575
  // 调用 mcpHub 云函数
487
- async function callMcpHub(tool, params, token) {
576
+ async function callMcpHub(tool, params, token, traceId, osInfo) {
488
577
  const body = {
489
578
  tool: tool,
490
- params: params,
579
+ params: {
580
+ ...params,
581
+ _traceId: traceId, // 传递 traceId 用于链路追踪
582
+ osType: osInfo?.osType, // 传递操作系统类型
583
+ osVersion: osInfo?.osVersion, // 传递操作系统版本
584
+ },
491
585
  };
492
586
  const timestamp = Date.now().toString();
493
587
  const signature = generateSignature(body, timestamp, CHAIMI_API_SECRET);
@@ -551,12 +645,42 @@ function recordSkillRead() {
551
645
  // 工具调用处理
552
646
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
553
647
  const { name, arguments: args } = request.params;
648
+ const startTime = Date.now();
649
+ const traceId = generateTraceId();
650
+ const osInfo = getOSInfo();
651
+
652
+ // 记录请求到达
653
+ logMcpCall({
654
+ traceId,
655
+ stage: 'request_received',
656
+ toolName: name,
657
+ params: sanitizeLogParams(args),
658
+ agentType: args.agentType || process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '',
659
+ apiProvider: args.apiProvider || process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '',
660
+ mcp_version: MCP_VERSION,
661
+ osType: osInfo.osType,
662
+ osVersion: osInfo.osVersion,
663
+ timestamp: new Date().toISOString(),
664
+ logSource: 'mcp'
665
+ });
554
666
 
555
667
  // 强制检查:记账工具必须先调用 get_skill
556
668
  if (name === 'save_expense' || name === 'save_receipt' || name === 'save_income') {
557
669
  const now = Date.now();
558
670
  const lastReadTime = getLastSkillReadTime();
559
671
  if (now - lastReadTime > SKILL_VALID_DURATION) {
672
+ // 记录校验失败
673
+ logMcpCall({
674
+ traceId,
675
+ stage: 'validation_failed',
676
+ toolName: name,
677
+ error: 'SKILL_NOT_READ',
678
+ duration: Date.now() - startTime,
679
+ timestamp: new Date().toISOString(),
680
+ logSource: 'mcp',
681
+ osType: osInfo.osType,
682
+ osVersion: osInfo.osVersion
683
+ });
560
684
  return {
561
685
  content: [
562
686
  {
@@ -696,7 +820,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
696
820
  }
697
821
 
698
822
  const mcpParams = convertParams('save_expense', processedArgs);
699
- result = await callMcpHub('addExpense', mcpParams, token);
823
+ result = await callMcpHubWithLogging('addExpense', mcpParams, token, traceId, startTime, osInfo);
700
824
 
701
825
  if (result.success) {
702
826
  const displayName = processedArgs.name || '未知商品';
@@ -721,9 +845,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
721
845
  insightsText = '\n\n消费洞察:' + result.insights.map(ins => ins.message || ins.title).join(';');
722
846
  }
723
847
 
724
- // 按 SKILL 规范格式:记账成功 + 友好结束语 + 消费洞察 + 版本号
848
+ // 新解锁成就(如果有)
849
+ let achievementsText = '';
850
+ if (result.newlyUnlocked && result.newlyUnlocked.length > 0) {
851
+ achievementsText = '\n\n🎉 成就解锁:' + result.newlyUnlocked.map(a => `${a.icon} ${a.title}(+${a.points}分)`).join(';');
852
+ }
853
+
854
+ // 按 SKILL 规范格式:记账成功 + 友好结束语 + 消费洞察 + 成就 + 版本号
725
855
  const displayStoreText = displayStore ? `【${displayStore}】` : '';
726
- userMessage = `✅ ${displayName}${displayStoreText} ¥${displayAmount} 已录入柴米AI记账。\n\n${friendlyEnding}${insightsText}\n-------------\nchaimi-keep-mcp: v${MCP_VERSION}`;
856
+ userMessage = `✅ ${displayName}${displayStoreText} ¥${displayAmount} 已录入柴米AI记账。\n\n${friendlyEnding}${insightsText}${achievementsText}\n-------------\nchaimi-keep-mcp: v${MCP_VERSION}`;
727
857
 
728
858
  if (!processedArgs.agentType || !processedArgs.apiProvider) {
729
859
  userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
@@ -957,7 +1087,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
957
1087
  }
958
1088
 
959
1089
  const mcpParams = convertParams('save_receipt', processedArgs);
960
- result = await callMcpHub('addReceipt', mcpParams, token);
1090
+ result = await callMcpHubWithLogging('addReceipt', mcpParams, token, traceId, startTime, osInfo);
961
1091
 
962
1092
  if (result.success) {
963
1093
  const totalAmount = processedArgs.totalAmount || processedArgs.items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
@@ -980,7 +1110,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
980
1110
  insightsText = '\n\n消费洞察:' + result.insights.map(ins => ins.message || ins.title).join(';');
981
1111
  }
982
1112
 
983
- userMessage = `✅ ${storeName} ¥${totalAmount} 已录入柴米AI记账(${itemCount}件商品)。\n\n${friendlyEnding}${insightsText}\n-------------\nchaimi-keep-mcp: v${MCP_VERSION}`;
1113
+ // 新解锁成就(如果有)
1114
+ let achievementsText = '';
1115
+ if (result.newlyUnlocked && result.newlyUnlocked.length > 0) {
1116
+ achievementsText = '\n\n🎉 成就解锁:' + result.newlyUnlocked.map(a => `${a.icon} ${a.title}(+${a.points}分)`).join(';');
1117
+ }
1118
+
1119
+ userMessage = `✅ ${storeName} ¥${totalAmount} 已录入柴米AI记账(${itemCount}件商品)。\n\n${friendlyEnding}${insightsText}${achievementsText}\n-------------\nchaimi-keep-mcp: v${MCP_VERSION}`;
984
1120
 
985
1121
  if (!processedArgs.agentType || !processedArgs.apiProvider) {
986
1122
  userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
@@ -993,7 +1129,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
993
1129
  case 'get_receipt_list': {
994
1130
  const toolName = toolMapping[name];
995
1131
  const mcpParams = convertParams(name, processedArgs);
996
- result = await callMcpHub(toolName, mcpParams, token);
1132
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
997
1133
 
998
1134
  if (result.success) {
999
1135
  const total = result.data?.total || 0;
@@ -1034,7 +1170,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1034
1170
  case 'get_statistics': {
1035
1171
  const toolName = toolMapping[name];
1036
1172
  const mcpParams = convertParams(name, processedArgs);
1037
- result = await callMcpHub(toolName, mcpParams, token);
1173
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1038
1174
 
1039
1175
  if (result.success) {
1040
1176
  const data = result.data || {};
@@ -1082,7 +1218,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1082
1218
  case 'get_insights': {
1083
1219
  const toolName = toolMapping[name];
1084
1220
  const mcpParams = convertParams(name, processedArgs);
1085
- result = await callMcpHub(toolName, mcpParams, token);
1221
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1086
1222
 
1087
1223
  if (result.success) {
1088
1224
  const data = result.data || {};
@@ -1128,7 +1264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1128
1264
  case 'export_data': {
1129
1265
  const toolName = toolMapping[name];
1130
1266
  const mcpParams = convertParams(name, processedArgs);
1131
- result = await callMcpHub(toolName, mcpParams, token);
1267
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1132
1268
 
1133
1269
  if (result.success) {
1134
1270
  const summary = result.data?.summary || {};
@@ -1182,7 +1318,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1182
1318
  }
1183
1319
 
1184
1320
  const mcpParams = convertParams('save_income', processedArgs);
1185
- result = await callMcpHub('addIncome', mcpParams, token);
1321
+ result = await callMcpHubWithLogging('addIncome', mcpParams, token, traceId, startTime, osInfo);
1186
1322
 
1187
1323
  if (result.success) {
1188
1324
  const displayName = processedArgs.name || '未知收入';
@@ -1222,7 +1358,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1222
1358
 
1223
1359
  case 'get_text_parse_prompt': {
1224
1360
  // 获取文字记账解析 Prompt
1225
- const textParseResult = await callMcpHub('getTextParsePrompt', {}, token);
1361
+ const textParseResult = await callMcpHubWithLogging('getTextParsePrompt', {}, token, traceId, startTime, osInfo);
1226
1362
 
1227
1363
  if (textParseResult.success) {
1228
1364
  result = {
@@ -1254,7 +1390,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1254
1390
  userAgent: request.headers?.['user-agent'] || ''
1255
1391
  };
1256
1392
 
1257
- result = await callMcpHub('addFeedback', feedbackData, token);
1393
+ result = await callMcpHubWithLogging('addFeedback', feedbackData, token, traceId, startTime, osInfo);
1258
1394
 
1259
1395
  if (result.success) {
1260
1396
  const typeText = {
@@ -1295,6 +1431,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1295
1431
  ],
1296
1432
  };
1297
1433
  } catch (error) {
1434
+ // 记录异常错误
1435
+ logMcpCall({
1436
+ traceId,
1437
+ stage: 'mcp_error',
1438
+ toolName: name,
1439
+ error: error.message,
1440
+ duration: Date.now() - startTime,
1441
+ timestamp: new Date().toISOString(),
1442
+ logSource: 'mcp',
1443
+ osType: osInfo.osType,
1444
+ osVersion: osInfo.osVersion
1445
+ });
1446
+
1298
1447
  // 处理授权错误
1299
1448
  if (error.message.startsWith('NEED_AUTH:')) {
1300
1449
  const userCode = error.message.replace('NEED_AUTH:', '');
@@ -1754,6 +1903,46 @@ async function startAuthFlowAndWait(timeout = 120000) {
1754
1903
  }
1755
1904
  }
1756
1905
 
1906
+ /**
1907
+ * 脱敏处理日志参数
1908
+ * 移除敏感信息,限制长度
1909
+ */
1910
+ function sanitizeLogParams(params) {
1911
+ if (!params || typeof params !== 'object') {
1912
+ return {};
1913
+ }
1914
+
1915
+ const sanitized = {};
1916
+ const allowedFields = [
1917
+ 'name', 'amount', 'category', 'store', 'date', 'period',
1918
+ 'format', 'types', 'limit', 'skip', 'content', 'feedType',
1919
+ 'yearMonth', 'startDate', 'endDate', 'includeReceipts'
1920
+ ];
1921
+
1922
+ for (const key of allowedFields) {
1923
+ if (params[key] !== undefined) {
1924
+ // 字符串字段长度限制
1925
+ if (typeof params[key] === 'string' && params[key].length > 200) {
1926
+ sanitized[key] = params[key].substring(0, 200) + '...';
1927
+ } else {
1928
+ sanitized[key] = params[key];
1929
+ }
1930
+ }
1931
+ }
1932
+
1933
+ // 特殊处理 items 数组(小票商品列表)
1934
+ if (params.items && Array.isArray(params.items)) {
1935
+ sanitized.itemsCount = params.items.length;
1936
+ sanitized.items = params.items.slice(0, 3).map(item => ({
1937
+ name: item.name,
1938
+ amount: item.amount,
1939
+ category: item.category
1940
+ }));
1941
+ }
1942
+
1943
+ return sanitized;
1944
+ }
1945
+
1757
1946
  // 注意:使用 console.error,避免污染 stdout(MCP 协议通信使用 stdout)
1758
1947
  main().catch((err) => {
1759
1948
  console.error('❌ MCP Server 启动失败:', err.message);