chaimi-bookkeeping-mcp 2.3.3 → 2.3.5
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/2.3.4 +0 -0
- package/README.md +10 -16
- package/package.json +1 -1
- package/server.js +85 -688
package/2.3.4
ADDED
|
File without changes
|
package/README.md
CHANGED
|
@@ -70,22 +70,9 @@ MCP Server 会显示:
|
|
|
70
70
|
|
|
71
71
|
## 配置说明
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{
|
|
77
|
-
"mcpServers": {
|
|
78
|
-
"柴米记账": {
|
|
79
|
-
"command": "chaimi-bookkeeping-mcp",
|
|
80
|
-
"env": {
|
|
81
|
-
"MCP_OAUTH_URL": "https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpOAuth",
|
|
82
|
-
"MCP_HUB_URL": "https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp",
|
|
83
|
-
"MCP_PROMPT_URL": "https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpPrompt"
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
73
|
+
npm 全局安装时会**自动配置**,无需手动操作。
|
|
74
|
+
|
|
75
|
+
如需手动配置(如从 GitHub 下载安装),参考 `~/.mcporter/mcporter.json` 格式。
|
|
89
76
|
|
|
90
77
|
## 常见问题
|
|
91
78
|
|
|
@@ -141,6 +128,13 @@ AI 会自动调用 `save_income` 工具记录收入。
|
|
|
141
128
|
|
|
142
129
|
## 更新日志
|
|
143
130
|
|
|
131
|
+
### v2.3.4 (2026-04-08)
|
|
132
|
+
- **简化** `save_expense` 工具,只保留 `name` 和 `amount` 为必填参数
|
|
133
|
+
- **优化** 其他参数使用默认值,简化 Agent 调用
|
|
134
|
+
|
|
135
|
+
### v2.3.3 (2026-04-08)
|
|
136
|
+
- **修正** 版本号统一为 v2.3.3(包含 v2.3.2 的所有修复)
|
|
137
|
+
|
|
144
138
|
### v2.3.2 (2026-04-08)
|
|
145
139
|
- **修复** `server.js` 中 `CURRENT_VERSION` 未定义错误,统一使用 `MCP_VERSION`
|
|
146
140
|
- **修复** 所有工具的 `userMessage` 添加默认值,防止显示 `undefined`
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1,77 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* 柴米记账 MCP Server (Node.js 版本)
|
|
4
|
-
* 适配微信云函数 mcpHub
|
|
5
4
|
* 支持 Claude Desktop、Cursor、WorkBuddy、OpenClaw
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
// 加载 .env 文件
|
|
9
|
-
try {
|
|
10
|
-
require('dotenv').config();
|
|
11
|
-
} catch (e) {
|
|
12
|
-
// dotenv 未安装,忽略
|
|
13
|
-
}
|
|
14
|
-
|
|
15
7
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
16
8
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
17
9
|
const {
|
|
18
10
|
CallToolRequestSchema,
|
|
19
11
|
ListToolsRequestSchema,
|
|
20
12
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const os = require('os');
|
|
23
13
|
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
24
15
|
|
|
25
|
-
// 读取 package.json
|
|
16
|
+
// 读取 package.json 获取版本号
|
|
26
17
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
27
18
|
const MCP_VERSION = packageJson.version;
|
|
28
|
-
console.log('柴米记账 MCP Server 版本:', MCP_VERSION);
|
|
29
|
-
|
|
30
|
-
// 导入 OAuth 模块
|
|
31
|
-
const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
32
|
-
|
|
33
|
-
// 配置 - 微信云函数
|
|
34
|
-
const MCP_HUB_URL = process.env.MCP_HUB_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp';
|
|
35
|
-
const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpPrompt';
|
|
36
|
-
const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpOAuth';
|
|
37
|
-
|
|
38
|
-
// Token 缓存
|
|
39
|
-
let cachedToken = null;
|
|
40
|
-
let tokenExpireTime = 0;
|
|
41
|
-
|
|
42
|
-
// OAuth 管理器实例
|
|
43
|
-
let oauthManager = null;
|
|
44
19
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
const tokenStorage = new FileTokenStorage(
|
|
48
|
-
path.join(os.homedir(), '.mcporter', 'oauth-token.json')
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
oauthManager = new OAuthManager({
|
|
52
|
-
mcpOAuthUrl: MCP_OAUTH_URL,
|
|
53
|
-
tokenStorage: tokenStorage,
|
|
54
|
-
onQrCode: (qrData) => {
|
|
55
|
-
// 显示授权信息
|
|
56
|
-
console.log('\n========================================');
|
|
57
|
-
console.log('🔐 请完成授权');
|
|
58
|
-
console.log('========================================');
|
|
59
|
-
console.log(`验证码: ${qrData.userCode}`);
|
|
60
|
-
console.log(`验证地址: ${qrData.verificationUri}`);
|
|
61
|
-
console.log('========================================\n');
|
|
62
|
-
},
|
|
63
|
-
onTokenReady: (token) => {
|
|
64
|
-
console.log('✅ Token 已就绪,可以开始使用');
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return oauthManager;
|
|
69
|
-
}
|
|
20
|
+
// 配置
|
|
21
|
+
const SCF_URL = process.env.SCF_URL || 'https://1412172089-4wbwsop8pe.ap-shanghai.tencentscf.com';
|
|
70
22
|
|
|
71
23
|
// 创建 MCP Server
|
|
72
24
|
const server = new Server(
|
|
73
25
|
{
|
|
74
|
-
name: '
|
|
26
|
+
name: 'chaihuo-mcp-server',
|
|
75
27
|
version: MCP_VERSION,
|
|
76
28
|
},
|
|
77
29
|
{
|
|
@@ -85,27 +37,6 @@ const server = new Server(
|
|
|
85
37
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
86
38
|
return {
|
|
87
39
|
tools: [
|
|
88
|
-
/*
|
|
89
|
-
{
|
|
90
|
-
name: 'quick_book',
|
|
91
|
-
description: '【推荐首选】极简快捷记账 - 自动识别支出/收入,智能匹配分类。仅需 name 和 amount 两个参数,其他自动补全。输入如:"午餐 30 元"、"工资 8000 元"、"木屋烧烤 35 元"',
|
|
92
|
-
inputSchema: {
|
|
93
|
-
type: 'object',
|
|
94
|
-
properties: {
|
|
95
|
-
name: { type: 'string', description: '商品/服务名称或自然语言描述(必填)' },
|
|
96
|
-
amount: { type: 'number', description: '金额(必填)' },
|
|
97
|
-
category: { type: 'string', description: '分类(可选,系统自动推荐)' },
|
|
98
|
-
store: { type: 'string', description: '商家名称(可选)' },
|
|
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 版本号(自动填充)' },
|
|
104
|
-
},
|
|
105
|
-
required: ['name', 'amount'],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
*/
|
|
109
40
|
{
|
|
110
41
|
name: 'save_expense',
|
|
111
42
|
description: '保存单商品消费记录(AI文字记账)',
|
|
@@ -114,22 +45,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
114
45
|
properties: {
|
|
115
46
|
name: { type: 'string', description: '商品名称' },
|
|
116
47
|
amount: { type: 'number', description: '金额' },
|
|
117
|
-
price: { type: 'number', description: '单价' },
|
|
118
|
-
quantity: { type: 'number', description: '数量' },
|
|
119
|
-
unit: { type: 'string', description: '单位(如:斤、个、瓶)' },
|
|
120
48
|
category: { type: 'string', description: '分类(如:餐饮、食品、交通)' },
|
|
121
49
|
store: { type: 'string', description: '商家名称' },
|
|
122
|
-
agentType: { type: 'string', description: 'Agent
|
|
123
|
-
apiProvider: { type: 'string', description: 'AI
|
|
124
|
-
rawInput: { type: 'string', description: '
|
|
125
|
-
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
50
|
+
agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
51
|
+
apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
|
|
52
|
+
rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
|
|
126
53
|
},
|
|
127
|
-
required: ['name', 'amount'
|
|
54
|
+
required: ['name', 'amount'],
|
|
128
55
|
},
|
|
129
56
|
},
|
|
130
57
|
{
|
|
131
58
|
name: 'save_receipt',
|
|
132
|
-
description: '
|
|
59
|
+
description: '【图片小票专用】保存购物小票/发票/收据(支持单商品或多商品)。当用户提供图片形式的小票、发票、收据、购物清单时,无论商品数量多少(1条或多条),都必须使用此接口。可自动识别商家名称、商品明细、金额、优惠等信息。典型场景:超市购物小票、餐厅发票、线上订单截图、手写收据照片等',
|
|
133
60
|
inputSchema: {
|
|
134
61
|
type: 'object',
|
|
135
62
|
properties: {
|
|
@@ -141,29 +68,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
141
68
|
actualAmount: { type: 'number', description: '实付金额' },
|
|
142
69
|
paymentMethod: { type: 'string', description: '支付方式' },
|
|
143
70
|
receiptNo: { type: 'string', description: '小票编号' },
|
|
71
|
+
agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
72
|
+
apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
|
|
73
|
+
rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
|
|
144
74
|
items: {
|
|
145
75
|
type: 'array',
|
|
146
|
-
description: '
|
|
76
|
+
description: '商品列表',
|
|
147
77
|
items: {
|
|
148
78
|
type: 'object',
|
|
149
79
|
properties: {
|
|
150
80
|
name: { type: 'string', description: '商品名称' },
|
|
151
81
|
originalName: { type: 'string', description: '原始商品名称' },
|
|
152
82
|
price: { type: 'number', description: '单价' },
|
|
153
|
-
quantity: { type: '
|
|
83
|
+
quantity: { type: 'string', description: '数量' },
|
|
154
84
|
unit: { type: 'string', description: '单位' },
|
|
155
85
|
amount: { type: 'number', description: '金额' },
|
|
156
86
|
weight: { type: 'string', description: '重量' },
|
|
157
87
|
marketPrice: { type: 'string', description: '市场单价' },
|
|
158
88
|
category: { type: 'string', description: '分类' },
|
|
159
89
|
},
|
|
160
|
-
required: ['name', 'amount'
|
|
90
|
+
required: ['name', 'amount'],
|
|
161
91
|
},
|
|
162
92
|
},
|
|
163
|
-
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
164
|
-
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
165
|
-
rawInput: { type: 'string', description: '用户原始输入' },
|
|
166
|
-
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
167
93
|
},
|
|
168
94
|
required: ['store', 'items'],
|
|
169
95
|
},
|
|
@@ -176,6 +102,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
176
102
|
properties: {
|
|
177
103
|
limit: { type: 'number', description: '返回数量限制', default: 10 },
|
|
178
104
|
source: { type: 'string', description: '来源筛选' },
|
|
105
|
+
agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
106
|
+
apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
|
|
107
|
+
rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
|
|
179
108
|
},
|
|
180
109
|
},
|
|
181
110
|
},
|
|
@@ -186,6 +115,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
186
115
|
type: 'object',
|
|
187
116
|
properties: {
|
|
188
117
|
limit: { type: 'number', description: '返回数量限制', default: 5 },
|
|
118
|
+
agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
119
|
+
apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
|
|
120
|
+
rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
|
|
189
121
|
},
|
|
190
122
|
},
|
|
191
123
|
},
|
|
@@ -197,417 +129,76 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
197
129
|
properties: {
|
|
198
130
|
yearMonth: { type: 'string', description: '年月(如:2026-03)' },
|
|
199
131
|
type: { type: 'string', description: '统计类型:item/receipt/category' },
|
|
132
|
+
agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
133
|
+
apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
|
|
134
|
+
rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
|
|
200
135
|
},
|
|
201
136
|
required: ['yearMonth'],
|
|
202
137
|
},
|
|
203
138
|
},
|
|
204
|
-
{
|
|
205
|
-
name: 'save_income',
|
|
206
|
-
description: '保存收入记录(工资、奖金、红包等)',
|
|
207
|
-
inputSchema: {
|
|
208
|
-
type: 'object',
|
|
209
|
-
properties: {
|
|
210
|
-
name: { type: 'string', description: '收入来源(如:工资、奖金、红包)' },
|
|
211
|
-
amount: { type: 'number', description: '收入金额' },
|
|
212
|
-
category: { type: 'string', description: '收入分类(如:工资、奖金、投资)' },
|
|
213
|
-
store: { type: 'string', description: '付款方(如:公司名称)' },
|
|
214
|
-
note: { type: 'string', description: '备注' },
|
|
215
|
-
agentType: { type: 'string', description: 'Agent 类型(如:workbuddy、claude、cursor)' },
|
|
216
|
-
apiProvider: { type: 'string', description: 'AI 提供商' },
|
|
217
|
-
rawInput: { type: 'string', description: '用户原始输入' },
|
|
218
|
-
mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
|
|
219
|
-
},
|
|
220
|
-
required: ['name', 'amount'],
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
139
|
],
|
|
224
140
|
};
|
|
225
141
|
});
|
|
226
142
|
|
|
227
|
-
// 工具名称映射:SCF action -> mcpHub tool
|
|
228
|
-
const toolMapping = {
|
|
229
|
-
'quick_book': 'addExpense', // quick_book 内部会判断支出/收入
|
|
230
|
-
'save_expense': 'addExpense',
|
|
231
|
-
'save_receipt': 'addReceipt',
|
|
232
|
-
'save_income': 'addIncome',
|
|
233
|
-
'get_expenses': 'getExpenses',
|
|
234
|
-
'get_receipt_list': 'getExpenses',
|
|
235
|
-
'get_statistics': 'getStatistics',
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// 获取或刷新 Token(OAuth 2.0 Device Flow)
|
|
239
|
-
// 自动刷新周期:2小时(测试用,生产环境建议24小时)
|
|
240
|
-
const TOKEN_REFRESH_INTERVAL = 2 * 60 * 60 * 1000; // 2小时
|
|
241
|
-
let lastRefreshTime = 0;
|
|
242
|
-
|
|
243
|
-
async function getToken() {
|
|
244
|
-
// 检查缓存的 Token 是否有效(提前5分钟过期)
|
|
245
|
-
if (cachedToken && tokenExpireTime > Date.now() + 5 * 60 * 1000) {
|
|
246
|
-
// 检查是否需要主动刷新(超过24小时)
|
|
247
|
-
if (Date.now() - lastRefreshTime < TOKEN_REFRESH_INTERVAL) {
|
|
248
|
-
return cachedToken;
|
|
249
|
-
}
|
|
250
|
-
// 超过2小时,尝试刷新
|
|
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() 开始授权');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// 使用 OAuth 获取有效 Token
|
|
259
|
-
if (!oauthManager) {
|
|
260
|
-
throw new Error('OAuth 管理器未初始化,请检查 MCP_OAUTH_URL 配置');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const oauthToken = await oauthManager.getValidToken();
|
|
265
|
-
cachedToken = oauthToken.accessToken;
|
|
266
|
-
tokenExpireTime = oauthToken.expiresAt;
|
|
267
|
-
lastRefreshTime = Date.now();
|
|
268
|
-
// 授权成功,清除授权状态
|
|
269
|
-
await oauthManager.clearAuthState();
|
|
270
|
-
return cachedToken;
|
|
271
|
-
} catch (err) {
|
|
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('启动新的授权流程...');
|
|
288
|
-
const newToken = await oauthManager.startAuthFlow();
|
|
289
|
-
cachedToken = newToken.accessToken;
|
|
290
|
-
tokenExpireTime = newToken.expiresAt;
|
|
291
|
-
lastRefreshTime = Date.now();
|
|
292
|
-
// 授权成功,清除授权状态
|
|
293
|
-
await oauthManager.clearAuthState();
|
|
294
|
-
return cachedToken;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 调用 mcpPrompt 云函数(获取 Prompt 或校验数据)
|
|
299
|
-
async function callMcpPrompt(tool, params, token) {
|
|
300
|
-
const response = await fetch(MCP_PROMPT_URL, {
|
|
301
|
-
method: 'POST',
|
|
302
|
-
headers: {
|
|
303
|
-
'Content-Type': 'application/json',
|
|
304
|
-
'Authorization': `Bearer ${token}`,
|
|
305
|
-
},
|
|
306
|
-
body: JSON.stringify({
|
|
307
|
-
tool: tool,
|
|
308
|
-
params: params,
|
|
309
|
-
}),
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (!response.ok) {
|
|
313
|
-
const errorText = await response.text();
|
|
314
|
-
throw new Error(`mcpPrompt 调用失败: ${errorText}`);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return await response.json();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 调用 mcpHub 云函数
|
|
321
|
-
async function callMcpHub(tool, params, token) {
|
|
322
|
-
const response = await fetch(MCP_HUB_URL, {
|
|
323
|
-
method: 'POST',
|
|
324
|
-
headers: {
|
|
325
|
-
'Content-Type': 'application/json',
|
|
326
|
-
'Authorization': `Bearer ${token}`,
|
|
327
|
-
},
|
|
328
|
-
body: JSON.stringify({
|
|
329
|
-
tool: tool,
|
|
330
|
-
params: params,
|
|
331
|
-
}),
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
if (!response.ok) {
|
|
335
|
-
const errorText = await response.text();
|
|
336
|
-
throw new Error(`mcpHub 调用失败: ${errorText}`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return await response.json();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
143
|
// 工具调用处理
|
|
343
144
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
344
145
|
const { name, arguments: args } = request.params;
|
|
345
146
|
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// 从环境变量获取 agentType 和 apiProvider
|
|
350
|
-
const agentTypeFromEnv = process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '';
|
|
351
|
-
const apiProviderFromEnv = process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '';
|
|
352
|
-
|
|
353
|
-
// 如果参数中没有,使用环境变量的值
|
|
354
|
-
if (!args.agentType && agentTypeFromEnv) {
|
|
355
|
-
args.agentType = agentTypeFromEnv;
|
|
356
|
-
}
|
|
357
|
-
if (!args.apiProvider && apiProviderFromEnv) {
|
|
358
|
-
args.apiProvider = apiProviderFromEnv;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const logMsg = `
|
|
362
|
-
[${new Date().toISOString()}] MCP 工具被调用
|
|
363
|
-
工具名称: ${name}
|
|
364
|
-
参数: ${JSON.stringify(args, null, 2)}
|
|
365
|
-
环境变量 AGENT_TYPE: ${agentTypeFromEnv || '(未设置)'}
|
|
366
|
-
环境变量 API_PROVIDER: ${apiProviderFromEnv || '(未设置)'}
|
|
367
|
-
`;
|
|
368
|
-
fs.appendFileSync('/tmp/mcp-debug.log', logMsg);
|
|
369
|
-
console.error(logMsg);
|
|
147
|
+
// 调试日志
|
|
148
|
+
console.error('收到工具调用请求:', name);
|
|
149
|
+
console.error('原始参数:', JSON.stringify(args, null, 2));
|
|
370
150
|
|
|
371
151
|
try {
|
|
372
|
-
// 获取 Token
|
|
373
|
-
const token = await
|
|
152
|
+
// 获取 Token(这里需要实现 Token 生成逻辑)
|
|
153
|
+
const token = await generateToken();
|
|
154
|
+
|
|
155
|
+
// SCF 使用下划线命名,直接传递工具名
|
|
156
|
+
const action = name;
|
|
374
157
|
|
|
375
|
-
// 处理参数
|
|
158
|
+
// 处理参数 - 将字符串格式的 items 转换为数组
|
|
376
159
|
const processedArgs = { ...args };
|
|
377
160
|
if (processedArgs.items && typeof processedArgs.items === 'string') {
|
|
378
161
|
try {
|
|
379
162
|
processedArgs.items = JSON.parse(processedArgs.items);
|
|
163
|
+
console.error('items 解析成功:', processedArgs.items);
|
|
380
164
|
} catch (e) {
|
|
381
|
-
//
|
|
165
|
+
// 如果解析失败,保持原样
|
|
166
|
+
console.error('解析 items 失败:', e);
|
|
382
167
|
}
|
|
383
168
|
}
|
|
384
169
|
|
|
170
|
+
console.error('处理后参数:', JSON.stringify(processedArgs, null, 2));
|
|
385
171
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
console.log('处理极简快捷记账...');
|
|
406
|
-
|
|
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. 判断是支出还是收入
|
|
413
|
-
const isIncome = isIncomeName(args.name);
|
|
414
|
-
|
|
415
|
-
// 3. 智能匹配分类
|
|
416
|
-
const category = args.category || getCategory(args.name);
|
|
417
|
-
|
|
418
|
-
// 4. 自动补全参数
|
|
419
|
-
const completedArgs = {
|
|
420
|
-
name: sanitizeString(args.name, 100),
|
|
421
|
-
amount: validateAmount(args.amount),
|
|
422
|
-
price: args.price || validateAmount(args.amount),
|
|
423
|
-
quantity: args.quantity || 1,
|
|
424
|
-
unit: args.unit || '个',
|
|
425
|
-
category: category,
|
|
426
|
-
store: sanitizeString(args.store, 100) || '未知商家',
|
|
427
|
-
note: sanitizeString(args.note, 500),
|
|
428
|
-
agentType: agentType,
|
|
429
|
-
apiProvider: apiProvider,
|
|
430
|
-
rawInput: rawInput,
|
|
431
|
-
mcp_version: MCP_VERSION,
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// 5. 根据类型选择接口
|
|
435
|
-
if (isIncome) {
|
|
436
|
-
// 收入记账
|
|
437
|
-
console.log('识别为收入,调用 save_income...');
|
|
438
|
-
const mcpParams = convertParams('save_income', completedArgs);
|
|
439
|
-
result = await callMcpHub('addIncome', mcpParams, token);
|
|
440
|
-
|
|
441
|
-
if (result.success) {
|
|
442
|
-
userMessage = `✅ 记账成功\n\n| 收入来源 | 金额 | 分类 |\n|------|------|------|\n| ${completedArgs.name} | ${completedArgs.amount}元 | ${completedArgs.category || '其他收入'} |\n\n已保存到你的柴米记账小程序数据库。`;
|
|
443
|
-
}
|
|
444
|
-
} else {
|
|
445
|
-
// 支出记账
|
|
446
|
-
console.log('识别为支出,调用 save_expense...');
|
|
447
|
-
const mcpParams = convertParams('save_expense', completedArgs);
|
|
448
|
-
result = await callMcpHub('addExpense', mcpParams, token);
|
|
449
|
-
|
|
450
|
-
if (result.success) {
|
|
451
|
-
userMessage = `✅ 记账成功\n\n| 商品 | 金额 | 分类 | 商家 |\n|------|------|------|------|\n| ${completedArgs.name} | ${completedArgs.amount}元 | ${completedArgs.category || '其他'} | ${completedArgs.store} |\n\n已保存到你的柴米记账小程序数据库。`;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
*/
|
|
457
|
-
|
|
458
|
-
case 'save_expense': {
|
|
459
|
-
// 步骤1:校验数据完整性
|
|
460
|
-
console.log('校验消费数据完整性...');
|
|
461
|
-
const validationResult = await callMcpPrompt(
|
|
462
|
-
'validateResult',
|
|
463
|
-
{
|
|
464
|
-
data: processedArgs,
|
|
465
|
-
type: 'parseText'
|
|
466
|
-
},
|
|
467
|
-
token
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
if (validationResult.success && !validationResult.data.valid) {
|
|
471
|
-
console.log('数据不完整,尝试补充默认值:', validationResult.data);
|
|
472
|
-
// 调用 fillDefaults 补充缺失字段
|
|
473
|
-
const fillResult = await callMcpPrompt(
|
|
474
|
-
'fillDefaults',
|
|
475
|
-
{
|
|
476
|
-
data: processedArgs,
|
|
477
|
-
type: 'parseText'
|
|
478
|
-
},
|
|
479
|
-
token
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
if (fillResult.success) {
|
|
483
|
-
processedArgs = fillResult.data;
|
|
484
|
-
console.log('补充后的数据:', processedArgs);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 步骤2:转换参数并调用 mcpHub 保存
|
|
489
|
-
const mcpParams = convertParams('save_expense', processedArgs);
|
|
490
|
-
result = await callMcpHub('addExpense', mcpParams, token);
|
|
491
|
-
|
|
492
|
-
// 格式化输出
|
|
493
|
-
if (result.success) {
|
|
494
|
-
const displayName = processedArgs.name || '未知商品';
|
|
495
|
-
const displayAmount = processedArgs.amount || 0;
|
|
496
|
-
const displayCategory = processedArgs.category || '其他';
|
|
497
|
-
const displayStore = processedArgs.store || '-';
|
|
498
|
-
userMessage = `✅ 记账成功\n\n| 商品 | 金额 | 分类 | 商家 |\n|------|------|------|------|\n| ${displayName} | ${displayAmount}元 | ${displayCategory} | ${displayStore} |\n\n已保存到你的柴米记账小程序数据库。`;
|
|
499
|
-
}
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
case 'save_receipt': {
|
|
504
|
-
// 步骤1:校验数据完整性
|
|
505
|
-
console.log('校验小票数据完整性...');
|
|
506
|
-
const validationResult = await callMcpPrompt(
|
|
507
|
-
'validateResult',
|
|
508
|
-
{
|
|
509
|
-
data: processedArgs,
|
|
510
|
-
type: 'parseReceipt'
|
|
511
|
-
},
|
|
512
|
-
token
|
|
513
|
-
);
|
|
514
|
-
|
|
515
|
-
if (validationResult.success && !validationResult.data.valid) {
|
|
516
|
-
console.log('数据不完整,尝试补充默认值:', validationResult.data);
|
|
517
|
-
// 调用 fillDefaults 补充缺失字段
|
|
518
|
-
const fillResult = await callMcpPrompt(
|
|
519
|
-
'fillDefaults',
|
|
520
|
-
{
|
|
521
|
-
data: processedArgs,
|
|
522
|
-
type: 'parseReceipt'
|
|
523
|
-
},
|
|
524
|
-
token
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
if (fillResult.success) {
|
|
528
|
-
processedArgs = fillResult.data;
|
|
529
|
-
console.log('补充后的数据:', processedArgs);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// 步骤2:转换参数并调用 mcpHub 保存
|
|
534
|
-
const mcpParams = convertParams('save_receipt', processedArgs);
|
|
535
|
-
result = await callMcpHub('addReceipt', mcpParams, token);
|
|
536
|
-
|
|
537
|
-
// 格式化输出
|
|
538
|
-
if (result.success) {
|
|
539
|
-
const totalAmount = processedArgs.totalAmount || processedArgs.items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
|
|
540
|
-
const itemsList = processedArgs.items?.map(item => `• ${item.name || '未知商品'} - ${item.amount || 0}元`).join('\n') || '暂无商品明细';
|
|
541
|
-
const storeName = processedArgs.store || '-';
|
|
542
|
-
const category = processedArgs.items?.[0]?.category || '其他';
|
|
543
|
-
userMessage = `✅ 小票记录成功\n\n| 项目 | 内容 |\n|------|------|\n| 商家 | ${storeName} |\n| 金额 | ${totalAmount}元 |\n| 分类 | ${category} |\n\n商品明细:\n${itemsList}\n\n已保存到你的柴米记账小程序数据库。`;
|
|
544
|
-
}
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
case 'get_expenses':
|
|
549
|
-
case 'get_receipt_list': {
|
|
550
|
-
// 查询类工具直接调用 mcpHub
|
|
551
|
-
const toolName = toolMapping[name];
|
|
552
|
-
const mcpParams = convertParams(name, processedArgs);
|
|
553
|
-
result = await callMcpHub(toolName, mcpParams, token);
|
|
554
|
-
|
|
555
|
-
if (result.success) {
|
|
556
|
-
userMessage = `📊 消费记录查询成功\n共找到 ${result.data?.length || 0} 条记录`;
|
|
557
|
-
}
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
case 'get_statistics': {
|
|
562
|
-
// 查询类工具直接调用 mcpHub
|
|
563
|
-
const toolName = toolMapping[name];
|
|
564
|
-
const mcpParams = convertParams(name, processedArgs);
|
|
565
|
-
result = await callMcpHub(toolName, mcpParams, token);
|
|
566
|
-
|
|
567
|
-
if (result.success) {
|
|
568
|
-
userMessage = `📈 统计查询成功`;
|
|
569
|
-
}
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
case 'save_income': {
|
|
574
|
-
// 收入记录直接调用 mcpHub
|
|
575
|
-
const mcpParams = convertParams('save_income', processedArgs);
|
|
576
|
-
result = await callMcpHub('addIncome', mcpParams, token);
|
|
577
|
-
|
|
578
|
-
// 格式化输出
|
|
579
|
-
if (result.success) {
|
|
580
|
-
const displayName = processedArgs.name || '未知来源';
|
|
581
|
-
const displayAmount = processedArgs.amount || 0;
|
|
582
|
-
const displayCategory = processedArgs.category || '其他';
|
|
583
|
-
const displayStore = processedArgs.store || '-';
|
|
584
|
-
userMessage = `✅ 收入记录成功\n\n| 来源 | 金额 | 分类 | 付款方 |\n|------|------|------|--------|\n| ${displayName} | ${displayAmount}元 | ${displayCategory} | ${displayStore} |`;
|
|
585
|
-
}
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
default:
|
|
590
|
-
throw new Error(`未知工具: ${name}`);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// 处理错误
|
|
594
|
-
if (!result.success) {
|
|
595
|
-
userMessage = `❌ 操作失败:${result.error || '未知错误'}`;
|
|
172
|
+
// 调用 SCF
|
|
173
|
+
const response = await fetch(SCF_URL, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
'Authorization': `Bearer ${token}`,
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
action: action,
|
|
181
|
+
data: processedArgs,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await response.json();
|
|
186
|
+
|
|
187
|
+
// 检查是否传递了agentType和apiProvider参数,如果没有则添加提示
|
|
188
|
+
let responseText = JSON.stringify(result, null, 2);
|
|
189
|
+
if (!processedArgs.agentType || !processedArgs.apiProvider) {
|
|
190
|
+
responseText += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
|
|
596
191
|
}
|
|
597
192
|
|
|
598
|
-
// 添加版本信息提示
|
|
599
|
-
userMessage += `\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}`;
|
|
600
|
-
|
|
601
193
|
return {
|
|
602
194
|
content: [
|
|
603
195
|
{
|
|
604
196
|
type: 'text',
|
|
605
|
-
text:
|
|
197
|
+
text: responseText,
|
|
606
198
|
},
|
|
607
199
|
],
|
|
608
200
|
};
|
|
609
201
|
} catch (error) {
|
|
610
|
-
console.error('工具调用错误:', error);
|
|
611
202
|
return {
|
|
612
203
|
content: [
|
|
613
204
|
{
|
|
@@ -620,238 +211,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
620
211
|
}
|
|
621
212
|
});
|
|
622
213
|
|
|
623
|
-
//
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return str.replace(/<[^>]*>/g, '').substring(0, maxLength);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function validateAmount(amount) {
|
|
631
|
-
const num = parseFloat(amount);
|
|
632
|
-
return isNaN(num) || num < 0 ? 0 : num;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function validateQuantity(quantity) {
|
|
636
|
-
const num = parseFloat(quantity);
|
|
637
|
-
return isNaN(num) || num <= 0 ? 1 : num;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// 从重量字符串中提取克数
|
|
641
|
-
function extractWeightInGrams(weightStr) {
|
|
642
|
-
if (!weightStr || typeof weightStr !== 'string') return 0;
|
|
214
|
+
// 生成 JWT Token
|
|
215
|
+
async function generateToken() {
|
|
216
|
+
const JWT_SECRET = process.env.JWT_SECRET;
|
|
217
|
+
const MCP_OPENID = process.env.MCP_OPENID;
|
|
643
218
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (!match) return 0;
|
|
647
|
-
|
|
648
|
-
const value = parseFloat(match[1]);
|
|
649
|
-
const unit = match[2].toLowerCase();
|
|
650
|
-
|
|
651
|
-
// 转换为克
|
|
652
|
-
switch (unit) {
|
|
653
|
-
case 'g':
|
|
654
|
-
case '克':
|
|
655
|
-
return value;
|
|
656
|
-
case 'kg':
|
|
657
|
-
case '千克':
|
|
658
|
-
return value * 1000;
|
|
659
|
-
case '斤':
|
|
660
|
-
return value * 500; // 1斤 = 500g
|
|
661
|
-
default:
|
|
662
|
-
return value;
|
|
219
|
+
if (!JWT_SECRET) {
|
|
220
|
+
throw new Error('JWT_SECRET 环境变量未设置');
|
|
663
221
|
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// 计算市场单价(元/500g)
|
|
667
|
-
function calculateMarketPrice(amount, weightStr) {
|
|
668
|
-
const amountNum = parseFloat(amount);
|
|
669
|
-
const weightInGrams = extractWeightInGrams(weightStr);
|
|
670
222
|
|
|
671
|
-
if (
|
|
672
|
-
|
|
223
|
+
if (!MCP_OPENID) {
|
|
224
|
+
throw new Error('MCP_OPENID 环境变量未设置,请从小程序获取用户Token');
|
|
673
225
|
}
|
|
674
226
|
|
|
675
|
-
//
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
originalName: sanitizeString(args.originalName, 200) || sanitizeString(args.name, 100),
|
|
694
|
-
store: sanitizeString(args.store, 100),
|
|
695
|
-
type: 'expense',
|
|
696
|
-
unit: sanitizeString(args.unit, 20),
|
|
697
|
-
weight: weight,
|
|
698
|
-
marketPrice: marketPrice,
|
|
699
|
-
price: validateAmount(args.price),
|
|
700
|
-
quantity: validateQuantity(args.quantity),
|
|
701
|
-
date: args.date,
|
|
702
|
-
note: sanitizeString(args.note, 500),
|
|
703
|
-
rawInput: sanitizeString(args.rawInput, 1000),
|
|
704
|
-
agentType: args.agentType || '',
|
|
705
|
-
apiProvider: args.apiProvider || '',
|
|
706
|
-
mcp_version: args.mcp_version || MCP_VERSION,
|
|
707
|
-
source: 'mcp_txt_expense',
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
case 'save_receipt': {
|
|
712
|
-
// 处理 items 可能是 JSON 字符串的情况
|
|
713
|
-
let items = args.items;
|
|
714
|
-
if (typeof items === 'string') {
|
|
715
|
-
try {
|
|
716
|
-
items = JSON.parse(items);
|
|
717
|
-
} catch (e) {
|
|
718
|
-
throw new Error('items 参数格式错误:必须是数组或 JSON 数组字符串');
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (!Array.isArray(items)) {
|
|
722
|
-
throw new Error('items 参数必须是数组');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
return {
|
|
726
|
-
items: items.map((item, index) => {
|
|
727
|
-
const weight = sanitizeString(item.weight, 50);
|
|
728
|
-
const amount = validateAmount(item.amount);
|
|
729
|
-
// 优先使用传入的 marketPrice,如果没有则自动计算
|
|
730
|
-
const marketPrice = sanitizeString(item.marketPrice, 50) || calculateMarketPrice(amount, weight);
|
|
731
|
-
|
|
732
|
-
return {
|
|
733
|
-
itemIndex: index,
|
|
734
|
-
name: sanitizeString(item.name, 100),
|
|
735
|
-
originalName: sanitizeString(item.originalName, 200) || sanitizeString(item.name, 100),
|
|
736
|
-
amount: amount,
|
|
737
|
-
category: sanitizeString(item.category, 50) || '其他',
|
|
738
|
-
subCategory: sanitizeString(item.subCategory, 50) || '',
|
|
739
|
-
transactionType: item.transactionType || 'expense',
|
|
740
|
-
price: validateAmount(item.price),
|
|
741
|
-
quantity: item.quantity ? String(item.quantity).substring(0, 20) : '1',
|
|
742
|
-
unit: sanitizeString(item.unit, 20),
|
|
743
|
-
weight: weight,
|
|
744
|
-
marketPrice: marketPrice,
|
|
745
|
-
note: sanitizeString(item.note, 500),
|
|
746
|
-
};
|
|
747
|
-
}),
|
|
748
|
-
store: sanitizeString(args.store, 100),
|
|
749
|
-
receiptNo: sanitizeString(args.receiptNo, 50),
|
|
750
|
-
totalAmount: validateAmount(args.totalAmount),
|
|
751
|
-
originalAmount: validateAmount(args.originalAmount),
|
|
752
|
-
discountAmount: validateAmount(args.discountAmount),
|
|
753
|
-
actualAmount: validateAmount(args.actualAmount),
|
|
754
|
-
paymentMethod: sanitizeString(args.paymentMethod, 50),
|
|
755
|
-
memberCardNo: sanitizeString(args.memberCardNo, 50),
|
|
756
|
-
currentPoints: parseInt(args.currentPoints) || 0,
|
|
757
|
-
totalPoints: parseInt(args.totalPoints) || 0,
|
|
758
|
-
date: args.date,
|
|
759
|
-
rawInput: sanitizeString(args.rawInput, 2000),
|
|
760
|
-
agentType: args.agentType || '',
|
|
761
|
-
apiProvider: args.apiProvider || '',
|
|
762
|
-
mcp_version: args.mcp_version || MCP_VERSION,
|
|
763
|
-
source: 'mcp_receipt',
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
case 'get_expenses':
|
|
768
|
-
return {
|
|
769
|
-
limit: Math.min(parseInt(args.limit) || 10, 100),
|
|
770
|
-
source: args.source,
|
|
771
|
-
};
|
|
772
|
-
|
|
773
|
-
case 'get_statistics':
|
|
774
|
-
return {
|
|
775
|
-
startDate: args.yearMonth ? `${args.yearMonth}-01` : undefined,
|
|
776
|
-
endDate: args.yearMonth ? getMonthEndDate(args.yearMonth) : undefined,
|
|
777
|
-
type: args.type,
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
case 'save_income':
|
|
781
|
-
return {
|
|
782
|
-
name: sanitizeString(args.name, 100),
|
|
783
|
-
amount: validateAmount(args.amount),
|
|
784
|
-
category: sanitizeString(args.category, 50) || '其他',
|
|
785
|
-
store: sanitizeString(args.store, 100),
|
|
786
|
-
note: sanitizeString(args.note, 500),
|
|
787
|
-
transactionType: 'income',
|
|
788
|
-
rawInput: sanitizeString(args.rawInput, 1000),
|
|
789
|
-
agentType: args.agentType || '',
|
|
790
|
-
apiProvider: args.apiProvider || '',
|
|
791
|
-
mcp_version: args.mcp_version || MCP_VERSION,
|
|
792
|
-
source: 'mcp_txt_income',
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
default:
|
|
796
|
-
return args;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// 获取月份最后一天
|
|
801
|
-
function getMonthEndDate(yearMonth) {
|
|
802
|
-
const [year, month] = yearMonth.split('-').map(Number);
|
|
803
|
-
const lastDay = new Date(year, month, 0).getDate();
|
|
804
|
-
return `${yearMonth}-${lastDay}`;
|
|
227
|
+
// 使用 crypto 生成 JWT
|
|
228
|
+
const crypto = require('crypto');
|
|
229
|
+
|
|
230
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
231
|
+
const payload = {
|
|
232
|
+
openid: MCP_OPENID, // 使用固定的用户 openid
|
|
233
|
+
iat: Math.floor(Date.now() / 1000),
|
|
234
|
+
exp: Math.floor(Date.now() / 1000) + 86400
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const h = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
238
|
+
const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
239
|
+
const sig = crypto
|
|
240
|
+
.createHmac('sha256', JWT_SECRET)
|
|
241
|
+
.update(h + '.' + p)
|
|
242
|
+
.digest('base64url');
|
|
243
|
+
|
|
244
|
+
return `${h}.${p}.${sig}`;
|
|
805
245
|
}
|
|
806
246
|
|
|
807
247
|
// 启动服务器
|
|
808
248
|
async function main() {
|
|
809
|
-
console.error('========================================');
|
|
810
|
-
console.error('柴米记账 MCP Server 启动中...');
|
|
811
|
-
console.error('========================================\n');
|
|
812
|
-
|
|
813
|
-
// 初始化 OAuth 管理器
|
|
814
|
-
console.error('🔄 初始化 OAuth 2.0 认证...');
|
|
815
|
-
initOAuthManager();
|
|
816
|
-
|
|
817
|
-
// 尝试加载已有 Token 或启动授权流程
|
|
818
|
-
try {
|
|
819
|
-
const existingToken = await oauthManager.tokenStorage.load();
|
|
820
|
-
if (existingToken) {
|
|
821
|
-
console.error('✅ 发现已存储的 OAuth Token');
|
|
822
|
-
// 验证 Token 是否有效
|
|
823
|
-
const validToken = await oauthManager.getValidToken();
|
|
824
|
-
console.error('✅ OAuth Token 验证通过');
|
|
825
|
-
cachedToken = validToken.accessToken;
|
|
826
|
-
tokenExpireTime = validToken.expiresAt;
|
|
827
|
-
} else {
|
|
828
|
-
console.error('\n⚠️ 首次使用,需要完成 OAuth 授权');
|
|
829
|
-
console.error('请按以下步骤操作:');
|
|
830
|
-
console.error('1. 等待授权信息出现');
|
|
831
|
-
console.error('2. 在小程序中完成授权');
|
|
832
|
-
console.error('3. 授权成功后即可使用\n');
|
|
833
|
-
|
|
834
|
-
// 启动授权流程(这会阻塞,直到授权完成)
|
|
835
|
-
const newToken = await oauthManager.startAuthFlow();
|
|
836
|
-
cachedToken = newToken.accessToken;
|
|
837
|
-
tokenExpireTime = newToken.expiresAt;
|
|
838
|
-
}
|
|
839
|
-
} catch (err) {
|
|
840
|
-
console.error('\n❌ OAuth 初始化失败:', err.message);
|
|
841
|
-
console.error('请检查 MCP_OAUTH_URL 配置是否正确\n');
|
|
842
|
-
throw err;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
console.error('\n========================================');
|
|
846
|
-
console.error('启动 MCP Server...');
|
|
847
|
-
console.error('========================================\n');
|
|
848
|
-
|
|
849
249
|
const transport = new StdioServerTransport();
|
|
850
250
|
await server.connect(transport);
|
|
851
|
-
console.error('
|
|
852
|
-
console.error(`MCP_HUB_URL: ${MCP_HUB_URL}`);
|
|
853
|
-
console.error(`MCP_PROMPT_URL: ${MCP_PROMPT_URL}`);
|
|
854
|
-
console.error(`MCP_OAUTH_URL: ${MCP_OAUTH_URL}`);
|
|
251
|
+
console.error('柴米记账 MCP Server 已启动');
|
|
855
252
|
}
|
|
856
253
|
|
|
857
254
|
main().catch(console.error);
|