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.
- package/package.json +1 -1
- package/server.js +203 -14
package/package.json
CHANGED
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:
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|