chaimi-keep-mcp 3.1.29 → 3.1.33-beta.0

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 CHANGED
@@ -125,28 +125,111 @@ 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
+ **⚠️ 重要**:缺少必填字段会导致保存失败,请严格按照清单核对!
178
+
179
+ ---
180
+
181
+ ## 数据准确性校验(P1)
182
+
183
+ 服务器会自动校验以下规则,**校验失败会拒绝保存**:
184
+
185
+ ### 日期合理性
186
+ - ✅ 不能是未来日期
187
+ - ✅ 不能是60年前的日期
188
+ - ✅ 格式必须是 ISO 8601(如:2026-01-08T11:42:27)
189
+
190
+ ### 金额一致性(save_receipt)
191
+ - ✅ **商品金额总和 ≈ 总金额**(允许0.1元误差)
192
+ - 计算方式:`items` 中每个商品 `amount` 之和
193
+ - 注意:`amount` 必须等于 `price × quantity`
194
+ - ✅ **优惠计算正确**(允许0.1元误差)
195
+ - 计算方式:`originalAmount - discountAmount = actualAmount`
196
+
197
+ ### 常见错误
198
+ | 错误 | 原因 | 解决 |
199
+ |------|------|------|
200
+ | 商品金额总和与总金额不一致 | 商品 amount 计算错误或遗漏商品 | 检查每个商品的 amount = price × quantity |
201
+ | 优惠计算不正确 | discountAmount 填写错误 | 检查 original - discount = actual |
202
+
203
+ ---
204
+
205
+ ## 智能补全(P2)
206
+
207
+ 如果调用 `save_receipt` 时遗漏了某些字段,但提供了 `rawInput`(原始小票文本),服务器会尝试自动补全:
208
+
209
+ ### 补全逻辑
210
+ 1. 检测缺失字段(store、date、totalAmount、actualAmount 等)
211
+ 2. 调用大模型重新解析 `rawInput`
212
+ 3. 提取缺失字段并补全
213
+ 4. 补全后再次进行 P1 校验
214
+
215
+ ### 使用建议
216
+ - **尽量传递完整字段**,不要依赖智能补全
217
+ - 智能补全是兜底机制,可能因解析不准确而失败
218
+ - 如果补全后仍校验失败,会返回错误
219
+
220
+ ### rawInput 格式
221
+ ```json
222
+ {
223
+ "store": "华润万家",
224
+ "date": "2026-01-08T11:42:27",
225
+ "totalAmount": 26.59,
226
+ "actualAmount": 25.88,
227
+ "originalAmount": 26.59,
228
+ "discountAmount": 0.71,
229
+ "items": [...],
230
+ "rawInput": "华润万家春风店 2026-01-08 丛林香菇、油条、红糖馒头..."
231
+ }
232
+ ```
150
233
 
151
234
  ---
152
235
 
@@ -160,6 +243,49 @@ Agent 通过 MCP 协议自动获取工具列表,无需手动查询。
160
243
 
161
244
  ---
162
245
 
246
+ ## 防重复录入(Agent 本地实现)
247
+
248
+ **目标**:提醒用户可能的重复录入,但不阻止保存
249
+
250
+ ### 实现方式
251
+
252
+ Agent 维护本地记账记录日志(内存或本地文件):
253
+
254
+ ```javascript
255
+ // 记录格式
256
+ {
257
+ store: "商家名称",
258
+ date: "2026-01-08", // 只记录到日期
259
+ actualAmount: 25.88,
260
+ timestamp: 1704694947000 // 记账时间戳
261
+ }
262
+ ```
263
+
264
+ ### 重复检测规则
265
+
266
+ - **时间窗口**:24 小时内
267
+ - **匹配条件**:`store` + `date(到天)` + `actualAmount` 相同
268
+ - **处理方式**:
269
+ 1. 发现重复时,提醒用户:"⚠️ 24 小时内已有相似记录(华润万家春风店 25.88元),确认要重复录入吗?"
270
+ 2. 用户确认后,继续调用 `save_receipt`
271
+ 3. 用户取消,停止操作
272
+
273
+ ### 代码示例
274
+
275
+ ```javascript
276
+ // 检查是否重复
277
+ function isDuplicate(store, date, actualAmount) {
278
+ const fingerprint = `${store}_${date.substring(0,10)}_${actualAmount}`;
279
+ const recentRecord = recentLogs.find(log => {
280
+ const timeDiff = Date.now() - log.timestamp;
281
+ return log.fingerprint === fingerprint && timeDiff < 24 * 60 * 60 * 1000;
282
+ });
283
+ return recentRecord;
284
+ }
285
+ ```
286
+
287
+ ---
288
+
163
289
  ## 回复规范(必须严格遵守)
164
290
 
165
291
  记账成功后,回复内容必须包含以下格式:
@@ -244,4 +370,10 @@ MCP Server: v3.1.23
244
370
 
245
371
  ---
246
372
 
373
+ ## 相关文档
374
+
375
+ - [MCP优化需求跟踪表](../../../docs/08-项目管理/02-需求跟踪/MCP优化需求跟踪表.md) - 项目需求跟踪
376
+
377
+ ---
378
+
247
379
  *最后更新:2026-04-14*
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Skill 文件同步脚本
4
+ *
5
+ * 功能:安装时自动同步 SKILL.md 到 Skill 架构的 Agent
6
+ * 支持:OpenClaw、WorkBuddy 等
7
+ * 扩展:可通过 ~/.chaimi-mcp/skill-config.json 添加自定义路径
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ // 内置常见 Skill 架构 Agent 路径
15
+ const BUILT_IN_PATHS = [
16
+ {
17
+ name: 'OpenClaw',
18
+ skillPath: path.join(os.homedir(), '.openclaw', 'skills', 'chaimi-keep-mcp'),
19
+ platform: ['darwin', 'linux']
20
+ },
21
+ {
22
+ name: 'WorkBuddy',
23
+ skillPath: path.join(os.homedir(), '.workbuddy', 'skills', 'chaimi-keep-mcp'),
24
+ platform: ['darwin', 'linux', 'win32']
25
+ }
26
+ ];
27
+
28
+ // 用户自定义配置文件路径
29
+ const USER_CONFIG_PATH = path.join(os.homedir(), '.chaimi-mcp', 'skill-config.json');
30
+
31
+ /**
32
+ * 加载用户自定义配置
33
+ */
34
+ function loadUserConfig() {
35
+ try {
36
+ if (fs.existsSync(USER_CONFIG_PATH)) {
37
+ const config = JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf8'));
38
+ return config.skillDirectories || [];
39
+ }
40
+ } catch (error) {
41
+ console.error(`⚠️ 读取用户配置失败: ${error.message}`);
42
+ }
43
+ return [];
44
+ }
45
+
46
+ /**
47
+ * 同步 SKILL.md 到指定目录
48
+ */
49
+ function syncSkillToDirectory(targetPath, agentName) {
50
+ try {
51
+ const skillSourcePath = path.join(__dirname, '..', 'SKILL.md');
52
+
53
+ // 检查源文件是否存在
54
+ if (!fs.existsSync(skillSourcePath)) {
55
+ console.error(`❌ ${agentName}: SKILL.md 源文件不存在`);
56
+ return { success: false, error: 'source-not-found' };
57
+ }
58
+
59
+ // 检查父目录是否存在(Agent 是否安装)
60
+ const parentDir = path.dirname(targetPath);
61
+ if (!fs.existsSync(parentDir)) {
62
+ // Agent 未安装,静默跳过
63
+ return { success: false, reason: 'agent-not-installed' };
64
+ }
65
+
66
+ // 创建 skill 目录
67
+ if (!fs.existsSync(targetPath)) {
68
+ fs.mkdirSync(targetPath, { recursive: true });
69
+ }
70
+
71
+ // 复制 SKILL.md
72
+ const targetFilePath = path.join(targetPath, 'SKILL.md');
73
+ fs.copyFileSync(skillSourcePath, targetFilePath);
74
+
75
+ return { success: true, path: targetFilePath };
76
+
77
+ } catch (error) {
78
+ console.error(`❌ ${agentName}: 同步失败 - ${error.message}`);
79
+ return { success: false, error: error.message };
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 主函数
85
+ */
86
+ function main() {
87
+ console.log('📋 同步 Skill 文件到 Agent...\n');
88
+
89
+ const results = [];
90
+
91
+ // 1. 同步内置路径
92
+ for (const agent of BUILT_IN_PATHS) {
93
+ // 平台检查
94
+ if (agent.platform && !agent.platform.includes(os.platform())) {
95
+ continue;
96
+ }
97
+
98
+ const result = syncSkillToDirectory(agent.skillPath, agent.name);
99
+ results.push({ agent: agent.name, ...result });
100
+
101
+ if (result.success) {
102
+ console.log(`✅ ${agent.name}: Skill 文件已同步`);
103
+ } else if (result.reason === 'agent-not-installed') {
104
+ // 静默跳过未安装的 Agent
105
+ } else {
106
+ console.error(`❌ ${agent.name}: 同步失败`);
107
+ }
108
+ }
109
+
110
+ // 2. 同步用户自定义路径
111
+ const userPaths = loadUserConfig();
112
+ for (const userPath of userPaths) {
113
+ if (!userPath.name || !userPath.path) {
114
+ console.error(`⚠️ 无效的用户配置: ${JSON.stringify(userPath)}`);
115
+ continue;
116
+ }
117
+
118
+ const result = syncSkillToDirectory(userPath.path, userPath.name);
119
+ results.push({ agent: userPath.name, ...result });
120
+
121
+ if (result.success) {
122
+ console.log(`✅ ${userPath.name}: Skill 文件已同步(自定义)`);
123
+ } else if (result.reason !== 'agent-not-installed') {
124
+ console.error(`❌ ${userPath.name}: 同步失败`);
125
+ }
126
+ }
127
+
128
+ // 统计
129
+ const successCount = results.filter(r => r.success).length;
130
+ const skipCount = results.filter(r => r.reason === 'agent-not-installed').length;
131
+
132
+ console.log(`\n📊 同步完成: ${successCount} 个成功, ${skipCount} 个跳过(未安装)\n`);
133
+
134
+ // 提示用户如何添加自定义路径
135
+ if (userPaths.length === 0) {
136
+ console.log('💡 提示: 如需添加其他 Skill 架构 Agent,可创建配置文件:');
137
+ console.log(` ${USER_CONFIG_PATH}`);
138
+ console.log(' 格式: {"skillDirectories": [{"name": "Agent名称", "path": "skill目录路径"}]}\n');
139
+ }
140
+ }
141
+
142
+ // 执行
143
+ main();
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.33-beta.0",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "type": "commonjs",
19
19
  "scripts": {
20
20
  "start": "node server.js",
21
- "dev": "node server.js"
21
+ "dev": "node server.js",
22
+ "postinstall": "node bin/sync-skill.js"
22
23
  },
23
24
  "keywords": [
24
25
  "mcp",
package/server.js CHANGED
@@ -441,6 +441,75 @@ 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
+
475
+ // P1: 日期合理性检查
476
+ if (processedArgs.date) {
477
+ const inputDate = new Date(processedArgs.date);
478
+ const now = new Date();
479
+ const sixtyYearsAgo = new Date();
480
+ sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
481
+
482
+ if (isNaN(inputDate.getTime())) {
483
+ result = {
484
+ success: false,
485
+ error: '日期格式无效',
486
+ code: 400
487
+ };
488
+ userMessage = '❌ 记账失败:日期格式无效,请使用 ISO 8601 格式(如:2026-01-08T11:42:27)';
489
+ break;
490
+ }
491
+
492
+ if (inputDate > now) {
493
+ result = {
494
+ success: false,
495
+ error: '日期不能是未来时间',
496
+ code: 400
497
+ };
498
+ userMessage = '❌ 记账失败:日期不能是未来时间';
499
+ break;
500
+ }
501
+
502
+ if (inputDate < sixtyYearsAgo) {
503
+ result = {
504
+ success: false,
505
+ error: '日期不能是60年前',
506
+ code: 400
507
+ };
508
+ userMessage = '❌ 记账失败:日期不能是60年前';
509
+ break;
510
+ }
511
+ }
512
+
444
513
  const mcpParams = convertParams('save_expense', processedArgs);
445
514
  result = await callMcpHub('addExpense', mcpParams, token);
446
515
 
@@ -459,44 +528,190 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
459
528
  }
460
529
 
461
530
  case 'save_receipt': {
462
- // 0. 参数完整性检查 - 提醒 Agent 哪些字段缺失
463
- const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount', 'paymentMethod'];
531
+ // P0: 数据完整性检查 - 必填字段验证
532
+ const requiredFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount'];
464
533
  const missingFields = [];
465
- requiredFields.forEach(field => {
534
+
535
+ for (const field of requiredFields) {
466
536
  if (processedArgs[field] === undefined || processedArgs[field] === null || processedArgs[field] === '') {
467
537
  missingFields.push(field);
468
538
  }
469
- });
539
+ }
470
540
 
471
541
  if (missingFields.length > 0) {
472
- console.error(`⚠️ 警告:save_receipt 缺少以下字段:${missingFields.join(', ')}`);
473
- console.error(` 请从 get_parse_prompt 的解析结果中提取并传递所有字段`);
542
+ result = {
543
+ success: false,
544
+ error: `必填字段缺失:${missingFields.join(', ')}。请从 get_parse_prompt 的解析结果中提取并传递所有字段。`,
545
+ code: 400
546
+ };
547
+ userMessage = `❌ 保存失败\n\n错误:缺少必填字段:${missingFields.join(', ')}\n\n💡 解决方案:\n1. 请检查是否从 get_parse_prompt 的解析结果中提取了所有信息\n2. 确保传递以下字段:store、date、totalAmount、actualAmount、originalAmount、discountAmount\n3. 参考 SKILL.md 中的"小票字段核对清单"`;
548
+ break;
474
549
  }
475
550
 
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
- });
551
+ // 数值校验
552
+ const amountFields = ['totalAmount', 'actualAmount', 'originalAmount'];
553
+ for (const field of amountFields) {
554
+ if (typeof processedArgs[field] !== 'number' || processedArgs[field] <= 0) {
555
+ result = {
556
+ success: false,
557
+ error: `${field} 必须是正数`,
558
+ code: 400
559
+ };
560
+ userMessage = `❌ 保存失败:${field} 必须是正数`;
561
+ break;
562
+ }
563
+ }
564
+ if (result && !result.success) break;
565
+
566
+ if (typeof processedArgs.discountAmount !== 'number' || processedArgs.discountAmount < 0) {
567
+ result = {
568
+ success: false,
569
+ error: 'discountAmount 必须是非负数',
570
+ code: 400
571
+ };
572
+ userMessage = '❌ 保存失败:优惠金额必须是非负数';
573
+ break;
574
+ }
575
+
576
+ // 检查 items 是否存在且非空
577
+ if (!processedArgs.items || !Array.isArray(processedArgs.items) || processedArgs.items.length === 0) {
578
+ result = {
579
+ success: false,
580
+ error: 'items 不能为空,必须包含至少一个商品',
581
+ code: 400
582
+ };
583
+ userMessage = '❌ 保存失败:商品列表不能为空\n\n💡 请确保从 get_parse_prompt 的解析结果中正确提取了 items 数组';
584
+ break;
585
+ }
586
+
587
+ // 检查 items 中每个商品的必填字段(包括 category)
588
+ const invalidItems = [];
589
+ processedArgs.items.forEach((item, index) => {
590
+ const itemMissingFields = [];
591
+ if (!item.hasOwnProperty('name') || item.name === '') itemMissingFields.push('name');
592
+ if (!item.hasOwnProperty('amount') || item.amount === undefined) itemMissingFields.push('amount');
593
+ if (!item.hasOwnProperty('price') || item.price === undefined) itemMissingFields.push('price');
594
+ if (!item.hasOwnProperty('quantity') || item.quantity === undefined) itemMissingFields.push('quantity');
595
+ if (!item.hasOwnProperty('category') || item.category === '') itemMissingFields.push('category');
490
596
 
491
- if (invalidItems.length > 0) {
597
+ if (itemMissingFields.length > 0) {
598
+ invalidItems.push(`商品${index + 1}(${item.name || '未命名'})缺少字段: ${itemMissingFields.join(', ')}`);
599
+ }
600
+ });
601
+
602
+ if (invalidItems.length > 0) {
603
+ result = {
604
+ success: false,
605
+ error: `商品数据不完整:${invalidItems.join('; ')}。每个商品必须包含:name, amount, price, quantity, category`,
606
+ code: 400
607
+ };
608
+ userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的5个字段:name, amount, price, quantity, category\n3. category 是记账的基本信息,不能为空`;
609
+ break;
610
+ }
611
+
612
+ // P1: 日期合理性检查
613
+ if (processedArgs.date) {
614
+ const inputDate = new Date(processedArgs.date);
615
+ const now = new Date();
616
+ const sixtyYearsAgo = new Date();
617
+ sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
618
+
619
+ if (isNaN(inputDate.getTime())) {
492
620
  result = {
493
621
  success: false,
494
- error: `参数格式错误:${invalidItems.join('; ')}。请更新您的 MCP Skill 或检查 items 数组格式。每个商品必须包含:name, amount, price, quantity`,
622
+ error: '日期格式无效',
495
623
  code: 400
496
624
  };
497
- userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的4个字段:name, amount, price, quantity\n3. amount 必须等于 price × quantity`;
625
+ userMessage = ' 保存失败:日期格式无效,请使用 ISO 8601 格式(如:2026-01-08T11:42:27)';
498
626
  break;
499
627
  }
628
+
629
+ if (inputDate > now) {
630
+ result = {
631
+ success: false,
632
+ error: '日期不能是未来时间',
633
+ code: 400
634
+ };
635
+ userMessage = '❌ 保存失败:日期不能是未来时间,请检查小票日期是否正确';
636
+ break;
637
+ }
638
+
639
+ if (inputDate < sixtyYearsAgo) {
640
+ result = {
641
+ success: false,
642
+ error: '日期不能是60年前',
643
+ code: 400
644
+ };
645
+ userMessage = '❌ 保存失败:日期不能是60年前,请检查小票日期是否正确';
646
+ break;
647
+ }
648
+ }
649
+
650
+ // P2: 智能补全 - 如果有缺失字段且提供了 rawInput,尝试重新解析
651
+ const missingFieldsForFill = [];
652
+ const fillableFields = ['store', 'date', 'totalAmount', 'actualAmount', 'originalAmount', 'discountAmount', 'paymentMethod'];
653
+ for (const field of fillableFields) {
654
+ if (processedArgs[field] === undefined || processedArgs[field] === null || processedArgs[field] === '') {
655
+ missingFieldsForFill.push(field);
656
+ }
657
+ }
658
+
659
+ if (missingFieldsForFill.length > 0 && processedArgs.rawInput) {
660
+ console.error(`⚠️ 字段缺失,尝试从 rawInput 重新解析:${missingFieldsForFill.join(', ')}`);
661
+
662
+ const parseResult = await callMcpPrompt(
663
+ 'parseReceipt',
664
+ {
665
+ text: processedArgs.rawInput,
666
+ type: 'parseReceipt'
667
+ },
668
+ token
669
+ );
670
+
671
+ if (parseResult.success && parseResult.data) {
672
+ // 只补全缺失的字段
673
+ for (const field of missingFieldsForFill) {
674
+ if (parseResult.data[field] !== undefined && parseResult.data[field] !== null && parseResult.data[field] !== '') {
675
+ processedArgs[field] = parseResult.data[field];
676
+ console.error(`✅ 补全字段:${field} = ${processedArgs[field]}`);
677
+ }
678
+ }
679
+
680
+ // 如果 items 也缺失,尝试补全
681
+ if ((!processedArgs.items || processedArgs.items.length === 0) && parseResult.data.items) {
682
+ processedArgs.items = parseResult.data.items;
683
+ console.error(`✅ 补全字段:items(${processedArgs.items.length} 个商品)`);
684
+ }
685
+ } else {
686
+ console.error(`❌ 重新解析失败:${parseResult.error || '未知错误'}`);
687
+ }
688
+ }
689
+
690
+ // P1: 金额一致性校验
691
+ const ERROR_MARGIN = 0.1; // 允许0.1元误差
692
+
693
+ // 校验1:items 各项 amount 之和 ≈ totalAmount
694
+ const itemsSum = processedArgs.items.reduce((sum, item) => sum + (item.amount || 0), 0);
695
+ if (Math.abs(itemsSum - processedArgs.totalAmount) > ERROR_MARGIN) {
696
+ result = {
697
+ success: false,
698
+ error: `商品金额总和 (${itemsSum.toFixed(2)}) 与总金额 (${processedArgs.totalAmount}) 不一致`,
699
+ code: 400
700
+ };
701
+ userMessage = `❌ 保存失败:金额校验不通过\n\n商品金额总和:${itemsSum.toFixed(2)}元\n小票总金额:${processedArgs.totalAmount}元\n\n💡 请检查:\n1. 每个商品的 amount 是否正确(amount = price × quantity)\n2. 是否有遗漏的商品`;
702
+ break;
703
+ }
704
+
705
+ // 校验2:originalAmount - discountAmount ≈ actualAmount
706
+ const calculatedActual = processedArgs.originalAmount - processedArgs.discountAmount;
707
+ if (Math.abs(calculatedActual - processedArgs.actualAmount) > ERROR_MARGIN) {
708
+ result = {
709
+ success: false,
710
+ error: `优惠计算不正确:原价(${processedArgs.originalAmount}) - 优惠(${processedArgs.discountAmount}) = ${calculatedActual.toFixed(2)},不等于实付金额(${processedArgs.actualAmount})`,
711
+ code: 400
712
+ };
713
+ userMessage = `❌ 保存失败:金额校验不通过\n\n原价:${processedArgs.originalAmount}元\n优惠:${processedArgs.discountAmount}元\n计算实付:${calculatedActual.toFixed(2)}元\n实际实付:${processedArgs.actualAmount}元\n\n💡 请检查优惠金额是否正确`;
714
+ break;
500
715
  }
501
716
 
502
717
  // 1. 调用 parseReceipt 重新提取小票信息(覆盖 Agent 传的数据)