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.
Files changed (3) hide show
  1. package/SKILL.md +79 -8
  2. package/package.json +1 -1
  3. 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` | ✅ | 否 | 商品数组,每个必须有 name/amount/price/quantity |
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、paymentMethod
146
- 3. 确认所有字段都在 args 中后再调用 `save_receipt`
147
- 4. 如果某个字段小票上确实没有,可以传空字符串或 0,但不能省略
172
+ 2. **重点检查标记"高频遗漏"的字段**:date、actualAmount、originalAmount、discountAmount
173
+ 3. **确保每个商品都有 category**,这是记账的基本信息
174
+ 4. 确认所有字段都在 args 中后再调用 `save_receipt`
175
+ 5. 如果某个字段小票上确实没有,可以传空字符串或 0,但不能省略
148
176
 
149
- **注意**:MCP 工具会不断迭代更新,请以实际通过 MCP 协议获取的工具定义为准。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.1.29",
3
+ "version": "3.1.30",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- // 0. 参数完整性检查 - 提醒 Agent 哪些字段缺失
463
- const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount', 'paymentMethod'];
493
+ // P0: 数据完整性检查 - 必填字段验证
494
+ const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount'];
464
495
  const missingFields = [];
465
- requiredFields.forEach(field => {
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
- console.error(`⚠️ 警告:save_receipt 缺少以下字段:${missingFields.join(', ')}`);
473
- console.error(` 请从 get_parse_prompt 的解析结果中提取并传递所有字段`);
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
- // 1. 参数格式强制检查 - 确保 items 中每个商品都有完整的字段
477
- if (processedArgs.items && Array.isArray(processedArgs.items)) {
478
- const invalidItems = [];
479
- processedArgs.items.forEach((item, index) => {
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: `参数格式错误:${invalidItems.join('; ')}。请更新您的 MCP Skill 或检查 items 数组格式。每个商品必须包含:name, amount, price, quantity`,
519
+ error: `${field} 必须是正数`,
495
520
  code: 400
496
521
  };
497
- userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的4个字段:name, amount, price, quantity\n3. amount 必须等于 price × quantity`;
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) {