chaimi-keep-mcp 3.1.42 → 3.1.44-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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +208 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.1.42",
3
+ "version": "3.1.44-beta.1",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -320,6 +320,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
320
320
  required: ['name', 'amount', 'category', 'agentType', 'apiProvider', 'rawInput'],
321
321
  },
322
322
  },
323
+ {
324
+ name: 'get_text_parse_prompt',
325
+ description: '获取文字记账解析的Prompt模板,用于指导大模型如何解析用户输入的记账文字。返回的Prompt应作为system message,配合用户输入的记账文字作为user message调用大模型',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
330
+ apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
331
+ },
332
+ },
333
+ },
323
334
  {
324
335
  name: 'get_parse_prompt',
325
336
  description: '获取小票图片解析的Prompt模板,用于指导大模型如何格式化输出小票信息。返回的Prompt应作为system message,配合小票图片作为user message调用大模型',
@@ -483,11 +494,105 @@ function generateSignature(body, timestamp, secret) {
483
494
  .digest('hex');
484
495
  }
485
496
 
497
+ // MCP 调用日志记录函数
498
+ async function logMcpCall(logData) {
499
+ try {
500
+ // 异步发送日志到云函数,不阻塞主流程
501
+ const body = {
502
+ tool: 'logMcpCall',
503
+ params: logData,
504
+ };
505
+ const timestamp = Date.now().toString();
506
+ const signature = generateSignature(body, timestamp, CHAIMI_API_SECRET);
507
+
508
+ fetch(MCP_HUB_URL, {
509
+ method: 'POST',
510
+ headers: {
511
+ 'Content-Type': 'application/json',
512
+ 'Authorization': `Bearer ${await getToken()}`,
513
+ 'X-Chaimi-Signature': signature,
514
+ 'X-Chaimi-Timestamp': timestamp,
515
+ },
516
+ body: JSON.stringify(body),
517
+ }).catch(err => {
518
+ // 日志记录失败不影响主流程
519
+ console.error('MCP 调用日志记录失败:', err.message);
520
+ });
521
+ } catch (e) {
522
+ // 日志记录失败不影响主流程
523
+ console.error('MCP 调用日志记录异常:', e.message);
524
+ }
525
+ }
526
+
527
+ // 生成 traceId
528
+ function generateTraceId() {
529
+ return `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
530
+ }
531
+
532
+ // 获取操作系统信息
533
+ function getOSInfo() {
534
+ return {
535
+ osType: process.platform, // 'darwin', 'win32', 'linux'
536
+ osVersion: os.release() // '23.6.0', '10.0.19045'
537
+ };
538
+ }
539
+
540
+ // 调用 mcpHub 云函数(带日志和链路追踪)
541
+ async function callMcpHubWithLogging(tool, params, token, traceId, startTime, osInfo) {
542
+ // 记录即将调用云函数
543
+ logMcpCall({
544
+ traceId,
545
+ stage: 'cloud_calling',
546
+ toolName: tool,
547
+ timestamp: new Date().toISOString(),
548
+ logSource: 'mcp',
549
+ osType: osInfo?.osType,
550
+ osVersion: osInfo?.osVersion
551
+ });
552
+
553
+ try {
554
+ const result = await callMcpHub(tool, params, token, traceId, osInfo);
555
+
556
+ // 记录云函数调用成功
557
+ logMcpCall({
558
+ traceId,
559
+ stage: 'cloud_success',
560
+ toolName: tool,
561
+ duration: Date.now() - startTime,
562
+ timestamp: new Date().toISOString(),
563
+ logSource: 'mcp',
564
+ osType: osInfo?.osType,
565
+ osVersion: osInfo?.osVersion
566
+ });
567
+
568
+ return result;
569
+ } catch (cloudErr) {
570
+ // 记录云函数调用失败
571
+ logMcpCall({
572
+ traceId,
573
+ stage: 'cloud_failed',
574
+ toolName: tool,
575
+ error: cloudErr.message,
576
+ duration: Date.now() - startTime,
577
+ timestamp: new Date().toISOString(),
578
+ logSource: 'mcp',
579
+ osType: osInfo?.osType,
580
+ osVersion: osInfo?.osVersion
581
+ });
582
+ throw cloudErr;
583
+ }
584
+ }
585
+
486
586
  // 调用 mcpHub 云函数
487
- async function callMcpHub(tool, params, token) {
587
+ async function callMcpHub(tool, params, token, traceId, osInfo) {
488
588
  const body = {
489
589
  tool: tool,
490
- params: params,
590
+ params: {
591
+ ...params,
592
+ _traceId: traceId, // 传递 traceId 用于链路追踪
593
+ osType: osInfo?.osType, // 传递操作系统类型
594
+ osVersion: osInfo?.osVersion, // 传递操作系统版本
595
+ },
491
596
  };
492
597
  const timestamp = Date.now().toString();
493
598
  const signature = generateSignature(body, timestamp, CHAIMI_API_SECRET);
@@ -551,12 +656,42 @@ function recordSkillRead() {
551
656
  // 工具调用处理
552
657
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
553
658
  const { name, arguments: args } = request.params;
659
+ const startTime = Date.now();
660
+ const traceId = generateTraceId();
661
+ const osInfo = getOSInfo();
662
+
663
+ // 记录请求到达
664
+ logMcpCall({
665
+ traceId,
666
+ stage: 'request_received',
667
+ toolName: name,
668
+ params: sanitizeLogParams(args),
669
+ agentType: args.agentType || process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '',
670
+ apiProvider: args.apiProvider || process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '',
671
+ mcp_version: MCP_VERSION,
672
+ osType: osInfo.osType,
673
+ osVersion: osInfo.osVersion,
674
+ timestamp: new Date().toISOString(),
675
+ logSource: 'mcp'
676
+ });
554
677
 
555
678
  // 强制检查:记账工具必须先调用 get_skill
556
679
  if (name === 'save_expense' || name === 'save_receipt' || name === 'save_income') {
557
680
  const now = Date.now();
558
681
  const lastReadTime = getLastSkillReadTime();
559
682
  if (now - lastReadTime > SKILL_VALID_DURATION) {
683
+ // 记录校验失败
684
+ logMcpCall({
685
+ traceId,
686
+ stage: 'validation_failed',
687
+ toolName: name,
688
+ error: 'SKILL_NOT_READ',
689
+ duration: Date.now() - startTime,
690
+ timestamp: new Date().toISOString(),
691
+ logSource: 'mcp',
692
+ osType: osInfo.osType,
693
+ osVersion: osInfo.osVersion
694
+ });
560
695
  return {
561
696
  content: [
562
697
  {
@@ -696,7 +831,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
696
831
  }
697
832
 
698
833
  const mcpParams = convertParams('save_expense', processedArgs);
699
- result = await callMcpHub('addExpense', mcpParams, token);
834
+ result = await callMcpHubWithLogging('addExpense', mcpParams, token, traceId, startTime, osInfo);
700
835
 
701
836
  if (result.success) {
702
837
  const displayName = processedArgs.name || '未知商品';
@@ -963,7 +1098,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
963
1098
  }
964
1099
 
965
1100
  const mcpParams = convertParams('save_receipt', processedArgs);
966
- result = await callMcpHub('addReceipt', mcpParams, token);
1101
+ result = await callMcpHubWithLogging('addReceipt', mcpParams, token, traceId, startTime, osInfo);
967
1102
 
968
1103
  if (result.success) {
969
1104
  const totalAmount = processedArgs.totalAmount || processedArgs.items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
@@ -1005,7 +1140,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1005
1140
  case 'get_receipt_list': {
1006
1141
  const toolName = toolMapping[name];
1007
1142
  const mcpParams = convertParams(name, processedArgs);
1008
- result = await callMcpHub(toolName, mcpParams, token);
1143
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1009
1144
 
1010
1145
  if (result.success) {
1011
1146
  const total = result.data?.total || 0;
@@ -1046,7 +1181,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1046
1181
  case 'get_statistics': {
1047
1182
  const toolName = toolMapping[name];
1048
1183
  const mcpParams = convertParams(name, processedArgs);
1049
- result = await callMcpHub(toolName, mcpParams, token);
1184
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1050
1185
 
1051
1186
  if (result.success) {
1052
1187
  const data = result.data || {};
@@ -1094,7 +1229,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1094
1229
  case 'get_insights': {
1095
1230
  const toolName = toolMapping[name];
1096
1231
  const mcpParams = convertParams(name, processedArgs);
1097
- result = await callMcpHub(toolName, mcpParams, token);
1232
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1098
1233
 
1099
1234
  if (result.success) {
1100
1235
  const data = result.data || {};
@@ -1140,7 +1275,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1140
1275
  case 'export_data': {
1141
1276
  const toolName = toolMapping[name];
1142
1277
  const mcpParams = convertParams(name, processedArgs);
1143
- result = await callMcpHub(toolName, mcpParams, token);
1278
+ result = await callMcpHubWithLogging(toolName, mcpParams, token, traceId, startTime, osInfo);
1144
1279
 
1145
1280
  if (result.success) {
1146
1281
  const summary = result.data?.summary || {};
@@ -1194,7 +1329,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1194
1329
  }
1195
1330
 
1196
1331
  const mcpParams = convertParams('save_income', processedArgs);
1197
- result = await callMcpHub('addIncome', mcpParams, token);
1332
+ result = await callMcpHubWithLogging('addIncome', mcpParams, token, traceId, startTime, osInfo);
1198
1333
 
1199
1334
  if (result.success) {
1200
1335
  const displayName = processedArgs.name || '未知收入';
@@ -1233,23 +1368,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1233
1368
  }
1234
1369
 
1235
1370
  case 'get_text_parse_prompt': {
1236
- // 获取文字记账解析 Prompt
1237
- const textParseResult = await callMcpHub('getTextParsePrompt', {}, token);
1371
+ // 调用 mcpPrompt 云函数获取文字记账解析 Prompt
1372
+ const promptResult = await callMcpPrompt('getTextParsePrompt', {}, token);
1238
1373
 
1239
- if (textParseResult.success) {
1374
+ if (promptResult.success) {
1240
1375
  result = {
1241
1376
  success: true,
1242
1377
  data: {
1243
- prompt: textParseResult.data.prompt,
1244
- currentDate: textParseResult.data.currentDate,
1245
- currentTime: textParseResult.data.currentTime,
1246
- instructions: '请将返回的 prompt 作为 system message,把用户输入的记账文字作为 user message,调用大模型解析。解析完成后,调用 save_expense 或 save_income 工具保存结果。'
1378
+ prompt: promptResult.data.prompt,
1379
+ currentDate: promptResult.data.currentDate,
1380
+ currentTime: promptResult.data.currentTime,
1381
+ instructions: promptResult.data.instructions
1247
1382
  }
1248
1383
  };
1249
- userMessage = `✅ 获取文字记账解析 Prompt 成功\n\n当前日期: ${textParseResult.data.currentDate}\n当前时间: ${textParseResult.data.currentTime}\n\n## Prompt\n\n请将以下内容作为 system message 发送给大模型:\n\n\`\`\`\n${textParseResult.data.prompt}\n\`\`\`\n\n## 使用说明\n\n1. 将上面的 Prompt 作为 system message\n2. 将用户输入的记账文字(如"午餐 24块")作为 user message\n3. 调用大模型解析,获取 JSON 结果\n4. 解析完成后,调用 save_expense 或 save_income 工具保存结果\n\n## 解析结果示例\n\n\`\`\`json\n{\n "name": "午餐",\n "amount": 24,\n "category": "餐饮",\n "datetime": "2026-04-14T12:30:00+08:00",\n "transactionType": "expense"\n}\n\`\`\``;
1384
+ userMessage = `✅ 获取文字记账解析 Prompt 成功\n\n当前日期: ${promptResult.data.currentDate}\n当前时间: ${promptResult.data.currentTime}\n\n## Prompt\n\n请将以下内容作为 system message 发送给大模型:\n\n\`\`\`\n${promptResult.data.prompt}\n\`\`\`\n\n## 使用说明\n\n1. 将上面的 Prompt 作为 system message\n2. 将用户输入的记账文字(如"午餐 24块")作为 user message\n3. 调用大模型解析,获取 JSON 结果\n4. 解析完成后,调用 save_expense 或 save_income 工具保存结果`;
1250
1385
  } else {
1251
- result = { success: false, error: textParseResult.error };
1252
- userMessage = `❌ 获取文字记账解析 Prompt 失败:${textParseResult.error}`;
1386
+ result = { success: false, error: promptResult.error };
1387
+ userMessage = `❌ 获取文字记账解析 Prompt 失败:${promptResult.error}`;
1253
1388
  }
1254
1389
  break;
1255
1390
  }
@@ -1266,7 +1401,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1266
1401
  userAgent: request.headers?.['user-agent'] || ''
1267
1402
  };
1268
1403
 
1269
- result = await callMcpHub('addFeedback', feedbackData, token);
1404
+ result = await callMcpHubWithLogging('addFeedback', feedbackData, token, traceId, startTime, osInfo);
1270
1405
 
1271
1406
  if (result.success) {
1272
1407
  const typeText = {
@@ -1307,6 +1442,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1307
1442
  ],
1308
1443
  };
1309
1444
  } catch (error) {
1445
+ // 记录异常错误
1446
+ logMcpCall({
1447
+ traceId,
1448
+ stage: 'mcp_error',
1449
+ toolName: name,
1450
+ error: error.message,
1451
+ duration: Date.now() - startTime,
1452
+ timestamp: new Date().toISOString(),
1453
+ logSource: 'mcp',
1454
+ osType: osInfo.osType,
1455
+ osVersion: osInfo.osVersion
1456
+ });
1457
+
1310
1458
  // 处理授权错误
1311
1459
  if (error.message.startsWith('NEED_AUTH:')) {
1312
1460
  const userCode = error.message.replace('NEED_AUTH:', '');
@@ -1766,6 +1914,46 @@ async function startAuthFlowAndWait(timeout = 120000) {
1766
1914
  }
1767
1915
  }
1768
1916
 
1917
+ /**
1918
+ * 脱敏处理日志参数
1919
+ * 移除敏感信息,限制长度
1920
+ */
1921
+ function sanitizeLogParams(params) {
1922
+ if (!params || typeof params !== 'object') {
1923
+ return {};
1924
+ }
1925
+
1926
+ const sanitized = {};
1927
+ const allowedFields = [
1928
+ 'name', 'amount', 'category', 'store', 'date', 'period',
1929
+ 'format', 'types', 'limit', 'skip', 'content', 'feedType',
1930
+ 'yearMonth', 'startDate', 'endDate', 'includeReceipts'
1931
+ ];
1932
+
1933
+ for (const key of allowedFields) {
1934
+ if (params[key] !== undefined) {
1935
+ // 字符串字段长度限制
1936
+ if (typeof params[key] === 'string' && params[key].length > 200) {
1937
+ sanitized[key] = params[key].substring(0, 200) + '...';
1938
+ } else {
1939
+ sanitized[key] = params[key];
1940
+ }
1941
+ }
1942
+ }
1943
+
1944
+ // 特殊处理 items 数组(小票商品列表)
1945
+ if (params.items && Array.isArray(params.items)) {
1946
+ sanitized.itemsCount = params.items.length;
1947
+ sanitized.items = params.items.slice(0, 3).map(item => ({
1948
+ name: item.name,
1949
+ amount: item.amount,
1950
+ category: item.category
1951
+ }));
1952
+ }
1953
+
1954
+ return sanitized;
1955
+ }
1956
+
1769
1957
  // 注意:使用 console.error,避免污染 stdout(MCP 协议通信使用 stdout)
1770
1958
  main().catch((err) => {
1771
1959
  console.error('❌ MCP Server 启动失败:', err.message);