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 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(`║ 柴米记账 MCP Server v${CURRENT_VERSION} ║`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-bookkeeping-mcp",
3
- "version": "2.2.6",
3
+ "version": "2.3.0",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
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 CURRENT_VERSION = packageJson.version;
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: '极简快捷记账 - 自动识别支出/收入,智能匹配分类。输入如:"午餐 30 元"、"工资 8000 元"、"木屋烧烤 35 元"',
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('OAuth Token 无效,启动授权流程...');
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
- // 2. 智能匹配分类
415
+ // 3. 智能匹配分类
377
416
  const category = args.category || getCategory(args.name);
378
417
 
379
- // 3. 自动补全参数
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: args.agentType || '',
390
- apiProvider: args.apiProvider || '',
391
- rawInput: args.rawInput || args.name,
428
+ agentType: agentType,
429
+ apiProvider: apiProvider,
430
+ rawInput: rawInput,
431
+ mcp_version: MCP_VERSION,
392
432
  };
393
433
 
394
- // 4. 根据类型选择接口
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
- // 步骤 1:校验数据完整性
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: args.items.map((item, index) => {
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);