chaimi-keep-mcp 3.1.29 → 3.1.30
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 +79 -8
- package/package.json +1 -1
- package/server.js +96 -24
package/SKILL.md
CHANGED
|
@@ -125,28 +125,56 @@ Agent 通过 MCP 协议自动获取工具列表,无需手动查询。
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
+
### 📝 简单记账字段核对清单(save_expense)
|
|
129
|
+
|
|
130
|
+
**调用 `save_expense` 前必须检查:**
|
|
131
|
+
|
|
132
|
+
| 字段 | 是否必须 | 说明 |
|
|
133
|
+
|------|----------|------|
|
|
134
|
+
| `name` | ✅ | 商品/服务名称 |
|
|
135
|
+
| `amount` | ✅ | 金额(正数)|
|
|
136
|
+
| `category` | ✅ **新增** | 分类(如:餐饮、交通)|
|
|
137
|
+
| `date` | ✅ | 日期(ISO 8601 格式)|
|
|
138
|
+
| `store` | 可选 | 商家名称 |
|
|
139
|
+
| `note` | 可选 | 备注 |
|
|
140
|
+
|
|
141
|
+
**⚠️ 注意**:category 是记账的基本信息,必须提供!
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
128
145
|
### 🚨 小票字段核对清单(调用前必须检查)
|
|
129
146
|
|
|
130
147
|
**从 prompt 解析结果中提取的所有字段,必须全部传递给 `save_receipt`**:
|
|
131
148
|
|
|
132
|
-
| 字段 | 是否必须 | 常见遗漏 |
|
|
133
|
-
|
|
149
|
+
| 字段 | 是否必须 | 常见遗漏 | 说明 |
|
|
150
|
+
|------|----------|----------|------|
|
|
134
151
|
| `store` | ✅ | 否 | 商家名称 |
|
|
135
152
|
| `date` | ✅ **高频遗漏** | **是** | ISO 8601 格式,如:2026-01-08T11:42:27 |
|
|
136
153
|
| `totalAmount` | ✅ | 否 | 商品原价总和 |
|
|
137
154
|
| `actualAmount` | ✅ **高频遗漏** | **是** | 实付金额(优惠后)|
|
|
138
155
|
| `originalAmount` | ✅ **高频遗漏** | **是** | 原价(优惠前)|
|
|
139
156
|
| `discountAmount` | ✅ **高频遗漏** | **是** | 优惠金额 |
|
|
140
|
-
| `paymentMethod` |
|
|
141
|
-
| `items` | ✅ | 否 |
|
|
157
|
+
| `paymentMethod` | 可选 | 是 | 如:支付宝支付、微信支付 |
|
|
158
|
+
| `items` | ✅ | 否 | 商品数组,每个必须有完整字段 |
|
|
159
|
+
|
|
160
|
+
**items 数组中每个商品必须包含:**
|
|
161
|
+
|
|
162
|
+
| 字段 | 是否必须 | 说明 |
|
|
163
|
+
|------|----------|------|
|
|
164
|
+
| `name` | ✅ | 商品名称 |
|
|
165
|
+
| `amount` | ✅ | 金额(= price × quantity)|
|
|
166
|
+
| `price` | ✅ | 单价 |
|
|
167
|
+
| `quantity` | ✅ | 数量 |
|
|
168
|
+
| `category` | ✅ **新增** | 分类(如:食品、日用品)|
|
|
142
169
|
|
|
143
170
|
**核对步骤**:
|
|
144
171
|
1. 提取完小票信息后,对照上表检查每个字段
|
|
145
|
-
2. **重点检查标记"高频遗漏"的字段**:date、actualAmount、originalAmount、discountAmount
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
172
|
+
2. **重点检查标记"高频遗漏"的字段**:date、actualAmount、originalAmount、discountAmount
|
|
173
|
+
3. **确保每个商品都有 category**,这是记账的基本信息
|
|
174
|
+
4. 确认所有字段都在 args 中后再调用 `save_receipt`
|
|
175
|
+
5. 如果某个字段小票上确实没有,可以传空字符串或 0,但不能省略
|
|
148
176
|
|
|
149
|
-
|
|
177
|
+
**⚠️ 重要**:缺少必填字段会导致保存失败,请严格按照清单核对!
|
|
150
178
|
|
|
151
179
|
---
|
|
152
180
|
|
|
@@ -160,6 +188,49 @@ Agent 通过 MCP 协议自动获取工具列表,无需手动查询。
|
|
|
160
188
|
|
|
161
189
|
---
|
|
162
190
|
|
|
191
|
+
## 防重复录入(Agent 本地实现)
|
|
192
|
+
|
|
193
|
+
**目标**:提醒用户可能的重复录入,但不阻止保存
|
|
194
|
+
|
|
195
|
+
### 实现方式
|
|
196
|
+
|
|
197
|
+
Agent 维护本地记账记录日志(内存或本地文件):
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// 记录格式
|
|
201
|
+
{
|
|
202
|
+
store: "商家名称",
|
|
203
|
+
date: "2026-01-08", // 只记录到日期
|
|
204
|
+
actualAmount: 25.88,
|
|
205
|
+
timestamp: 1704694947000 // 记账时间戳
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 重复检测规则
|
|
210
|
+
|
|
211
|
+
- **时间窗口**:24 小时内
|
|
212
|
+
- **匹配条件**:`store` + `date(到天)` + `actualAmount` 相同
|
|
213
|
+
- **处理方式**:
|
|
214
|
+
1. 发现重复时,提醒用户:"⚠️ 24 小时内已有相似记录(华润万家春风店 25.88元),确认要重复录入吗?"
|
|
215
|
+
2. 用户确认后,继续调用 `save_receipt`
|
|
216
|
+
3. 用户取消,停止操作
|
|
217
|
+
|
|
218
|
+
### 代码示例
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
// 检查是否重复
|
|
222
|
+
function isDuplicate(store, date, actualAmount) {
|
|
223
|
+
const fingerprint = `${store}_${date.substring(0,10)}_${actualAmount}`;
|
|
224
|
+
const recentRecord = recentLogs.find(log => {
|
|
225
|
+
const timeDiff = Date.now() - log.timestamp;
|
|
226
|
+
return log.fingerprint === fingerprint && timeDiff < 24 * 60 * 60 * 1000;
|
|
227
|
+
});
|
|
228
|
+
return recentRecord;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
163
234
|
## 回复规范(必须严格遵守)
|
|
164
235
|
|
|
165
236
|
记账成功后,回复内容必须包含以下格式:
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -441,6 +441,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
441
441
|
|
|
442
442
|
switch (name) {
|
|
443
443
|
case 'save_expense': {
|
|
444
|
+
// P0: 数据完整性检查 - 必填字段验证
|
|
445
|
+
const requiredFields = ['name', 'amount', 'category', 'date'];
|
|
446
|
+
const missingFields = [];
|
|
447
|
+
|
|
448
|
+
for (const field of requiredFields) {
|
|
449
|
+
if (processedArgs[field] === undefined || processedArgs[field] === null || processedArgs[field] === '') {
|
|
450
|
+
missingFields.push(field);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (missingFields.length > 0) {
|
|
455
|
+
result = {
|
|
456
|
+
success: false,
|
|
457
|
+
error: `必填字段缺失:${missingFields.join(', ')}。请从解析结果中提取并传递所有字段。`,
|
|
458
|
+
code: 400
|
|
459
|
+
};
|
|
460
|
+
userMessage = `❌ 记账失败\n\n错误:缺少必填字段:${missingFields.join(', ')}\n\n💡 解决方案:\n1. 请检查是否从用户输入中提取了所有信息\n2. 确保传递以下字段:name(商品名)、amount(金额)、category(分类)、date(日期)\n3. 参考 SKILL.md 中的"调用前检查清单"`;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 数值校验
|
|
465
|
+
if (typeof processedArgs.amount !== 'number' || processedArgs.amount <= 0) {
|
|
466
|
+
result = {
|
|
467
|
+
success: false,
|
|
468
|
+
error: '金额必须是正数',
|
|
469
|
+
code: 400
|
|
470
|
+
};
|
|
471
|
+
userMessage = '❌ 记账失败:金额必须是正数';
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
444
475
|
const mcpParams = convertParams('save_expense', processedArgs);
|
|
445
476
|
result = await callMcpHub('addExpense', mcpParams, token);
|
|
446
477
|
|
|
@@ -459,45 +490,86 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
459
490
|
}
|
|
460
491
|
|
|
461
492
|
case 'save_receipt': {
|
|
462
|
-
//
|
|
463
|
-
const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount'
|
|
493
|
+
// P0: 数据完整性检查 - 必填字段验证
|
|
494
|
+
const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount'];
|
|
464
495
|
const missingFields = [];
|
|
465
|
-
|
|
496
|
+
|
|
497
|
+
for (const field of requiredFields) {
|
|
466
498
|
if (processedArgs[field] === undefined || processedArgs[field] === null || processedArgs[field] === '') {
|
|
467
499
|
missingFields.push(field);
|
|
468
500
|
}
|
|
469
|
-
}
|
|
501
|
+
}
|
|
470
502
|
|
|
471
503
|
if (missingFields.length > 0) {
|
|
472
|
-
|
|
473
|
-
|
|
504
|
+
result = {
|
|
505
|
+
success: false,
|
|
506
|
+
error: `必填字段缺失:${missingFields.join(', ')}。请从 get_parse_prompt 的解析结果中提取并传递所有字段。`,
|
|
507
|
+
code: 400
|
|
508
|
+
};
|
|
509
|
+
userMessage = `❌ 保存失败\n\n错误:缺少必填字段:${missingFields.join(', ')}\n\n💡 解决方案:\n1. 请检查是否从 get_parse_prompt 的解析结果中提取了所有信息\n2. 确保传递以下字段:store、date、totalAmount、actualAmount、originalAmount、discountAmount\n3. 参考 SKILL.md 中的"小票字段核对清单"`;
|
|
510
|
+
break;
|
|
474
511
|
}
|
|
475
512
|
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const itemMissingFields = [];
|
|
481
|
-
if (!item.hasOwnProperty('name')) itemMissingFields.push('name');
|
|
482
|
-
if (!item.hasOwnProperty('amount')) itemMissingFields.push('amount');
|
|
483
|
-
if (!item.hasOwnProperty('price')) itemMissingFields.push('price');
|
|
484
|
-
if (!item.hasOwnProperty('quantity')) itemMissingFields.push('quantity');
|
|
485
|
-
|
|
486
|
-
if (itemMissingFields.length > 0) {
|
|
487
|
-
invalidItems.push(`商品${index + 1}(${item.name || '未命名'})缺少字段: ${itemMissingFields.join(', ')}`);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
if (invalidItems.length > 0) {
|
|
513
|
+
// 数值校验
|
|
514
|
+
const amountFields = ['totalAmount', 'actualAmount', 'originalAmount'];
|
|
515
|
+
for (const field of amountFields) {
|
|
516
|
+
if (typeof processedArgs[field] !== 'number' || processedArgs[field] <= 0) {
|
|
492
517
|
result = {
|
|
493
518
|
success: false,
|
|
494
|
-
error:
|
|
519
|
+
error: `${field} 必须是正数`,
|
|
495
520
|
code: 400
|
|
496
521
|
};
|
|
497
|
-
userMessage = `❌
|
|
522
|
+
userMessage = `❌ 保存失败:${field} 必须是正数`;
|
|
498
523
|
break;
|
|
499
524
|
}
|
|
500
525
|
}
|
|
526
|
+
if (result && !result.success) break;
|
|
527
|
+
|
|
528
|
+
if (typeof processedArgs.discountAmount !== 'number' || processedArgs.discountAmount < 0) {
|
|
529
|
+
result = {
|
|
530
|
+
success: false,
|
|
531
|
+
error: 'discountAmount 必须是非负数',
|
|
532
|
+
code: 400
|
|
533
|
+
};
|
|
534
|
+
userMessage = '❌ 保存失败:优惠金额必须是非负数';
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 检查 items 是否存在且非空
|
|
539
|
+
if (!processedArgs.items || !Array.isArray(processedArgs.items) || processedArgs.items.length === 0) {
|
|
540
|
+
result = {
|
|
541
|
+
success: false,
|
|
542
|
+
error: 'items 不能为空,必须包含至少一个商品',
|
|
543
|
+
code: 400
|
|
544
|
+
};
|
|
545
|
+
userMessage = '❌ 保存失败:商品列表不能为空\n\n💡 请确保从 get_parse_prompt 的解析结果中正确提取了 items 数组';
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 检查 items 中每个商品的必填字段(包括 category)
|
|
550
|
+
const invalidItems = [];
|
|
551
|
+
processedArgs.items.forEach((item, index) => {
|
|
552
|
+
const itemMissingFields = [];
|
|
553
|
+
if (!item.hasOwnProperty('name') || item.name === '') itemMissingFields.push('name');
|
|
554
|
+
if (!item.hasOwnProperty('amount') || item.amount === undefined) itemMissingFields.push('amount');
|
|
555
|
+
if (!item.hasOwnProperty('price') || item.price === undefined) itemMissingFields.push('price');
|
|
556
|
+
if (!item.hasOwnProperty('quantity') || item.quantity === undefined) itemMissingFields.push('quantity');
|
|
557
|
+
if (!item.hasOwnProperty('category') || item.category === '') itemMissingFields.push('category');
|
|
558
|
+
|
|
559
|
+
if (itemMissingFields.length > 0) {
|
|
560
|
+
invalidItems.push(`商品${index + 1}(${item.name || '未命名'})缺少字段: ${itemMissingFields.join(', ')}`);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
if (invalidItems.length > 0) {
|
|
565
|
+
result = {
|
|
566
|
+
success: false,
|
|
567
|
+
error: `商品数据不完整:${invalidItems.join('; ')}。每个商品必须包含:name, amount, price, quantity, category`,
|
|
568
|
+
code: 400
|
|
569
|
+
};
|
|
570
|
+
userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的5个字段:name, amount, price, quantity, category\n3. category 是记账的基本信息,不能为空`;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
501
573
|
|
|
502
574
|
// 1. 调用 parseReceipt 重新提取小票信息(覆盖 Agent 传的数据)
|
|
503
575
|
if (processedArgs.rawInput) {
|