chaimi-bookkeeping-mcp 2.2.0 → 2.2.2
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 +66 -10
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
|
},
|
|
@@ -182,6 +188,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
182
188
|
category: { type: 'string', description: '收入分类(如:工资、奖金、投资)' },
|
|
183
189
|
store: { type: 'string', description: '付款方(如:公司名称)' },
|
|
184
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: '用户原始输入' },
|
|
185
194
|
},
|
|
186
195
|
required: ['name', 'amount'],
|
|
187
196
|
},
|
|
@@ -214,6 +223,10 @@ async function getToken() {
|
|
|
214
223
|
}
|
|
215
224
|
// 超过2小时,尝试刷新
|
|
216
225
|
console.log('Token 超过2小时,尝试自动刷新...');
|
|
226
|
+
} else if (cachedToken && tokenExpireTime <= Date.now() + 5 * 60 * 1000) {
|
|
227
|
+
// Token 已过期,添加友好提示
|
|
228
|
+
console.log('🔐 Token 已过期,需要重新授权');
|
|
229
|
+
console.log('请使用 mcporter 调用 柴米记账.save_expense() 开始授权');
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
// 使用 OAuth 获取有效 Token
|
|
@@ -226,14 +239,32 @@ async function getToken() {
|
|
|
226
239
|
cachedToken = oauthToken.accessToken;
|
|
227
240
|
tokenExpireTime = oauthToken.expiresAt;
|
|
228
241
|
lastRefreshTime = Date.now();
|
|
242
|
+
// 授权成功,清除授权状态
|
|
243
|
+
await oauthManager.clearAuthState();
|
|
229
244
|
return cachedToken;
|
|
230
245
|
} catch (err) {
|
|
231
|
-
// OAuth Token
|
|
232
|
-
console.log('
|
|
246
|
+
// OAuth Token 无效,检查是否有未完成的授权
|
|
247
|
+
console.log('🔐 Token 已过期,需要重新授权');
|
|
248
|
+
console.log('请使用 mcporter 调用 柴米记账.save_expense() 开始授权');
|
|
249
|
+
console.log('OAuth Token 无效,检查授权状态...');
|
|
250
|
+
|
|
251
|
+
// 检查是否有未完成的授权
|
|
252
|
+
const authState = await oauthManager.loadAuthState();
|
|
253
|
+
if (authState && !oauthManager.isAuthStateExpired(authState)) {
|
|
254
|
+
console.log(`检测到未完成的授权,验证码: ${authState.userCode}`);
|
|
255
|
+
console.log('请在微信柴米记账小程序中输入此验证码完成授权');
|
|
256
|
+
console.log('授权完成后,请重新调用工具');
|
|
257
|
+
throw new Error(`授权进行中,请使用验证码 ${authState.userCode} 完成授权`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 没有未完成的授权,启动新的授权流程
|
|
261
|
+
console.log('启动新的授权流程...');
|
|
233
262
|
const newToken = await oauthManager.startAuthFlow();
|
|
234
263
|
cachedToken = newToken.accessToken;
|
|
235
264
|
tokenExpireTime = newToken.expiresAt;
|
|
236
265
|
lastRefreshTime = Date.now();
|
|
266
|
+
// 授权成功,清除授权状态
|
|
267
|
+
await oauthManager.clearAuthState();
|
|
237
268
|
return cachedToken;
|
|
238
269
|
}
|
|
239
270
|
}
|
|
@@ -286,6 +317,31 @@ async function callMcpHub(tool, params, token) {
|
|
|
286
317
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
287
318
|
const { name, arguments: args } = request.params;
|
|
288
319
|
|
|
320
|
+
// 【强制日志】测试日志是否输出 - 写入文件
|
|
321
|
+
const fs = require('fs');
|
|
322
|
+
|
|
323
|
+
// 从环境变量获取 agentType 和 apiProvider
|
|
324
|
+
const agentTypeFromEnv = process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '';
|
|
325
|
+
const apiProviderFromEnv = process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '';
|
|
326
|
+
|
|
327
|
+
// 如果参数中没有,使用环境变量的值
|
|
328
|
+
if (!args.agentType && agentTypeFromEnv) {
|
|
329
|
+
args.agentType = agentTypeFromEnv;
|
|
330
|
+
}
|
|
331
|
+
if (!args.apiProvider && apiProviderFromEnv) {
|
|
332
|
+
args.apiProvider = apiProviderFromEnv;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const logMsg = `
|
|
336
|
+
[${new Date().toISOString()}] MCP 工具被调用
|
|
337
|
+
工具名称: ${name}
|
|
338
|
+
参数: ${JSON.stringify(args, null, 2)}
|
|
339
|
+
环境变量 AGENT_TYPE: ${agentTypeFromEnv || '(未设置)'}
|
|
340
|
+
环境变量 API_PROVIDER: ${apiProviderFromEnv || '(未设置)'}
|
|
341
|
+
`;
|
|
342
|
+
fs.appendFileSync('/tmp/mcp-debug.log', logMsg);
|
|
343
|
+
console.error(logMsg);
|
|
344
|
+
|
|
289
345
|
try {
|
|
290
346
|
// 获取 Token
|
|
291
347
|
const token = await getToken();
|
|
@@ -306,14 +362,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
306
362
|
let userMessage;
|
|
307
363
|
|
|
308
364
|
// 【调试日志】打印原始传入参数
|
|
309
|
-
console.
|
|
310
|
-
console.
|
|
311
|
-
console.
|
|
312
|
-
console.
|
|
313
|
-
console.
|
|
314
|
-
console.
|
|
315
|
-
console.
|
|
316
|
-
console.
|
|
365
|
+
console.error('\n========== MCP 参数调试日志 ==========');
|
|
366
|
+
console.error('工具名称:', name);
|
|
367
|
+
console.error('原始参数 agentType:', args.agentType || '(未传入)');
|
|
368
|
+
console.error('原始参数 apiProvider:', args.apiProvider || '(未传入)');
|
|
369
|
+
console.error('原始参数 rawInput:', args.rawInput ? args.rawInput.substring(0, 50) + '...' : '(未传入)');
|
|
370
|
+
console.error('原始参数 llm_provider:', args.llm_provider || '(未传入)');
|
|
371
|
+
console.error('原始参数 store:', args.store || '(未传入)');
|
|
372
|
+
console.error('======================================\n');
|
|
317
373
|
|
|
318
374
|
// 根据工具类型选择处理流程
|
|
319
375
|
switch (name) {
|