chaimi-bookkeeping-mcp 2.1.9 → 2.2.1
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 +33 -0
- package/oauth.js +89 -4
- package/package.json +1 -1
- package/server.js +102 -11
package/README.md
CHANGED
|
@@ -111,8 +111,41 @@ A: `npm update -g chaimi-bookkeeping-mcp`
|
|
|
111
111
|
- 邮箱:songyangx@gmail.com
|
|
112
112
|
- npm 包地址:https://www.npmjs.com/package/chaimi-bookkeeping-mcp
|
|
113
113
|
|
|
114
|
+
## 功能说明
|
|
115
|
+
|
|
116
|
+
### 支持的记账类型
|
|
117
|
+
|
|
118
|
+
| 工具 | 用途 | 示例 |
|
|
119
|
+
|------|------|------|
|
|
120
|
+
| `save_expense` | 单商品支出 | "午餐 35 元" |
|
|
121
|
+
| `save_receipt` | 小票/多商品 | "超市购物:牛奶 10 元,面包 15 元" |
|
|
122
|
+
| `save_income` | **收入记录** | "工资到账 8000 元" |
|
|
123
|
+
| `get_expenses` | 查询记录 | "查看本月消费" |
|
|
124
|
+
| `get_statistics` | 消费统计 | "本月花了多少" |
|
|
125
|
+
|
|
126
|
+
### 收入记账示例
|
|
127
|
+
|
|
128
|
+
在 WorkBuddy/OpenClaw 中输入:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
记录工资收入 8000 元
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
或:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
今天收到奖金 3000
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
AI 会自动调用 `save_income` 工具记录收入。
|
|
141
|
+
|
|
114
142
|
## 更新日志
|
|
115
143
|
|
|
144
|
+
### v2.2.0 (2026-04-05)
|
|
145
|
+
- **新增收入记账功能** (`save_income`)
|
|
146
|
+
- 支持工资、奖金、红包、投资收益等收入类型
|
|
147
|
+
- 支出和收入统一存储,便于统计分析
|
|
148
|
+
|
|
116
149
|
### v2.1.0 (2026-04-03)
|
|
117
150
|
- 升级 OAuth 2.0 Device Flow 认证
|
|
118
151
|
- 支持 npm 全局安装
|
package/oauth.js
CHANGED
|
@@ -69,10 +69,24 @@ class OAuthManager {
|
|
|
69
69
|
|
|
70
70
|
console.log('✅ 获取设备码成功');
|
|
71
71
|
console.log('');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
|
|
73
|
+
// 【柴米记账授权】格式化输出,确保Agent能识别
|
|
74
|
+
const authMessage = `
|
|
75
|
+
【柴米记账授权】
|
|
76
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
|
+
📱 验证码: ${deviceCodeRes.userCode}
|
|
78
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
请在微信柴米记账小程序中输入此验证码完成授权
|
|
80
|
+
`;
|
|
81
|
+
console.log(authMessage);
|
|
82
|
+
|
|
83
|
+
// 保存授权状态,支持进程重启后复用
|
|
84
|
+
await this.saveAuthState({
|
|
85
|
+
deviceCode: deviceCodeRes.deviceCode,
|
|
86
|
+
userCode: deviceCodeRes.userCode,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
status: 'pending'
|
|
89
|
+
});
|
|
76
90
|
|
|
77
91
|
// 2. 根据环境选择交互方式
|
|
78
92
|
if (useUrlScheme && deviceCodeRes.urlScheme) {
|
|
@@ -423,6 +437,77 @@ class OAuthManager {
|
|
|
423
437
|
delay(ms) {
|
|
424
438
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
425
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* 保存授权状态到文件
|
|
443
|
+
* 支持进程重启后复用验证码
|
|
444
|
+
*/
|
|
445
|
+
async saveAuthState(state) {
|
|
446
|
+
try {
|
|
447
|
+
const fs = require('fs').promises;
|
|
448
|
+
const path = require('path');
|
|
449
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
450
|
+
|
|
451
|
+
// 确保目录存在
|
|
452
|
+
const dir = path.dirname(stateFile);
|
|
453
|
+
await fs.mkdir(dir, { recursive: true });
|
|
454
|
+
|
|
455
|
+
// 保存状态
|
|
456
|
+
await fs.writeFile(
|
|
457
|
+
stateFile,
|
|
458
|
+
JSON.stringify(state, null, 2),
|
|
459
|
+
{ mode: 0o600 }
|
|
460
|
+
);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
// 保存失败不影响主流程
|
|
463
|
+
console.warn('保存授权状态失败:', err.message);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* 加载授权状态
|
|
469
|
+
* 用于进程重启后恢复授权流程
|
|
470
|
+
*/
|
|
471
|
+
async loadAuthState() {
|
|
472
|
+
try {
|
|
473
|
+
const fs = require('fs').promises;
|
|
474
|
+
const path = require('path');
|
|
475
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
476
|
+
|
|
477
|
+
const data = await fs.readFile(stateFile, 'utf8');
|
|
478
|
+
return JSON.parse(data);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
if (err.code === 'ENOENT') {
|
|
481
|
+
return null; // 文件不存在
|
|
482
|
+
}
|
|
483
|
+
console.warn('加载授权状态失败:', err.message);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 清除授权状态
|
|
490
|
+
* 授权成功或过期后清理
|
|
491
|
+
*/
|
|
492
|
+
async clearAuthState() {
|
|
493
|
+
try {
|
|
494
|
+
const fs = require('fs').promises;
|
|
495
|
+
const path = require('path');
|
|
496
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
497
|
+
await fs.unlink(stateFile);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
// 文件不存在或其他错误,忽略
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 检查授权状态是否过期
|
|
505
|
+
* 默认10分钟有效期
|
|
506
|
+
*/
|
|
507
|
+
isAuthStateExpired(state, maxAge = 10 * 60 * 1000) {
|
|
508
|
+
if (!state || !state.timestamp) return true;
|
|
509
|
+
return Date.now() - state.timestamp > maxAge;
|
|
510
|
+
}
|
|
426
511
|
}
|
|
427
512
|
|
|
428
513
|
/**
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -97,6 +97,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
97
97
|
unit: { type: 'string', description: '单位(如:斤、个、瓶)' },
|
|
98
98
|
category: { type: 'string', description: '分类(如:餐饮、食品、交通)' },
|
|
99
99
|
store: { type: 'string', description: '商家名称' },
|
|
100
|
+
agentType: { type: 'string', description: 'Agent类型(如:workbuddy、claude、cursor)' },
|
|
101
|
+
apiProvider: { type: 'string', description: 'AI提供商' },
|
|
102
|
+
rawInput: { type: 'string', description: '用户原始输入' },
|
|
100
103
|
},
|
|
101
104
|
required: ['name', 'amount', 'price', 'quantity'],
|
|
102
105
|
},
|
|
@@ -134,6 +137,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
134
137
|
required: ['name', 'amount', 'price', 'quantity'],
|
|
135
138
|
},
|
|
136
139
|
},
|
|
140
|
+
agentType: { type: 'string', description: 'Agent类型(如:workbuddy、claude、cursor)' },
|
|
141
|
+
apiProvider: { type: 'string', description: 'AI提供商' },
|
|
142
|
+
rawInput: { type: 'string', description: '用户原始输入' },
|
|
137
143
|
},
|
|
138
144
|
required: ['store', 'items'],
|
|
139
145
|
},
|
|
@@ -171,6 +177,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
171
177
|
required: ['yearMonth'],
|
|
172
178
|
},
|
|
173
179
|
},
|
|
180
|
+
{
|
|
181
|
+
name: 'save_income',
|
|
182
|
+
description: '保存收入记录(工资、奖金、红包等)',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
name: { type: 'string', description: '收入来源(如:工资、奖金、红包)' },
|
|
187
|
+
amount: { type: 'number', description: '收入金额' },
|
|
188
|
+
category: { type: 'string', description: '收入分类(如:工资、奖金、投资)' },
|
|
189
|
+
store: { type: 'string', description: '付款方(如:公司名称)' },
|
|
190
|
+
note: { type: 'string', description: '备注' },
|
|
191
|
+
agentType: { type: 'string', description: 'Agent类型(如:workbuddy、claude、cursor)' },
|
|
192
|
+
apiProvider: { type: 'string', description: 'AI提供商' },
|
|
193
|
+
rawInput: { type: 'string', description: '用户原始输入' },
|
|
194
|
+
},
|
|
195
|
+
required: ['name', 'amount'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
174
198
|
],
|
|
175
199
|
};
|
|
176
200
|
});
|
|
@@ -179,6 +203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
179
203
|
const toolMapping = {
|
|
180
204
|
'save_expense': 'addExpense',
|
|
181
205
|
'save_receipt': 'addReceipt',
|
|
206
|
+
'save_income': 'addIncome',
|
|
182
207
|
'get_expenses': 'getExpenses',
|
|
183
208
|
'get_receipt_list': 'getExpenses',
|
|
184
209
|
'get_statistics': 'getStatistics',
|
|
@@ -210,14 +235,30 @@ async function getToken() {
|
|
|
210
235
|
cachedToken = oauthToken.accessToken;
|
|
211
236
|
tokenExpireTime = oauthToken.expiresAt;
|
|
212
237
|
lastRefreshTime = Date.now();
|
|
238
|
+
// 授权成功,清除授权状态
|
|
239
|
+
await oauthManager.clearAuthState();
|
|
213
240
|
return cachedToken;
|
|
214
241
|
} catch (err) {
|
|
215
|
-
// OAuth Token
|
|
216
|
-
console.log('OAuth Token
|
|
242
|
+
// OAuth Token 无效,检查是否有未完成的授权
|
|
243
|
+
console.log('OAuth Token 无效,检查授权状态...');
|
|
244
|
+
|
|
245
|
+
// 检查是否有未完成的授权
|
|
246
|
+
const authState = await oauthManager.loadAuthState();
|
|
247
|
+
if (authState && !oauthManager.isAuthStateExpired(authState)) {
|
|
248
|
+
console.log(`检测到未完成的授权,验证码: ${authState.userCode}`);
|
|
249
|
+
console.log('请在微信柴米记账小程序中输入此验证码完成授权');
|
|
250
|
+
console.log('授权完成后,请重新调用工具');
|
|
251
|
+
throw new Error(`授权进行中,请使用验证码 ${authState.userCode} 完成授权`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 没有未完成的授权,启动新的授权流程
|
|
255
|
+
console.log('启动新的授权流程...');
|
|
217
256
|
const newToken = await oauthManager.startAuthFlow();
|
|
218
257
|
cachedToken = newToken.accessToken;
|
|
219
258
|
tokenExpireTime = newToken.expiresAt;
|
|
220
259
|
lastRefreshTime = Date.now();
|
|
260
|
+
// 授权成功,清除授权状态
|
|
261
|
+
await oauthManager.clearAuthState();
|
|
221
262
|
return cachedToken;
|
|
222
263
|
}
|
|
223
264
|
}
|
|
@@ -270,6 +311,31 @@ async function callMcpHub(tool, params, token) {
|
|
|
270
311
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
271
312
|
const { name, arguments: args } = request.params;
|
|
272
313
|
|
|
314
|
+
// 【强制日志】测试日志是否输出 - 写入文件
|
|
315
|
+
const fs = require('fs');
|
|
316
|
+
|
|
317
|
+
// 从环境变量获取 agentType 和 apiProvider
|
|
318
|
+
const agentTypeFromEnv = process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '';
|
|
319
|
+
const apiProviderFromEnv = process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '';
|
|
320
|
+
|
|
321
|
+
// 如果参数中没有,使用环境变量的值
|
|
322
|
+
if (!args.agentType && agentTypeFromEnv) {
|
|
323
|
+
args.agentType = agentTypeFromEnv;
|
|
324
|
+
}
|
|
325
|
+
if (!args.apiProvider && apiProviderFromEnv) {
|
|
326
|
+
args.apiProvider = apiProviderFromEnv;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const logMsg = `
|
|
330
|
+
[${new Date().toISOString()}] MCP 工具被调用
|
|
331
|
+
工具名称: ${name}
|
|
332
|
+
参数: ${JSON.stringify(args, null, 2)}
|
|
333
|
+
环境变量 AGENT_TYPE: ${agentTypeFromEnv || '(未设置)'}
|
|
334
|
+
环境变量 API_PROVIDER: ${apiProviderFromEnv || '(未设置)'}
|
|
335
|
+
`;
|
|
336
|
+
fs.appendFileSync('/tmp/mcp-debug.log', logMsg);
|
|
337
|
+
console.error(logMsg);
|
|
338
|
+
|
|
273
339
|
try {
|
|
274
340
|
// 获取 Token
|
|
275
341
|
const token = await getToken();
|
|
@@ -290,14 +356,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
290
356
|
let userMessage;
|
|
291
357
|
|
|
292
358
|
// 【调试日志】打印原始传入参数
|
|
293
|
-
console.
|
|
294
|
-
console.
|
|
295
|
-
console.
|
|
296
|
-
console.
|
|
297
|
-
console.
|
|
298
|
-
console.
|
|
299
|
-
console.
|
|
300
|
-
console.
|
|
359
|
+
console.error('\n========== MCP 参数调试日志 ==========');
|
|
360
|
+
console.error('工具名称:', name);
|
|
361
|
+
console.error('原始参数 agentType:', args.agentType || '(未传入)');
|
|
362
|
+
console.error('原始参数 apiProvider:', args.apiProvider || '(未传入)');
|
|
363
|
+
console.error('原始参数 rawInput:', args.rawInput ? args.rawInput.substring(0, 50) + '...' : '(未传入)');
|
|
364
|
+
console.error('原始参数 llm_provider:', args.llm_provider || '(未传入)');
|
|
365
|
+
console.error('原始参数 store:', args.store || '(未传入)');
|
|
366
|
+
console.error('======================================\n');
|
|
301
367
|
|
|
302
368
|
// 根据工具类型选择处理流程
|
|
303
369
|
switch (name) {
|
|
@@ -403,13 +469,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
403
469
|
const toolName = toolMapping[name];
|
|
404
470
|
const mcpParams = convertParams(name, processedArgs);
|
|
405
471
|
result = await callMcpHub(toolName, mcpParams, token);
|
|
406
|
-
|
|
472
|
+
|
|
407
473
|
if (result.success) {
|
|
408
474
|
userMessage = `📈 统计查询成功`;
|
|
409
475
|
}
|
|
410
476
|
break;
|
|
411
477
|
}
|
|
412
478
|
|
|
479
|
+
case 'save_income': {
|
|
480
|
+
// 收入记录直接调用 mcpHub
|
|
481
|
+
const mcpParams = convertParams('save_income', processedArgs);
|
|
482
|
+
result = await callMcpHub('addIncome', mcpParams, token);
|
|
483
|
+
|
|
484
|
+
// 格式化输出
|
|
485
|
+
if (result.success) {
|
|
486
|
+
userMessage = `✅ 收入记录成功\n\n| 来源 | 金额 | 分类 | 付款方 |\n|------|------|------|--------|\n| ${processedArgs.name} | ${processedArgs.amount}元 | ${processedArgs.category || '其他'} | ${processedArgs.store || '-'} |`;
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
|
|
413
491
|
default:
|
|
414
492
|
throw new Error(`未知工具: ${name}`);
|
|
415
493
|
}
|
|
@@ -583,6 +661,19 @@ function convertParams(toolName, args) {
|
|
|
583
661
|
type: args.type,
|
|
584
662
|
};
|
|
585
663
|
|
|
664
|
+
case 'save_income':
|
|
665
|
+
return {
|
|
666
|
+
name: sanitizeString(args.name, 100),
|
|
667
|
+
amount: validateAmount(args.amount),
|
|
668
|
+
category: sanitizeString(args.category, 50) || '其他',
|
|
669
|
+
store: sanitizeString(args.store, 100),
|
|
670
|
+
note: sanitizeString(args.note, 500),
|
|
671
|
+
transactionType: 'income',
|
|
672
|
+
rawInput: sanitizeString(args.rawInput, 1000),
|
|
673
|
+
agentType: args.agentType || '',
|
|
674
|
+
apiProvider: args.apiProvider || '',
|
|
675
|
+
};
|
|
676
|
+
|
|
586
677
|
default:
|
|
587
678
|
return args;
|
|
588
679
|
}
|