chaimi-keep-mcp 3.1.47-beta.7 → 3.1.47
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/SKILL.md +25 -52
- package/package.json +1 -1
- package/server.js +152 -15
package/SKILL.md
CHANGED
|
@@ -96,7 +96,7 @@ Agent 必须输出:
|
|
|
96
96
|
|
|
97
97
|
**如果不执行此步骤:**
|
|
98
98
|
- ❌ 直接调用 `save_expense` / `save_receipt` / `save_income` 会被拒绝
|
|
99
|
-
- ❌
|
|
99
|
+
- ❌ 返回错误:没有读取 Skill 文件,请先调用 get_skill 工具
|
|
100
100
|
|
|
101
101
|
**正确流程:**
|
|
102
102
|
```
|
|
@@ -195,6 +195,20 @@ get_statistics period="this_week"
|
|
|
195
195
|
|
|
196
196
|
## 工具详细说明
|
|
197
197
|
|
|
198
|
+
### 时间戳规则(所有记账工具通用)
|
|
199
|
+
|
|
200
|
+
**要求:** 13 位毫秒级时间戳(北京时间 UTC+8)
|
|
201
|
+
|
|
202
|
+
**⚠️ 常见错误:** 不要直接把秒级时间戳 × 1000(会导致结尾是 000)
|
|
203
|
+
|
|
204
|
+
**示例:**
|
|
205
|
+
- 当前时间:`Date.now()`
|
|
206
|
+
- 指定时间:`new Date("2026-04-21T14:30:00+08:00").getTime()`
|
|
207
|
+
|
|
208
|
+
**校验:** 13 位数字,不能是未来时间
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
198
212
|
### 文字记账(save_expense)
|
|
199
213
|
|
|
200
214
|
**流程:**
|
|
@@ -202,20 +216,6 @@ get_statistics period="this_week"
|
|
|
202
216
|
2. 使用 Prompt 解析用户输入,获取 JSON 结果
|
|
203
217
|
3. 调用 `save_expense` 保存(传入所有解析结果字段)
|
|
204
218
|
|
|
205
|
-
**时间处理规则:**
|
|
206
|
-
|
|
207
|
-
Agent **直接使用当前时间**作为消费时间,无需进行时间语义解析。
|
|
208
|
-
|
|
209
|
-
**最终传入的 `date` 参数格式:**毫秒级时间戳(13位数字)⚠️ **⚠️ ⚠️ 重要 ⚠️ ⚠️ ⚠️**:
|
|
210
|
-
|
|
211
|
-
- **必须使用 date 命令计算时间戳,禁止手动填写或猜测**
|
|
212
|
-
- **强制使用以下命令:**
|
|
213
|
-
```bash
|
|
214
|
-
date -j -f "%Y-%m-%d %H:%M:%S" "YYYY-MM-DD HH:MM:SS" +%s000
|
|
215
|
-
```
|
|
216
|
-
- 如果未使用 date 命令计算,服务端将拒绝保存
|
|
217
|
-
- 示例格式:`YYYY-MM-DD HH:mm:ss` → `[毫秒时间戳]`(北京时间),禁止使用示例中的具体数字,必须根据当前实际时间重新计算
|
|
218
|
-
|
|
219
219
|
---
|
|
220
220
|
|
|
221
221
|
### 收入记账(save_income)
|
|
@@ -225,20 +225,6 @@ Agent **直接使用当前时间**作为消费时间,无需进行时间语义
|
|
|
225
225
|
2. 使用 Prompt 解析用户输入,获取 JSON 结果
|
|
226
226
|
3. 调用 `save_income` 保存(传入所有解析结果字段)
|
|
227
227
|
|
|
228
|
-
**时间处理规则:**
|
|
229
|
-
|
|
230
|
-
Agent **直接使用当前时间**作为消费时间,无需进行时间语义解析。
|
|
231
|
-
|
|
232
|
-
**最终传入的 `date` 参数格式:**毫秒级时间戳(13位数字)⚠️ **⚠️ ⚠️ 重要 ⚠️ ⚠️ ⚠️**:
|
|
233
|
-
|
|
234
|
-
- **必须使用 date 命令计算时间戳,禁止手动填写或猜测**
|
|
235
|
-
- **强制使用以下命令:**
|
|
236
|
-
```bash
|
|
237
|
-
date -j -f "%Y-%m-%d %H:%M:%S" "YYYY-MM-DD HH:MM:SS" +%s000
|
|
238
|
-
```
|
|
239
|
-
- 如果未使用 date 命令计算,服务端将拒绝保存
|
|
240
|
-
- 示例格式:`YYYY-MM-DD HH:mm:ss` → `[毫秒时间戳]`(北京时间),禁止使用示例中的具体数字,必须根据当前实际时间重新计算
|
|
241
|
-
|
|
242
228
|
---
|
|
243
229
|
|
|
244
230
|
### 小票记账(save_receipt)
|
|
@@ -249,20 +235,6 @@ Agent **直接使用当前时间**作为消费时间,无需进行时间语义
|
|
|
249
235
|
3. **字段核对**:确保所有解析结果字段都传给 `save_receipt`
|
|
250
236
|
4. 调用 `save_receipt` 保存
|
|
251
237
|
|
|
252
|
-
**时间处理规则:**
|
|
253
|
-
|
|
254
|
-
Agent **直接使用当前时间**作为消费时间,无需进行时间语义解析。
|
|
255
|
-
|
|
256
|
-
**最终传入的 `date` 参数格式:**毫秒级时间戳(13位数字)⚠️ **⚠️ ⚠️ 重要 ⚠️ ⚠️ ⚠️**:
|
|
257
|
-
|
|
258
|
-
- **必须使用 date 命令计算时间戳,禁止手动填写或猜测**
|
|
259
|
-
- **强制使用以下命令:**
|
|
260
|
-
```bash
|
|
261
|
-
date -j -f "%Y-%m-%d %H:%M:%S" "YYYY-MM-DD HH:MM:SS" +%s000
|
|
262
|
-
```
|
|
263
|
-
- 如果未使用 date 命令计算,服务端将拒绝保存
|
|
264
|
-
- 示例格式:`YYYY-MM-DD HH:mm:ss` → `[毫秒时间戳]`(北京时间),禁止使用示例中的具体数字,必须根据当前实际时间重新计算
|
|
265
|
-
|
|
266
238
|
---
|
|
267
239
|
|
|
268
240
|
## 确认规则
|
|
@@ -286,12 +258,12 @@ Agent **直接使用当前时间**作为消费时间,无需进行时间语义
|
|
|
286
258
|
|
|
287
259
|
## 回复规范(必须严格遵守)
|
|
288
260
|
|
|
289
|
-
|
|
261
|
+
记账成功后,使用以下格式回复(代码块内每行一个换行):
|
|
290
262
|
|
|
291
263
|
```
|
|
292
|
-
✅ 【商品名/店名】¥【金额】
|
|
264
|
+
✅ 【商品名/店名】¥【金额】
|
|
293
265
|
已录入【柴米AI记账】
|
|
294
|
-
|
|
266
|
+
【补充信息,如购买地点日期】
|
|
295
267
|
✅ 【友好结束语】
|
|
296
268
|
|
|
297
269
|
消费洞察:【洞察内容】(可选)
|
|
@@ -300,11 +272,11 @@ chaimi-keep-mcp: v【版本号】
|
|
|
300
272
|
```
|
|
301
273
|
|
|
302
274
|
**友好结束语参考:**
|
|
303
|
-
- 餐饮类:用餐愉快!🍚 /
|
|
304
|
-
- 食品类:吃好喝好!🍎
|
|
305
|
-
- 交通类:出行顺利!🚗
|
|
306
|
-
- 购物类:买得开心!🛍️
|
|
307
|
-
- 收入类:入账顺利!💰
|
|
275
|
+
- 餐饮类:用餐愉快!🍚 / 好好吃饭哦~
|
|
276
|
+
- 食品类:吃好喝好!🍎
|
|
277
|
+
- 交通类:出行顺利!🚗
|
|
278
|
+
- 购物类:买得开心!🛍️
|
|
279
|
+
- 收入类:入账顺利!💰
|
|
308
280
|
- 通用:记账完成!继续保持~ ✨
|
|
309
281
|
|
|
310
282
|
**消费洞察(可选):**
|
|
@@ -312,8 +284,9 @@ chaimi-keep-mcp: v【版本号】
|
|
|
312
284
|
- 示例:这是你本周第19次餐饮消费,要不要看看本周餐饮花了多少钱?
|
|
313
285
|
|
|
314
286
|
**真实示例:**
|
|
287
|
+
|
|
315
288
|
```
|
|
316
|
-
✅ 【牛肉】¥24
|
|
289
|
+
✅ 【牛肉】¥24
|
|
317
290
|
已录入【柴米AI记账】
|
|
318
291
|
华润万家超市,2026年4月16日购买
|
|
319
292
|
✅ 好好吃饭哦~
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -36,6 +36,93 @@ const { validateDate } = require('./utils/validators.js');
|
|
|
36
36
|
// API 签名密钥(用于验证请求来自合法 MCP Server)
|
|
37
37
|
const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
|
|
38
38
|
|
|
39
|
+
// ==================== 请求频率限制 ====================
|
|
40
|
+
|
|
41
|
+
// 限流配置
|
|
42
|
+
const RATE_LIMITS = {
|
|
43
|
+
// 记账工具:30 秒 3 次
|
|
44
|
+
write: {
|
|
45
|
+
tools: ['save_expense', 'save_receipt', 'save_income'],
|
|
46
|
+
maxRequests: 3,
|
|
47
|
+
windowMs: 30 * 1000, // 30 秒
|
|
48
|
+
},
|
|
49
|
+
// 查询工具:30 秒 2 次
|
|
50
|
+
read: {
|
|
51
|
+
tools: ['get_expenses', 'get_receipt_list', 'get_statistics', 'get_insights', 'export_data'],
|
|
52
|
+
maxRequests: 2,
|
|
53
|
+
windowMs: 30 * 1000, // 30 秒
|
|
54
|
+
},
|
|
55
|
+
// 配置工具:30 秒 3 次
|
|
56
|
+
config: {
|
|
57
|
+
tools: ['get_skill', 'get_parse_prompt', 'get_text_parse_prompt'],
|
|
58
|
+
maxRequests: 3,
|
|
59
|
+
windowMs: 30 * 1000, // 30 秒
|
|
60
|
+
},
|
|
61
|
+
// 反馈提交:30 秒 1 次
|
|
62
|
+
feedback: {
|
|
63
|
+
tools: ['submit_feedback'],
|
|
64
|
+
maxRequests: 1,
|
|
65
|
+
windowMs: 30 * 1000, // 30 秒
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// 限流记录:{ toolName: [timestamp1, timestamp2, ...] }
|
|
70
|
+
const rateLimitRecords = {};
|
|
71
|
+
|
|
72
|
+
// 检查是否超过频率限制
|
|
73
|
+
function checkRateLimit(toolName) {
|
|
74
|
+
// 查找工具所属的限流组
|
|
75
|
+
let limitConfig = null;
|
|
76
|
+
let limitType = null;
|
|
77
|
+
|
|
78
|
+
for (const [type, config] of Object.entries(RATE_LIMITS)) {
|
|
79
|
+
if (config.tools.includes(toolName)) {
|
|
80
|
+
limitConfig = config;
|
|
81
|
+
limitType = type;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 如果工具不在限流列表中,直接通过
|
|
87
|
+
if (!limitConfig) {
|
|
88
|
+
return { allowed: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const windowStart = now - limitConfig.windowMs;
|
|
93
|
+
|
|
94
|
+
// 初始化记录
|
|
95
|
+
if (!rateLimitRecords[toolName]) {
|
|
96
|
+
rateLimitRecords[toolName] = [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 清理过期记录
|
|
100
|
+
rateLimitRecords[toolName] = rateLimitRecords[toolName].filter(
|
|
101
|
+
timestamp => timestamp > windowStart
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// 检查是否超过限制
|
|
105
|
+
if (rateLimitRecords[toolName].length >= limitConfig.maxRequests) {
|
|
106
|
+
const oldestRequest = rateLimitRecords[toolName][0];
|
|
107
|
+
const resetTime = oldestRequest + limitConfig.windowMs;
|
|
108
|
+
const waitSeconds = Math.ceil((resetTime - now) / 1000);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
allowed: false,
|
|
112
|
+
limitType,
|
|
113
|
+
maxRequests: limitConfig.maxRequests,
|
|
114
|
+
windowMs: limitConfig.windowMs,
|
|
115
|
+
resetTime,
|
|
116
|
+
waitSeconds,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 记录本次请求
|
|
121
|
+
rateLimitRecords[toolName].push(now);
|
|
122
|
+
|
|
123
|
+
return { allowed: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
39
126
|
// URL 加密密钥
|
|
40
127
|
const URL_ENCRYPT_KEY = 'chaimi-url-key-2024';
|
|
41
128
|
|
|
@@ -676,6 +763,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
676
763
|
logSource: 'mcp'
|
|
677
764
|
});
|
|
678
765
|
|
|
766
|
+
// 检查频率限制
|
|
767
|
+
const rateLimitResult = checkRateLimit(name);
|
|
768
|
+
if (!rateLimitResult.allowed) {
|
|
769
|
+
const waitTime = rateLimitResult.waitSeconds < 60
|
|
770
|
+
? `${rateLimitResult.waitSeconds} 秒`
|
|
771
|
+
: `${Math.ceil(rateLimitResult.waitSeconds / 60)} 分钟`;
|
|
772
|
+
|
|
773
|
+
// 记录限流日志
|
|
774
|
+
logMcpCall({
|
|
775
|
+
traceId,
|
|
776
|
+
stage: 'rate_limited',
|
|
777
|
+
toolName: name,
|
|
778
|
+
error: 'RATE_LIMIT_EXCEEDED',
|
|
779
|
+
duration: Date.now() - startTime,
|
|
780
|
+
agentType,
|
|
781
|
+
apiProvider,
|
|
782
|
+
mcpVersion: MCP_VERSION,
|
|
783
|
+
osType: osInfo.osType,
|
|
784
|
+
osVersion: osInfo.osVersion,
|
|
785
|
+
timestamp: new Date().toISOString(),
|
|
786
|
+
logSource: 'mcp'
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
content: [
|
|
791
|
+
{
|
|
792
|
+
type: 'text',
|
|
793
|
+
text: JSON.stringify({
|
|
794
|
+
success: false,
|
|
795
|
+
error: `请求过于频繁,30 秒内最多调用 ${rateLimitResult.maxRequests} 次,请 ${waitTime} 后再试`,
|
|
796
|
+
code: 429
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
],
|
|
800
|
+
isError: true,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
679
804
|
// 强制检查:记账工具必须先调用 get_skill
|
|
680
805
|
if (name === 'save_expense' || name === 'save_receipt' || name === 'save_income') {
|
|
681
806
|
const now = Date.now();
|
|
@@ -1582,14 +1707,26 @@ function formatDateWithTimezone(dateInput) {
|
|
|
1582
1707
|
}
|
|
1583
1708
|
|
|
1584
1709
|
try {
|
|
1585
|
-
|
|
1710
|
+
let timestamp;
|
|
1711
|
+
|
|
1712
|
+
// 如果是数字(时间戳)
|
|
1586
1713
|
if (typeof dateInput === 'number') {
|
|
1587
|
-
|
|
1714
|
+
timestamp = dateInput;
|
|
1588
1715
|
}
|
|
1589
|
-
|
|
1590
1716
|
// 如果是字符串形式的数字(时间戳)
|
|
1591
|
-
if (/^\d+$/.test(dateInput)) {
|
|
1592
|
-
|
|
1717
|
+
else if (/^\d+$/.test(dateInput)) {
|
|
1718
|
+
timestamp = parseInt(dateInput, 10);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// 检测是否是假的时间戳(秒级转毫秒级,结尾是000)
|
|
1722
|
+
if (timestamp && timestamp % 1000 === 0 && String(timestamp).length === 13) {
|
|
1723
|
+
console.error(`⚠️ 警告:检测到可能错误的时间戳(秒级转毫秒级):${timestamp},使用当前时间替代`);
|
|
1724
|
+
return Date.now();
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// 如果是数字时间戳且没问题,直接返回
|
|
1728
|
+
if (timestamp) {
|
|
1729
|
+
return timestamp;
|
|
1593
1730
|
}
|
|
1594
1731
|
|
|
1595
1732
|
// 如果已经是 UTC 格式(以 Z 结尾),转换为时间戳
|
|
@@ -1613,12 +1750,12 @@ function formatDateWithTimezone(dateInput) {
|
|
|
1613
1750
|
}
|
|
1614
1751
|
|
|
1615
1752
|
// 其他情况,尝试解析为时间戳
|
|
1616
|
-
const
|
|
1617
|
-
if (isNaN(
|
|
1753
|
+
const parsedTimestamp = new Date(dateInput).getTime();
|
|
1754
|
+
if (isNaN(parsedTimestamp)) {
|
|
1618
1755
|
return Date.now(); // 解析失败,返回当前时间戳
|
|
1619
1756
|
}
|
|
1620
1757
|
|
|
1621
|
-
return
|
|
1758
|
+
return parsedTimestamp;
|
|
1622
1759
|
} catch (error) {
|
|
1623
1760
|
console.error('日期格式化错误:', error);
|
|
1624
1761
|
return Date.now();
|
|
@@ -1689,8 +1826,8 @@ function convertParams(toolName, args) {
|
|
|
1689
1826
|
transactionType: 'expense',
|
|
1690
1827
|
note: sanitizeString(args.note, 500) || '',
|
|
1691
1828
|
rawInput: sanitizeString(args.rawInput, 1000) || '',
|
|
1692
|
-
agentType: args.agentType || '',
|
|
1693
|
-
apiProvider: args.apiProvider || '',
|
|
1829
|
+
agentType: sanitizeString(args.agentType, 50) || '',
|
|
1830
|
+
apiProvider: sanitizeString(args.apiProvider, 50) || '',
|
|
1694
1831
|
mcpVersion: MCP_VERSION,
|
|
1695
1832
|
source: 'mcp_txt_expense',
|
|
1696
1833
|
};
|
|
@@ -1722,7 +1859,7 @@ function convertParams(toolName, args) {
|
|
|
1722
1859
|
amount: amount,
|
|
1723
1860
|
category: sanitizeString(item.category, 50) || '其他',
|
|
1724
1861
|
subCategory: sanitizeString(item.subCategory, 50) || '',
|
|
1725
|
-
transactionType: item.transactionType || 'expense',
|
|
1862
|
+
transactionType: sanitizeString(item.transactionType, 50) || 'expense',
|
|
1726
1863
|
price: validateAmount(item.price),
|
|
1727
1864
|
quantity: item.quantity ? String(item.quantity).substring(0, 20) : '1',
|
|
1728
1865
|
unit: sanitizeString(item.unit, 20),
|
|
@@ -1743,8 +1880,8 @@ function convertParams(toolName, args) {
|
|
|
1743
1880
|
totalPoints: parseInt(args.totalPoints) || 0,
|
|
1744
1881
|
date: formatDateWithTimezone(args.date),
|
|
1745
1882
|
rawInput: sanitizeString(args.rawInput, 2000),
|
|
1746
|
-
agentType: args.agentType || '',
|
|
1747
|
-
apiProvider: args.apiProvider || '',
|
|
1883
|
+
agentType: sanitizeString(args.agentType, 50) || '',
|
|
1884
|
+
apiProvider: sanitizeString(args.apiProvider, 50) || '',
|
|
1748
1885
|
mcpVersion: MCP_VERSION,
|
|
1749
1886
|
source: 'mcp_receipt',
|
|
1750
1887
|
storeCategory: sanitizeString(args.storeCategory, 50) || '其他',
|
|
@@ -1775,8 +1912,8 @@ function convertParams(toolName, args) {
|
|
|
1775
1912
|
note: sanitizeString(args.note, 500),
|
|
1776
1913
|
transactionType: 'income',
|
|
1777
1914
|
rawInput: sanitizeString(args.rawInput, 1000),
|
|
1778
|
-
agentType: args.agentType || '',
|
|
1779
|
-
apiProvider: args.apiProvider || '',
|
|
1915
|
+
agentType: sanitizeString(args.agentType, 50) || '',
|
|
1916
|
+
apiProvider: sanitizeString(args.apiProvider, 50) || '',
|
|
1780
1917
|
mcpVersion: MCP_VERSION,
|
|
1781
1918
|
source: 'mcp_txt_income',
|
|
1782
1919
|
};
|