chaimi-bookkeeping-mcp 2.2.6 → 2.3.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/README.md +6 -0
- package/bin/cli.js +1 -5
- package/package.json +1 -1
- package/server.js +81 -54
package/README.md
CHANGED
|
@@ -141,6 +141,12 @@ AI 会自动调用 `save_income` 工具记录收入。
|
|
|
141
141
|
|
|
142
142
|
## 更新日志
|
|
143
143
|
|
|
144
|
+
### v2.3.0 (2026-04-08)
|
|
145
|
+
- **修复** `save_receipt` 的 `items` 参数解析 bug,支持 JSON 字符串自动转换
|
|
146
|
+
- **优化** `source` 字段规范:`save_expense`→`mcp_txt_expense`、`save_income`→`mcp_txt_income`、`save_receipt`→`mcp_receipt`
|
|
147
|
+
- **新增** `mcp_version` 字段追踪,便于版本监控
|
|
148
|
+
- **注释** `quick_book` 工具(暂时下线,待优化后重新开放)
|
|
149
|
+
|
|
144
150
|
### v2.2.0 (2026-04-05)
|
|
145
151
|
- **新增收入记账功能** (`save_income`)
|
|
146
152
|
- 支持工资、奖金、红包、投资收益等收入类型
|
package/bin/cli.js
CHANGED
|
@@ -10,10 +10,6 @@ const fs = require('fs');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { spawn } = require('child_process');
|
|
12
12
|
|
|
13
|
-
// 读取 package.json 获取版本
|
|
14
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
15
|
-
const CURRENT_VERSION = packageJson.version;
|
|
16
|
-
|
|
17
13
|
// 获取 server.js 的绝对路径
|
|
18
14
|
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
19
15
|
|
|
@@ -187,7 +183,7 @@ function configureAllAgents() {
|
|
|
187
183
|
function showWelcome() {
|
|
188
184
|
console.log('╔════════════════════════════════════════════════════════╗');
|
|
189
185
|
console.log('║ ║');
|
|
190
|
-
console.log(
|
|
186
|
+
console.log('║ 柴米记账 MCP Server v2.1.0 ║');
|
|
191
187
|
console.log('║ ║');
|
|
192
188
|
console.log('║ 支持: OpenClaw | WorkBuddy | Claude | Cursor ║');
|
|
193
189
|
console.log('║ ║');
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,7 +24,8 @@ const fs = require('fs');
|
|
|
24
24
|
|
|
25
25
|
// 读取 package.json 获取版本
|
|
26
26
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
27
|
-
const
|
|
27
|
+
const MCP_VERSION = packageJson.version;
|
|
28
|
+
console.log('柴米记账 MCP Server 版本:', MCP_VERSION);
|
|
28
29
|
|
|
29
30
|
// 导入 OAuth 模块
|
|
30
31
|
const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
@@ -84,9 +85,10 @@ const server = new Server(
|
|
|
84
85
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
85
86
|
return {
|
|
86
87
|
tools: [
|
|
88
|
+
/*
|
|
87
89
|
{
|
|
88
90
|
name: 'quick_book',
|
|
89
|
-
description: '
|
|
91
|
+
description: '【推荐首选】极简快捷记账 - 自动识别支出/收入,智能匹配分类。仅需 name 和 amount 两个参数,其他自动补全。输入如:"午餐 30 元"、"工资 8000 元"、"木屋烧烤 35 元"',
|
|
90
92
|
inputSchema: {
|
|
91
93
|
type: 'object',
|
|
92
94
|
properties: {
|
|
@@ -95,13 +97,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
95
97
|
category: { type: 'string', description: '分类(可选,系统自动推荐)' },
|
|
96
98
|
store: { type: 'string', description: '商家名称(可选)' },
|
|
97
99
|
note: { type: 'string', description: '备注(可选)' },
|
|
100
|
+
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
101
|
+
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
102
|
+
rawInput: { type: 'string', description: '用户原始输入' },
|
|
103
|
+
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
98
104
|
},
|
|
99
105
|
required: ['name', 'amount'],
|
|
100
106
|
},
|
|
101
107
|
},
|
|
108
|
+
*/
|
|
102
109
|
{
|
|
103
110
|
name: 'save_expense',
|
|
104
|
-
description: '保存单商品消费记录(AI
|
|
111
|
+
description: '保存单商品消费记录(AI文字记账)',
|
|
105
112
|
inputSchema: {
|
|
106
113
|
type: 'object',
|
|
107
114
|
properties: {
|
|
@@ -115,6 +122,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
115
122
|
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
116
123
|
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
117
124
|
rawInput: { type: 'string', description: '用户原始输入' },
|
|
125
|
+
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
118
126
|
},
|
|
119
127
|
required: ['name', 'amount', 'price', 'quantity'],
|
|
120
128
|
},
|
|
@@ -135,7 +143,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
135
143
|
receiptNo: { type: 'string', description: '小票编号' },
|
|
136
144
|
items: {
|
|
137
145
|
type: 'array',
|
|
138
|
-
description: '
|
|
146
|
+
description: '商品列表(必须是数组格式,如:[{"name":"苹果","amount":5.5,"price":5.5,"quantity":1}],不要传JSON字符串)',
|
|
139
147
|
items: {
|
|
140
148
|
type: 'object',
|
|
141
149
|
properties: {
|
|
@@ -152,9 +160,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
152
160
|
required: ['name', 'amount', 'price', 'quantity'],
|
|
153
161
|
},
|
|
154
162
|
},
|
|
155
|
-
agentType: { type: 'string', description: 'Agent类型(如:workbuddy、claude、cursor)' },
|
|
156
|
-
apiProvider: { type: 'string', description: 'AI提供商' },
|
|
163
|
+
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
164
|
+
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
157
165
|
rawInput: { type: 'string', description: '用户原始输入' },
|
|
166
|
+
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
158
167
|
},
|
|
159
168
|
required: ['store', 'items'],
|
|
160
169
|
},
|
|
@@ -203,9 +212,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
203
212
|
category: { type: 'string', description: '收入分类(如:工资、奖金、投资)' },
|
|
204
213
|
store: { type: 'string', description: '付款方(如:公司名称)' },
|
|
205
214
|
note: { type: 'string', description: '备注' },
|
|
206
|
-
agentType: { type: 'string', description: 'Agent类型(如:workbuddy、claude、cursor)' },
|
|
207
|
-
apiProvider: { type: 'string', description: 'AI提供商' },
|
|
215
|
+
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
216
|
+
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
208
217
|
rawInput: { type: 'string', description: '用户原始输入' },
|
|
218
|
+
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
209
219
|
},
|
|
210
220
|
required: ['name', 'amount'],
|
|
211
221
|
},
|
|
@@ -216,6 +226,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
216
226
|
|
|
217
227
|
// 工具名称映射:SCF action -> mcpHub tool
|
|
218
228
|
const toolMapping = {
|
|
229
|
+
'quick_book': 'addExpense', // quick_book 内部会判断支出/收入
|
|
219
230
|
'save_expense': 'addExpense',
|
|
220
231
|
'save_receipt': 'addReceipt',
|
|
221
232
|
'save_income': 'addIncome',
|
|
@@ -238,6 +249,10 @@ async function getToken() {
|
|
|
238
249
|
}
|
|
239
250
|
// 超过2小时,尝试刷新
|
|
240
251
|
console.log('Token 超过2小时,尝试自动刷新...');
|
|
252
|
+
} else if (cachedToken && tokenExpireTime <= Date.now() + 5 * 60 * 1000) {
|
|
253
|
+
// Token 已过期,添加友好提示
|
|
254
|
+
console.log('🔐 Token 已过期,需要重新授权');
|
|
255
|
+
console.log('请使用 mcporter 调用 柴米记账.save_expense() 开始授权');
|
|
241
256
|
}
|
|
242
257
|
|
|
243
258
|
// 使用 OAuth 获取有效 Token
|
|
@@ -250,14 +265,32 @@ async function getToken() {
|
|
|
250
265
|
cachedToken = oauthToken.accessToken;
|
|
251
266
|
tokenExpireTime = oauthToken.expiresAt;
|
|
252
267
|
lastRefreshTime = Date.now();
|
|
268
|
+
// 授权成功,清除授权状态
|
|
269
|
+
await oauthManager.clearAuthState();
|
|
253
270
|
return cachedToken;
|
|
254
271
|
} catch (err) {
|
|
255
|
-
// OAuth Token
|
|
256
|
-
console.log('
|
|
272
|
+
// OAuth Token 无效,检查是否有未完成的授权
|
|
273
|
+
console.log('🔐 Token 已过期,需要重新授权');
|
|
274
|
+
console.log('请使用 mcporter 调用 柴米记账.save_expense() 开始授权');
|
|
275
|
+
console.log('OAuth Token 无效,检查授权状态...');
|
|
276
|
+
|
|
277
|
+
// 检查是否有未完成的授权
|
|
278
|
+
const authState = await oauthManager.loadAuthState();
|
|
279
|
+
if (authState && !oauthManager.isAuthStateExpired(authState)) {
|
|
280
|
+
console.log(`检测到未完成的授权,验证码: ${authState.userCode}`);
|
|
281
|
+
console.log('请在微信柴米记账小程序中输入此验证码完成授权');
|
|
282
|
+
console.log('授权完成后,请重新调用工具');
|
|
283
|
+
throw new Error(`授权进行中,请使用验证码 ${authState.userCode} 完成授权`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 没有未完成的授权,启动新的授权流程
|
|
287
|
+
console.log('启动新的授权流程...');
|
|
257
288
|
const newToken = await oauthManager.startAuthFlow();
|
|
258
289
|
cachedToken = newToken.accessToken;
|
|
259
290
|
tokenExpireTime = newToken.expiresAt;
|
|
260
291
|
lastRefreshTime = Date.now();
|
|
292
|
+
// 授权成功,清除授权状态
|
|
293
|
+
await oauthManager.clearAuthState();
|
|
261
294
|
return cachedToken;
|
|
262
295
|
}
|
|
263
296
|
}
|
|
@@ -366,17 +399,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
366
399
|
|
|
367
400
|
// 根据工具类型选择处理流程
|
|
368
401
|
switch (name) {
|
|
402
|
+
/*
|
|
369
403
|
case 'quick_book': {
|
|
370
404
|
// 极简快捷记账:自动识别支出/收入,智能匹配分类
|
|
371
405
|
console.log('处理极简快捷记账...');
|
|
372
406
|
|
|
373
|
-
// 1.
|
|
407
|
+
// 1. 从环境变量补充参数(确保上报)
|
|
408
|
+
const agentType = process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || args.agentType || '';
|
|
409
|
+
const apiProvider = process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || args.apiProvider || '';
|
|
410
|
+
const rawInput = args.rawInput || args.name;
|
|
411
|
+
|
|
412
|
+
// 2. 判断是支出还是收入
|
|
374
413
|
const isIncome = isIncomeName(args.name);
|
|
375
414
|
|
|
376
|
-
//
|
|
415
|
+
// 3. 智能匹配分类
|
|
377
416
|
const category = args.category || getCategory(args.name);
|
|
378
417
|
|
|
379
|
-
//
|
|
418
|
+
// 4. 自动补全参数
|
|
380
419
|
const completedArgs = {
|
|
381
420
|
name: sanitizeString(args.name, 100),
|
|
382
421
|
amount: validateAmount(args.amount),
|
|
@@ -386,12 +425,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
386
425
|
category: category,
|
|
387
426
|
store: sanitizeString(args.store, 100) || '未知商家',
|
|
388
427
|
note: sanitizeString(args.note, 500),
|
|
389
|
-
agentType:
|
|
390
|
-
apiProvider:
|
|
391
|
-
rawInput:
|
|
428
|
+
agentType: agentType,
|
|
429
|
+
apiProvider: apiProvider,
|
|
430
|
+
rawInput: rawInput,
|
|
431
|
+
mcp_version: MCP_VERSION,
|
|
392
432
|
};
|
|
393
433
|
|
|
394
|
-
//
|
|
434
|
+
// 5. 根据类型选择接口
|
|
395
435
|
if (isIncome) {
|
|
396
436
|
// 收入记账
|
|
397
437
|
console.log('识别为收入,调用 save_income...');
|
|
@@ -413,9 +453,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
413
453
|
}
|
|
414
454
|
break;
|
|
415
455
|
}
|
|
456
|
+
*/
|
|
416
457
|
|
|
417
458
|
case 'save_expense': {
|
|
418
|
-
// 步骤
|
|
459
|
+
// 步骤1:校验数据完整性
|
|
419
460
|
console.log('校验消费数据完整性...');
|
|
420
461
|
const validationResult = await callMcpPrompt(
|
|
421
462
|
'validateResult',
|
|
@@ -652,12 +693,27 @@ function convertParams(toolName, args) {
|
|
|
652
693
|
rawInput: sanitizeString(args.rawInput, 1000),
|
|
653
694
|
agentType: args.agentType || '',
|
|
654
695
|
apiProvider: args.apiProvider || '',
|
|
696
|
+
mcp_version: args.mcp_version || MCP_VERSION,
|
|
697
|
+
source: 'mcp_txt_expense',
|
|
655
698
|
};
|
|
656
699
|
}
|
|
657
700
|
|
|
658
|
-
case 'save_receipt':
|
|
701
|
+
case 'save_receipt': {
|
|
702
|
+
// 处理 items 可能是 JSON 字符串的情况
|
|
703
|
+
let items = args.items;
|
|
704
|
+
if (typeof items === 'string') {
|
|
705
|
+
try {
|
|
706
|
+
items = JSON.parse(items);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
throw new Error('items 参数格式错误:必须是数组或 JSON 数组字符串');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!Array.isArray(items)) {
|
|
712
|
+
throw new Error('items 参数必须是数组');
|
|
713
|
+
}
|
|
714
|
+
|
|
659
715
|
return {
|
|
660
|
-
items:
|
|
716
|
+
items: items.map((item, index) => {
|
|
661
717
|
const weight = sanitizeString(item.weight, 50);
|
|
662
718
|
const amount = validateAmount(item.amount);
|
|
663
719
|
// 优先使用传入的 marketPrice,如果没有则自动计算
|
|
@@ -693,7 +749,10 @@ function convertParams(toolName, args) {
|
|
|
693
749
|
rawInput: sanitizeString(args.rawInput, 2000),
|
|
694
750
|
agentType: args.agentType || '',
|
|
695
751
|
apiProvider: args.apiProvider || '',
|
|
752
|
+
mcp_version: args.mcp_version || MCP_VERSION,
|
|
753
|
+
source: 'mcp_receipt',
|
|
696
754
|
};
|
|
755
|
+
}
|
|
697
756
|
|
|
698
757
|
case 'get_expenses':
|
|
699
758
|
return {
|
|
@@ -719,6 +778,8 @@ function convertParams(toolName, args) {
|
|
|
719
778
|
rawInput: sanitizeString(args.rawInput, 1000),
|
|
720
779
|
agentType: args.agentType || '',
|
|
721
780
|
apiProvider: args.apiProvider || '',
|
|
781
|
+
mcp_version: args.mcp_version || MCP_VERSION,
|
|
782
|
+
source: 'mcp_txt_income',
|
|
722
783
|
};
|
|
723
784
|
|
|
724
785
|
default:
|
|
@@ -726,40 +787,6 @@ function convertParams(toolName, args) {
|
|
|
726
787
|
}
|
|
727
788
|
}
|
|
728
789
|
|
|
729
|
-
// 智能分类:根据商品名称匹配分类
|
|
730
|
-
function getCategory(name) {
|
|
731
|
-
if (!name) return '其他';
|
|
732
|
-
|
|
733
|
-
const categoryMap = {
|
|
734
|
-
'餐饮': ['饭', '菜', '餐', '酒', '饮料', '咖啡', '茶', '早餐', '午餐', '晚餐', '夜宵', '烧烤', '火锅', '快餐', '外卖', '小吃', '奶茶', '面包', '蛋糕'],
|
|
735
|
-
'交通': ['车', '油', '票', '打车', '公交', '地铁', '加油', '停车', '租车', '火车', '飞机', '轮船', '客运'],
|
|
736
|
-
'购物': ['买', '购', '商场', '超市', '衣服', '鞋', '包', '化妆品', '护肤品', '数码', '电器', '家具', '日用品'],
|
|
737
|
-
'娱乐': ['玩', '游戏', '电影', '歌', 'KTV', '酒吧', '旅游', '景点', '门票', '健身', '运动', '书', '音乐'],
|
|
738
|
-
'医疗': ['药', '医', '病', '医院', '诊所', '体检', '疫苗', '牙科', '眼科', '门诊'],
|
|
739
|
-
'教育': ['学', '课', '培训', '考试', '学校', '学费', '教材', '文具', '培训费'],
|
|
740
|
-
'居住': ['房', '租', '物业', '水电', '燃气', '网络', '宽带', '话费', '维修', '装修'],
|
|
741
|
-
'工资': ['工资', '薪水', '奖金', '提成', '分红', '补贴', '津贴', '绩效'],
|
|
742
|
-
'其他收入': ['红包', '转账', '借款', '还款', '退款', '理赔', '中奖', '利息'],
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
for (const [category, keywords] of Object.entries(categoryMap)) {
|
|
746
|
-
if (keywords.some(keyword => name.includes(keyword))) {
|
|
747
|
-
return category;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
return '其他';
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// 判断是否为收入
|
|
755
|
-
function isIncomeName(name) {
|
|
756
|
-
if (!name) return false;
|
|
757
|
-
|
|
758
|
-
const incomeKeywords = ['工资', '薪水', '奖金', '提成', '分红', '补贴', '津贴', '绩效', '红包', '转账', '借款', '还款', '退款', '理赔', '中奖', '利息', '收入', '入账', '到账'];
|
|
759
|
-
|
|
760
|
-
return incomeKeywords.some(keyword => name.includes(keyword));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
790
|
// 获取月份最后一天
|
|
764
791
|
function getMonthEndDate(yearMonth) {
|
|
765
792
|
const [year, month] = yearMonth.split('-').map(Number);
|