chaimi-keep-mcp 3.1.30 → 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 +61 -0
- package/bin/sync-skill.js +143 -0
- package/package.json +3 -2
- package/server.js +143 -0
package/SKILL.md
CHANGED
|
@@ -178,6 +178,61 @@ Agent 通过 MCP 协议自动获取工具列表,无需手动查询。
|
|
|
178
178
|
|
|
179
179
|
---
|
|
180
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
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
181
236
|
## 确认规则
|
|
182
237
|
|
|
183
238
|
| 风险等级 | 场景 | 策略 |
|
|
@@ -315,4 +370,10 @@ MCP Server: v3.1.23
|
|
|
315
370
|
|
|
316
371
|
---
|
|
317
372
|
|
|
373
|
+
## 相关文档
|
|
374
|
+
|
|
375
|
+
- [MCP优化需求跟踪表](../../../docs/08-项目管理/02-需求跟踪/MCP优化需求跟踪表.md) - 项目需求跟踪
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
318
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.
|
|
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
|
@@ -472,6 +472,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
472
472
|
break;
|
|
473
473
|
}
|
|
474
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
|
+
|
|
475
513
|
const mcpParams = convertParams('save_expense', processedArgs);
|
|
476
514
|
result = await callMcpHub('addExpense', mcpParams, token);
|
|
477
515
|
|
|
@@ -571,6 +609,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
571
609
|
break;
|
|
572
610
|
}
|
|
573
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())) {
|
|
620
|
+
result = {
|
|
621
|
+
success: false,
|
|
622
|
+
error: '日期格式无效',
|
|
623
|
+
code: 400
|
|
624
|
+
};
|
|
625
|
+
userMessage = '❌ 保存失败:日期格式无效,请使用 ISO 8601 格式(如:2026-01-08T11:42:27)';
|
|
626
|
+
break;
|
|
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;
|
|
715
|
+
}
|
|
716
|
+
|
|
574
717
|
// 1. 调用 parseReceipt 重新提取小票信息(覆盖 Agent 传的数据)
|
|
575
718
|
if (processedArgs.rawInput) {
|
|
576
719
|
const parseResult = await callMcpPrompt(
|