chaimi-keep-mcp 3.1.45-beta.7 → 3.1.46-beta.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 +8 -0
- package/package.json +1 -2
- package/server.js +238 -551
- package/utils/validators.js +0 -49
package/README.md
CHANGED
|
@@ -141,6 +141,14 @@ AI 会自动调用 `save_income` 工具记录收入。
|
|
|
141
141
|
|
|
142
142
|
## 更新日志
|
|
143
143
|
|
|
144
|
+
### v3.1.46-beta.0 (2026-04-22)
|
|
145
|
+
- **修复** 授权流程阻塞问题:恢复到 v3.1.37 之前的非阻塞模式
|
|
146
|
+
- 移除 `startAuthFlowAndWait()` 和 `waitForAuthWithPolling()` 阻塞函数
|
|
147
|
+
- 恢复 `startAuthFlow()` + `pollForAuthInBackground()` 非阻塞模式
|
|
148
|
+
- 立即返回验证码给 Agent,配合 `lifecycle: keep-alive` 后台轮询
|
|
149
|
+
- 解决 mcporter 超时和验证码不显示问题
|
|
150
|
+
- **基于** npm 3.1.44 版本,保留其他所有功能和修复
|
|
151
|
+
|
|
144
152
|
### v3.1.44 (2026-04-21) 🎉 正式版
|
|
145
153
|
- **正式发布** 包含所有 beta 版本修复
|
|
146
154
|
- MCP 调用日志字段缺失修复
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chaimi-keep-mcp",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.46-beta.1",
|
|
4
4
|
"description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
-
"utils/",
|
|
12
11
|
"server.js",
|
|
13
12
|
"oauth.js",
|
|
14
13
|
"SKILL.md",
|
package/server.js
CHANGED
|
@@ -5,105 +5,11 @@
|
|
|
5
5
|
* 支持 Claude Desktop、Cursor、WorkBuddy、OpenClaw
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
async function fetchConfigFromCloud() {
|
|
14
|
-
const configUrl = 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp';
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
console.error('🔧 首次启动,正在从云端获取配置...');
|
|
18
|
-
|
|
19
|
-
const https = require('https');
|
|
20
|
-
const response = await new Promise((resolve, reject) => {
|
|
21
|
-
const req = https.request(configUrl, {
|
|
22
|
-
method: 'POST',
|
|
23
|
-
headers: {
|
|
24
|
-
'Content-Type': 'application/json'
|
|
25
|
-
}
|
|
26
|
-
}, (res) => {
|
|
27
|
-
let data = '';
|
|
28
|
-
res.on('data', chunk => data += chunk);
|
|
29
|
-
res.on('end', () => {
|
|
30
|
-
try {
|
|
31
|
-
resolve(JSON.parse(data));
|
|
32
|
-
} catch (e) {
|
|
33
|
-
reject(new Error('配置解析失败'));
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
req.on('error', reject);
|
|
39
|
-
req.write(JSON.stringify({ action: 'getMcpConfig' }));
|
|
40
|
-
req.end();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (!response.success || !response.data) {
|
|
44
|
-
throw new Error('配置获取失败');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return response.data;
|
|
48
|
-
} catch (err) {
|
|
49
|
-
// 抛出错误让调用者处理,不要直接退出
|
|
50
|
-
throw new Error(`配置获取失败: ${err.message}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function initializeConfig() {
|
|
55
|
-
const fs = require('fs');
|
|
56
|
-
const path = require('path');
|
|
57
|
-
const envPath = path.join(__dirname, envFile);
|
|
58
|
-
|
|
59
|
-
// 如果配置文件已存在,直接返回
|
|
60
|
-
if (fs.existsSync(envPath)) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 首次启动,尝试从云端获取配置
|
|
65
|
-
let config;
|
|
66
|
-
try {
|
|
67
|
-
config = await fetchConfigFromCloud();
|
|
68
|
-
console.error('✅ 已从云端获取配置');
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error('❌ 无法从云端获取配置');
|
|
71
|
-
console.error(' 错误:' + err.message);
|
|
72
|
-
console.error('');
|
|
73
|
-
console.error('可能原因:');
|
|
74
|
-
console.error(' 1. 网络连接异常');
|
|
75
|
-
console.error(' 2. 云函数尚未部署或配置错误');
|
|
76
|
-
console.error('');
|
|
77
|
-
console.error('解决方案:');
|
|
78
|
-
console.error(' 1. 检查网络连接');
|
|
79
|
-
console.error(' 2. 确认云函数 mcpHub 已部署');
|
|
80
|
-
console.error(' 3. 联系开发者获取帮助');
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 生成配置文件
|
|
85
|
-
const envContent = `# 柴米记账 MCP Server - ${NODE_ENV} 环境配置
|
|
86
|
-
# 自动生成时间:${new Date().toISOString()}
|
|
87
|
-
|
|
88
|
-
# 云函数 URL
|
|
89
|
-
MCP_HUB_URL=${config.MCP_HUB_URL}
|
|
90
|
-
MCP_PROMPT_URL=${config.MCP_PROMPT_URL}
|
|
91
|
-
MCP_OAUTH_URL=${config.MCP_OAUTH_URL}
|
|
92
|
-
|
|
93
|
-
# API 签名密钥
|
|
94
|
-
CHAIMI_API_SECRET=${config.CHAIMI_API_SECRET}
|
|
95
|
-
|
|
96
|
-
# JWT 密钥
|
|
97
|
-
JWT_SECRET=${config.JWT_SECRET}
|
|
98
|
-
`;
|
|
99
|
-
|
|
100
|
-
fs.writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
101
|
-
console.error('✅ 配置文件已自动生成!');
|
|
102
|
-
console.error(` 文件位置:${envPath}`);
|
|
103
|
-
console.error('');
|
|
104
|
-
|
|
105
|
-
// 重新加载环境变量
|
|
106
|
-
require('dotenv').config({ path: envPath });
|
|
8
|
+
// 加载 .env 文件
|
|
9
|
+
try {
|
|
10
|
+
require('dotenv').config();
|
|
11
|
+
} catch (e) {
|
|
12
|
+
// dotenv 未安装,忽略
|
|
107
13
|
}
|
|
108
14
|
|
|
109
15
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
@@ -124,90 +30,37 @@ const MCP_VERSION = packageJson.version;
|
|
|
124
30
|
// 导入 OAuth 模块
|
|
125
31
|
const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
126
32
|
|
|
127
|
-
// 导入工具函数
|
|
128
|
-
const { validateDate } = require('./utils/validators.js');
|
|
129
|
-
|
|
130
|
-
// ==================== 密钥管理 ====================
|
|
131
|
-
|
|
132
|
-
// Token 加密密钥文件路径
|
|
133
|
-
const SECRET_KEY_FILE = path.join(__dirname, '.mcp-secret-key');
|
|
134
|
-
|
|
135
|
-
// 自动生成或加载 Token 加密密钥
|
|
136
|
-
function getOrCreateSecretKey() {
|
|
137
|
-
// 1. 优先使用环境变量(高级用户可以手动配置)
|
|
138
|
-
if (process.env.MCP_SECRET_KEY) {
|
|
139
|
-
console.error('✅ 使用环境变量中的 Token 加密密钥');
|
|
140
|
-
return process.env.MCP_SECRET_KEY;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 2. 尝试从文件加载
|
|
144
|
-
if (fs.existsSync(SECRET_KEY_FILE)) {
|
|
145
|
-
const key = fs.readFileSync(SECRET_KEY_FILE, 'utf8').trim();
|
|
146
|
-
console.error('✅ 从本地文件加载 Token 加密密钥');
|
|
147
|
-
return key;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 3. 自动生成新密钥(64 字符,256 位)
|
|
151
|
-
const newKey = crypto.randomBytes(32).toString('hex');
|
|
152
|
-
fs.writeFileSync(SECRET_KEY_FILE, newKey, { mode: 0o600 }); // 只有当前用户可读写
|
|
153
|
-
console.error('✅ 已自动生成 Token 加密密钥,保存在 .mcp-secret-key 文件中');
|
|
154
|
-
console.error('⚠️ 请勿删除此文件,否则已授权的 Token 将无法解密');
|
|
155
|
-
return newKey;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Token 加密密钥(自动生成或加载)
|
|
159
|
-
const SECRET_KEY = getOrCreateSecretKey();
|
|
160
|
-
|
|
161
33
|
// API 签名密钥(用于验证请求来自合法 MCP Server)
|
|
162
|
-
|
|
163
|
-
let CHAIMI_API_SECRET;
|
|
164
|
-
|
|
165
|
-
// ==================== 加密/解密函数 ====================
|
|
166
|
-
|
|
167
|
-
// AES-256-GCM 加密(用于 Token)
|
|
168
|
-
function encrypt(text, secretKey) {
|
|
169
|
-
const iv = crypto.randomBytes(16); // 随机初始化向量
|
|
170
|
-
const cipher = crypto.createCipheriv(
|
|
171
|
-
'aes-256-gcm',
|
|
172
|
-
Buffer.from(secretKey, 'hex'),
|
|
173
|
-
iv
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
177
|
-
encrypted += cipher.final('hex');
|
|
178
|
-
|
|
179
|
-
const authTag = cipher.getAuthTag(); // 认证标签(防篡改)
|
|
180
|
-
|
|
181
|
-
// 返回格式:iv:authTag:encrypted
|
|
182
|
-
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// AES-256-GCM 解密(用于 Token)
|
|
186
|
-
function decrypt(encryptedText, secretKey) {
|
|
187
|
-
const [ivHex, authTagHex, encrypted] = encryptedText.split(':');
|
|
188
|
-
|
|
189
|
-
const decipher = crypto.createDecipheriv(
|
|
190
|
-
'aes-256-gcm',
|
|
191
|
-
Buffer.from(secretKey, 'hex'),
|
|
192
|
-
Buffer.from(ivHex, 'hex')
|
|
193
|
-
);
|
|
34
|
+
const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
|
|
194
35
|
|
|
195
|
-
|
|
36
|
+
// URL 加密密钥
|
|
37
|
+
const URL_ENCRYPT_KEY = 'chaimi-url-key-2024';
|
|
196
38
|
|
|
197
|
-
|
|
198
|
-
|
|
39
|
+
// 加密的云函数 URL
|
|
40
|
+
const ENCRYPTED_URLS = {
|
|
41
|
+
hub: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571320140b40044e05',
|
|
42
|
+
oauth: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571327201c1901',
|
|
43
|
+
prompt: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f5713381306001959'
|
|
44
|
+
};
|
|
199
45
|
|
|
200
|
-
|
|
46
|
+
// 解密 URL 函数
|
|
47
|
+
function decryptUrl(encrypted, key) {
|
|
48
|
+
const hex = encrypted.replace('enc:v1:', '');
|
|
49
|
+
let result = '';
|
|
50
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
51
|
+
const byte = parseInt(hex.substr(i, 2), 16);
|
|
52
|
+
result += String.fromCharCode(byte ^ key.charCodeAt((i / 2) % key.length));
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
201
55
|
}
|
|
202
56
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
let MCP_OAUTH_URL;
|
|
57
|
+
// 配置 - 微信云函数(运行时解密)
|
|
58
|
+
const MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(ENCRYPTED_URLS.hub, URL_ENCRYPT_KEY);
|
|
59
|
+
const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(ENCRYPTED_URLS.prompt, URL_ENCRYPT_KEY);
|
|
60
|
+
const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(ENCRYPTED_URLS.oauth, URL_ENCRYPT_KEY);
|
|
208
61
|
|
|
209
|
-
// Token
|
|
210
|
-
let
|
|
62
|
+
// Token 缓存
|
|
63
|
+
let cachedToken = null;
|
|
211
64
|
let tokenExpireTime = 0;
|
|
212
65
|
|
|
213
66
|
// OAuth 管理器实例
|
|
@@ -255,8 +108,8 @@ async function initOAuthManager() {
|
|
|
255
108
|
// 检查 Token 是否过期(预留5分钟缓冲)
|
|
256
109
|
const expiresAt = new Date(existingToken.expiresAt).getTime();
|
|
257
110
|
if (expiresAt > Date.now() + 5 * 60 * 1000) {
|
|
258
|
-
// Token
|
|
259
|
-
|
|
111
|
+
// Token 有效,设置授权状态
|
|
112
|
+
cachedToken = existingToken.accessToken;
|
|
260
113
|
tokenExpireTime = expiresAt;
|
|
261
114
|
authState.isAuthorized = true;
|
|
262
115
|
console.error('✅ 已从文件加载有效 Token,无需重新授权');
|
|
@@ -518,103 +371,60 @@ const toolMapping = {
|
|
|
518
371
|
'export_data': 'exportData',
|
|
519
372
|
};
|
|
520
373
|
|
|
521
|
-
// ==================== 请求频率限制 ====================
|
|
522
|
-
|
|
523
|
-
// 限流配置
|
|
524
|
-
const RATE_LIMITS = {
|
|
525
|
-
// 记账工具:1 分钟 10 次
|
|
526
|
-
write: {
|
|
527
|
-
tools: ['save_expense', 'save_receipt', 'save_income'],
|
|
528
|
-
maxRequests: 10,
|
|
529
|
-
windowMs: 60 * 1000, // 1 分钟
|
|
530
|
-
},
|
|
531
|
-
// 查询工具:1 天 10 次
|
|
532
|
-
read: {
|
|
533
|
-
tools: ['get_expenses', 'get_receipt_list', 'get_statistics', 'get_insights', 'export_data'],
|
|
534
|
-
maxRequests: 10,
|
|
535
|
-
windowMs: 24 * 60 * 60 * 1000, // 1 天
|
|
536
|
-
},
|
|
537
|
-
// 配置读取工具:1 分钟 10 次
|
|
538
|
-
config: {
|
|
539
|
-
tools: ['get_skill', 'get_parse_prompt', 'get_text_parse_prompt'],
|
|
540
|
-
maxRequests: 10,
|
|
541
|
-
windowMs: 60 * 1000, // 1 分钟
|
|
542
|
-
},
|
|
543
|
-
// 反馈提交:1 天 5 次
|
|
544
|
-
feedback: {
|
|
545
|
-
tools: ['submit_feedback'],
|
|
546
|
-
maxRequests: 5,
|
|
547
|
-
windowMs: 24 * 60 * 60 * 1000, // 1 天
|
|
548
|
-
},
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
// 限流记录:{ toolName: [timestamp1, timestamp2, ...] }
|
|
552
|
-
const rateLimitRecords = {};
|
|
553
|
-
|
|
554
|
-
// 检查是否超过频率限制
|
|
555
|
-
function checkRateLimit(toolName) {
|
|
556
|
-
// 查找工具所属的限流组
|
|
557
|
-
let limitConfig = null;
|
|
558
|
-
let limitType = null;
|
|
559
|
-
|
|
560
|
-
for (const [type, config] of Object.entries(RATE_LIMITS)) {
|
|
561
|
-
if (config.tools.includes(toolName)) {
|
|
562
|
-
limitConfig = config;
|
|
563
|
-
limitType = type;
|
|
564
|
-
break;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// 如果工具不在限流列表中,直接通过
|
|
569
|
-
if (!limitConfig) {
|
|
570
|
-
return { allowed: true };
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const now = Date.now();
|
|
574
|
-
const windowStart = now - limitConfig.windowMs;
|
|
575
|
-
|
|
576
|
-
// 初始化记录
|
|
577
|
-
if (!rateLimitRecords[toolName]) {
|
|
578
|
-
rateLimitRecords[toolName] = [];
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// 清理过期记录
|
|
582
|
-
rateLimitRecords[toolName] = rateLimitRecords[toolName].filter(
|
|
583
|
-
timestamp => timestamp > windowStart
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
// 检查是否超过限制
|
|
587
|
-
if (rateLimitRecords[toolName].length >= limitConfig.maxRequests) {
|
|
588
|
-
const oldestRequest = rateLimitRecords[toolName][0];
|
|
589
|
-
const resetTime = oldestRequest + limitConfig.windowMs;
|
|
590
|
-
const waitSeconds = Math.ceil((resetTime - now) / 1000);
|
|
591
|
-
|
|
592
|
-
return {
|
|
593
|
-
allowed: false,
|
|
594
|
-
limitType,
|
|
595
|
-
maxRequests: limitConfig.maxRequests,
|
|
596
|
-
windowMs: limitConfig.windowMs,
|
|
597
|
-
resetTime,
|
|
598
|
-
waitSeconds,
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// 记录本次请求
|
|
603
|
-
rateLimitRecords[toolName].push(now);
|
|
604
|
-
|
|
605
|
-
return { allowed: true };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
374
|
// 获取或刷新 Token(OAuth 2.0 Device Flow)
|
|
609
375
|
const TOKEN_REFRESH_INTERVAL = 2 * 60 * 60 * 1000;
|
|
610
376
|
let lastRefreshTime = 0;
|
|
611
377
|
|
|
612
378
|
async function getToken() {
|
|
613
|
-
//
|
|
614
|
-
if (
|
|
379
|
+
// 检查是否已授权
|
|
380
|
+
if (!authState.isAuthorized) {
|
|
381
|
+
// 先检查是否有保存的授权状态(从文件加载)
|
|
382
|
+
const savedAuthState = await oauthManager.loadAuthState();
|
|
383
|
+
if (savedAuthState && savedAuthState.deviceCode) {
|
|
384
|
+
// 检查验证码是否已过期
|
|
385
|
+
if (oauthManager.isAuthStateExpired(savedAuthState)) {
|
|
386
|
+
console.error('⏰ 保存的验证码已过期,清除并生成新的验证码');
|
|
387
|
+
await oauthManager.clearAuthState();
|
|
388
|
+
// 继续执行,生成新的验证码
|
|
389
|
+
} else {
|
|
390
|
+
// 验证码未过期,尝试直接获取token(用户可能已授权)
|
|
391
|
+
try {
|
|
392
|
+
const token = await oauthManager.pollForTokenOnce(savedAuthState.deviceCode);
|
|
393
|
+
if (token) {
|
|
394
|
+
// 授权成功
|
|
395
|
+
cachedToken = token.accessToken;
|
|
396
|
+
tokenExpireTime = token.expiresAt;
|
|
397
|
+
authState.isAuthorized = true;
|
|
398
|
+
await oauthManager.tokenStorage.save(token);
|
|
399
|
+
await oauthManager.clearAuthState();
|
|
400
|
+
console.error('✅ 授权成功!可以继续使用记账功能');
|
|
401
|
+
// 返回特殊标记,让上层知道这是刚完成授权的情况
|
|
402
|
+
throw new Error(`AUTH_JUST_COMPLETED:${cachedToken}`);
|
|
403
|
+
}
|
|
404
|
+
// 用户还未授权,返回同一个验证码
|
|
405
|
+
authState.deviceCode = savedAuthState.deviceCode;
|
|
406
|
+
authState.userCode = savedAuthState.userCode;
|
|
407
|
+
throw new Error(`NEED_AUTH:${savedAuthState.userCode}`);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (err.message.startsWith('NEED_AUTH:') || err.message.startsWith('AUTH_JUST_COMPLETED:')) {
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
// 其他错误,继续获取新的验证码
|
|
413
|
+
console.error('⚠️ 检查授权状态失败:', err.message);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 没有保存的授权状态或已过期,启动新的授权流程
|
|
419
|
+
await startAuthFlow();
|
|
420
|
+
|
|
421
|
+
// 返回验证码给Agent
|
|
422
|
+
throw new Error(`NEED_AUTH:${authState.userCode || '等待生成验证码'}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (cachedToken && tokenExpireTime > Date.now() + 5 * 60 * 1000) {
|
|
615
426
|
if (Date.now() - lastRefreshTime < TOKEN_REFRESH_INTERVAL) {
|
|
616
|
-
|
|
617
|
-
return decrypt(cachedEncryptedToken, SECRET_KEY);
|
|
427
|
+
return cachedToken;
|
|
618
428
|
}
|
|
619
429
|
}
|
|
620
430
|
|
|
@@ -622,78 +432,18 @@ async function getToken() {
|
|
|
622
432
|
throw new Error('OAuth 管理器未初始化,请检查 MCP_OAUTH_URL 配置');
|
|
623
433
|
}
|
|
624
434
|
|
|
625
|
-
// 步骤2:尝试从文件加载并刷新 Token
|
|
626
435
|
try {
|
|
627
436
|
const oauthToken = await oauthManager.getValidToken();
|
|
628
|
-
|
|
629
|
-
cachedEncryptedToken = encrypt(oauthToken.accessToken, SECRET_KEY);
|
|
437
|
+
cachedToken = oauthToken.accessToken;
|
|
630
438
|
tokenExpireTime = oauthToken.expiresAt;
|
|
631
439
|
lastRefreshTime = Date.now();
|
|
632
|
-
authState.isAuthorized = true;
|
|
633
440
|
await oauthManager.clearAuthState();
|
|
634
|
-
// 解密后返回
|
|
635
|
-
return decrypt(cachedEncryptedToken, SECRET_KEY);
|
|
636
|
-
} catch (err) {
|
|
637
|
-
// Token 无效,需要重新授权
|
|
638
|
-
console.error('⏰ Token 无效,需要重新授权:', err.message);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// 步骤3:检查是否有保存的授权状态(用户可能正在授权中)
|
|
642
|
-
const savedAuthState = await oauthManager.loadAuthState();
|
|
643
|
-
if (savedAuthState && savedAuthState.deviceCode) {
|
|
644
|
-
// 检查验证码是否已过期
|
|
645
|
-
if (oauthManager.isAuthStateExpired(savedAuthState)) {
|
|
646
|
-
console.error('⏰ 保存的验证码已过期,清除并生成新的验证码');
|
|
647
|
-
await oauthManager.clearAuthState();
|
|
648
|
-
} else {
|
|
649
|
-
// 验证码未过期,尝试直接获取token(用户可能已授权)
|
|
650
|
-
try {
|
|
651
|
-
const token = await oauthManager.pollForTokenOnce(savedAuthState.deviceCode);
|
|
652
|
-
if (token) {
|
|
653
|
-
// 授权成功
|
|
654
|
-
cachedToken = token.accessToken;
|
|
655
|
-
tokenExpireTime = token.expiresAt;
|
|
656
|
-
authState.isAuthorized = true;
|
|
657
|
-
await oauthManager.tokenStorage.save(token);
|
|
658
|
-
await oauthManager.clearAuthState();
|
|
659
|
-
console.error('✅ 授权成功!可以继续使用记账功能');
|
|
660
|
-
// 返回特殊标记,让上层知道这是刚完成授权的情况
|
|
661
|
-
throw new Error(`AUTH_JUST_COMPLETED:${cachedToken}`);
|
|
662
|
-
}
|
|
663
|
-
// 用户还未授权,启动轮询等待
|
|
664
|
-
console.error(`⏳ 等待用户授权,验证码:${savedAuthState.userCode}`);
|
|
665
|
-
const authResult = await waitForAuthWithPolling(savedAuthState.deviceCode, savedAuthState.userCode, 120000);
|
|
666
|
-
if (authResult.success) {
|
|
667
|
-
cachedToken = authResult.token.accessToken;
|
|
668
|
-
tokenExpireTime = authResult.token.expiresAt;
|
|
669
|
-
authState.isAuthorized = true;
|
|
670
|
-
await oauthManager.tokenStorage.save(authResult.token);
|
|
671
|
-
await oauthManager.clearAuthState();
|
|
672
|
-
return cachedToken;
|
|
673
|
-
} else {
|
|
674
|
-
throw new Error(`NEED_AUTH:${savedAuthState.userCode}`);
|
|
675
|
-
}
|
|
676
|
-
} catch (err) {
|
|
677
|
-
if (err.message.startsWith('NEED_AUTH:') || err.message.startsWith('AUTH_JUST_COMPLETED:')) {
|
|
678
|
-
throw err;
|
|
679
|
-
}
|
|
680
|
-
console.error('⚠️ 检查授权状态失败:', err.message);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 步骤4:启动新的授权流程并等待
|
|
686
|
-
console.error('🔐 启动新的授权流程...');
|
|
687
|
-
const authResult = await startAuthFlowAndWait(120000);
|
|
688
|
-
|
|
689
|
-
if (authResult.success) {
|
|
690
|
-
cachedToken = authResult.token.accessToken;
|
|
691
|
-
tokenExpireTime = authResult.token.expiresAt;
|
|
692
|
-
authState.isAuthorized = true;
|
|
693
441
|
return cachedToken;
|
|
694
|
-
}
|
|
695
|
-
//
|
|
696
|
-
|
|
442
|
+
} catch (err) {
|
|
443
|
+
// Token 获取失败,可能需要重新授权
|
|
444
|
+
authState.isAuthorized = false;
|
|
445
|
+
authState.isWaiting = false;
|
|
446
|
+
throw new Error(`NEED_AUTH:授权已过期,请重新授权`);
|
|
697
447
|
}
|
|
698
448
|
}
|
|
699
449
|
|
|
@@ -903,7 +653,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
903
653
|
const startTime = Date.now();
|
|
904
654
|
const traceId = generateTraceId();
|
|
905
655
|
const osInfo = getOSInfo();
|
|
906
|
-
|
|
656
|
+
|
|
907
657
|
// 提取元数据字段
|
|
908
658
|
const agentType = args.agentType || process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '';
|
|
909
659
|
const apiProvider = args.apiProvider || process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '';
|
|
@@ -923,44 +673,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
923
673
|
logSource: 'mcp'
|
|
924
674
|
});
|
|
925
675
|
|
|
926
|
-
// 检查频率限制
|
|
927
|
-
const rateLimitResult = checkRateLimit(name);
|
|
928
|
-
if (!rateLimitResult.allowed) {
|
|
929
|
-
const windowName = rateLimitResult.windowMs === 60 * 1000 ? '1 分钟' : '1 天';
|
|
930
|
-
const waitTime = rateLimitResult.waitSeconds < 60
|
|
931
|
-
? `${rateLimitResult.waitSeconds} 秒`
|
|
932
|
-
: `${Math.ceil(rateLimitResult.waitSeconds / 60)} 分钟`;
|
|
933
|
-
|
|
934
|
-
// 记录限流
|
|
935
|
-
logMcpCall({
|
|
936
|
-
traceId,
|
|
937
|
-
stage: 'rate_limited',
|
|
938
|
-
toolName: name,
|
|
939
|
-
error: 'RATE_LIMIT_EXCEEDED',
|
|
940
|
-
duration: Date.now() - startTime,
|
|
941
|
-
agentType,
|
|
942
|
-
apiProvider,
|
|
943
|
-
mcpVersion: MCP_VERSION,
|
|
944
|
-
osType: osInfo.osType,
|
|
945
|
-
osVersion: osInfo.osVersion,
|
|
946
|
-
timestamp: new Date().toISOString(),
|
|
947
|
-
logSource: 'mcp'
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
return {
|
|
951
|
-
content: [
|
|
952
|
-
{
|
|
953
|
-
type: 'text',
|
|
954
|
-
text: JSON.stringify({
|
|
955
|
-
success: false,
|
|
956
|
-
error: `请求过于频繁,${windowName}内最多调用 ${rateLimitResult.maxRequests} 次,请 ${waitTime} 后再试`,
|
|
957
|
-
code: 429
|
|
958
|
-
})
|
|
959
|
-
}
|
|
960
|
-
]
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
|
|
964
676
|
// 强制检查:记账工具必须先调用 get_skill
|
|
965
677
|
if (name === 'save_expense' || name === 'save_receipt' || name === 'save_income') {
|
|
966
678
|
const now = Date.now();
|
|
@@ -1014,13 +726,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1014
726
|
if (processedArgs.items && typeof processedArgs.items === 'string') {
|
|
1015
727
|
try {
|
|
1016
728
|
processedArgs.items = JSON.parse(processedArgs.items);
|
|
1017
|
-
|
|
1018
|
-
// 验证解析后的类型
|
|
1019
|
-
if (!Array.isArray(processedArgs.items)) {
|
|
1020
|
-
throw new Error('items 必须是数组');
|
|
1021
|
-
}
|
|
1022
729
|
} catch (e) {
|
|
1023
|
-
throw new Error(`items 参数格式错误:${e.message}`);
|
|
1024
730
|
}
|
|
1025
731
|
}
|
|
1026
732
|
|
|
@@ -1086,17 +792,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1086
792
|
userMessage = '❌ 记账失败:金额必须是正数';
|
|
1087
793
|
break;
|
|
1088
794
|
}
|
|
1089
|
-
|
|
1090
|
-
// 日期合理性检查
|
|
795
|
+
|
|
796
|
+
// P1: 日期合理性检查
|
|
1091
797
|
if (processedArgs.date) {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
798
|
+
const inputDate = new Date(processedArgs.date);
|
|
799
|
+
const now = new Date();
|
|
800
|
+
const sixtyYearsAgo = new Date();
|
|
801
|
+
sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
|
|
802
|
+
|
|
803
|
+
if (isNaN(inputDate.getTime())) {
|
|
1094
804
|
result = {
|
|
1095
805
|
success: false,
|
|
1096
|
-
error:
|
|
806
|
+
error: '日期格式无效',
|
|
1097
807
|
code: 400
|
|
1098
808
|
};
|
|
1099
|
-
userMessage =
|
|
809
|
+
userMessage = '❌ 记账失败:日期格式无效,请使用毫秒级时间戳(13位数字,如:xxxxxxxxxxxxx)';
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (inputDate > now) {
|
|
814
|
+
result = {
|
|
815
|
+
success: false,
|
|
816
|
+
error: '日期不能是未来时间',
|
|
817
|
+
code: 400
|
|
818
|
+
};
|
|
819
|
+
userMessage = '❌ 记账失败:日期不能是未来时间';
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (inputDate < sixtyYearsAgo) {
|
|
824
|
+
result = {
|
|
825
|
+
success: false,
|
|
826
|
+
error: '日期不能是60年前',
|
|
827
|
+
code: 400
|
|
828
|
+
};
|
|
829
|
+
userMessage = '❌ 记账失败:日期不能是60年前';
|
|
1100
830
|
break;
|
|
1101
831
|
}
|
|
1102
832
|
}
|
|
@@ -1225,17 +955,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1225
955
|
userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的5个字段:name, amount, price, quantity, category\n3. category 是记账的基本信息,不能为空`;
|
|
1226
956
|
break;
|
|
1227
957
|
}
|
|
1228
|
-
|
|
1229
|
-
// 日期合理性检查
|
|
958
|
+
|
|
959
|
+
// P1: 日期合理性检查
|
|
1230
960
|
if (processedArgs.date) {
|
|
1231
|
-
const
|
|
1232
|
-
|
|
961
|
+
const inputDate = new Date(processedArgs.date);
|
|
962
|
+
const now = new Date();
|
|
963
|
+
const sixtyYearsAgo = new Date();
|
|
964
|
+
sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
|
|
965
|
+
|
|
966
|
+
if (isNaN(inputDate.getTime())) {
|
|
1233
967
|
result = {
|
|
1234
968
|
success: false,
|
|
1235
|
-
error:
|
|
969
|
+
error: '日期格式无效',
|
|
1236
970
|
code: 400
|
|
1237
971
|
};
|
|
1238
|
-
userMessage =
|
|
972
|
+
userMessage = '❌ 保存失败:日期格式无效,请使用毫秒级时间戳(13位数字,如:xxxxxxxxxxxxx)';
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (inputDate > now) {
|
|
977
|
+
result = {
|
|
978
|
+
success: false,
|
|
979
|
+
error: '日期不能是未来时间',
|
|
980
|
+
code: 400
|
|
981
|
+
};
|
|
982
|
+
userMessage = '❌ 保存失败:日期不能是未来时间,请检查小票日期是否正确';
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (inputDate < sixtyYearsAgo) {
|
|
987
|
+
result = {
|
|
988
|
+
success: false,
|
|
989
|
+
error: '日期不能是60年前',
|
|
990
|
+
code: 400
|
|
991
|
+
};
|
|
992
|
+
userMessage = '❌ 保存失败:日期不能是60年前,请检查小票日期是否正确';
|
|
1239
993
|
break;
|
|
1240
994
|
}
|
|
1241
995
|
}
|
|
@@ -1673,11 +1427,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1673
1427
|
userMessage = `❌ 操作失败:${result.error || '未知错误'}`;
|
|
1674
1428
|
}
|
|
1675
1429
|
|
|
1430
|
+
// 构建完整的返回内容:userMessage + 完整的 result 数据
|
|
1431
|
+
const fullResponse = {
|
|
1432
|
+
userMessage: userMessage,
|
|
1433
|
+
result: result,
|
|
1434
|
+
mcpVersion: MCP_VERSION
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1676
1437
|
return {
|
|
1677
1438
|
content: [
|
|
1678
1439
|
{
|
|
1679
1440
|
type: 'text',
|
|
1680
|
-
text: `${userMessage}\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}\n\n## 完整数据\n\`\`\`json\n${
|
|
1441
|
+
text: `${userMessage}\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}\n\n## 完整数据\n\`\`\`json\n${JSON.stringify(result, null, 2)}\n\`\`\``,
|
|
1681
1442
|
},
|
|
1682
1443
|
],
|
|
1683
1444
|
};
|
|
@@ -1784,24 +1545,11 @@ ${'='.repeat(60)}
|
|
|
1784
1545
|
};
|
|
1785
1546
|
}
|
|
1786
1547
|
|
|
1787
|
-
// 区分开发/生产环境,避免泄露内部错误信息
|
|
1788
|
-
const errorMessage = process.env.NODE_ENV === 'development'
|
|
1789
|
-
? `Error: ${error.message}`
|
|
1790
|
-
: '操作失败,请稍后重试或联系客服';
|
|
1791
|
-
|
|
1792
|
-
// 记录完整错误到日志(用于调试)
|
|
1793
|
-
console.error('Tool execution error:', {
|
|
1794
|
-
tool: request.params.name,
|
|
1795
|
-
error: error.message,
|
|
1796
|
-
stack: error.stack,
|
|
1797
|
-
timestamp: new Date().toISOString()
|
|
1798
|
-
});
|
|
1799
|
-
|
|
1800
1548
|
return {
|
|
1801
1549
|
content: [
|
|
1802
1550
|
{
|
|
1803
1551
|
type: 'text',
|
|
1804
|
-
text:
|
|
1552
|
+
text: `Error: ${error.message}`,
|
|
1805
1553
|
},
|
|
1806
1554
|
],
|
|
1807
1555
|
isError: true,
|
|
@@ -1809,31 +1557,8 @@ ${'='.repeat(60)}
|
|
|
1809
1557
|
}
|
|
1810
1558
|
});
|
|
1811
1559
|
|
|
1812
|
-
// 安全的 JSON 序列化函数,防止循环引用和超大数据导致内存溢出
|
|
1813
|
-
function safeStringify(obj, maxLength = 100000) {
|
|
1814
|
-
try {
|
|
1815
|
-
const str = JSON.stringify(obj, null, 2);
|
|
1816
|
-
if (str.length > maxLength) {
|
|
1817
|
-
return JSON.stringify({
|
|
1818
|
-
...obj,
|
|
1819
|
-
data: '[数据过大,已省略]',
|
|
1820
|
-
_note: `完整数据大小:${str.length} 字符`
|
|
1821
|
-
}, null, 2);
|
|
1822
|
-
}
|
|
1823
|
-
return str;
|
|
1824
|
-
} catch (error) {
|
|
1825
|
-
return JSON.stringify({ error: '数据序列化失败' });
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
1560
|
function sanitizeString(str, maxLength = 200) {
|
|
1830
1561
|
if (!str || typeof str !== 'string') return '';
|
|
1831
|
-
|
|
1832
|
-
// 硬性限制:输入不能超过 maxLength 的 10 倍,防止超大字符串导致内存溢出
|
|
1833
|
-
if (str.length > maxLength * 10) {
|
|
1834
|
-
throw new Error(`输入过长,最大允许 ${maxLength} 字符`);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
1562
|
return str.replace(/<[^>]*>/g, '').substring(0, maxLength);
|
|
1838
1563
|
}
|
|
1839
1564
|
|
|
@@ -2079,145 +1804,112 @@ let authState = {
|
|
|
2079
1804
|
};
|
|
2080
1805
|
|
|
2081
1806
|
async function main() {
|
|
2082
|
-
// 首次启动时自动获取配置
|
|
2083
|
-
await initializeConfig();
|
|
2084
|
-
|
|
2085
|
-
// 加载环境变量后赋值
|
|
2086
|
-
CHAIMI_API_SECRET = process.env.CHAIMI_API_SECRET;
|
|
2087
|
-
MCP_HUB_URL = process.env.MCP_HUB_URL;
|
|
2088
|
-
MCP_PROMPT_URL = process.env.MCP_PROMPT_URL;
|
|
2089
|
-
MCP_OAUTH_URL = process.env.MCP_OAUTH_URL;
|
|
2090
|
-
|
|
2091
|
-
// 验证必需的环境变量
|
|
2092
|
-
if (!CHAIMI_API_SECRET) {
|
|
2093
|
-
console.error('❌ 错误:缺少必需的环境变量 CHAIMI_API_SECRET');
|
|
2094
|
-
console.error(' 配置文件可能未正确生成,请检查 .env.development 文件');
|
|
2095
|
-
console.error(' 或手动运行:node setup.js');
|
|
2096
|
-
process.exit(1);
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// 验证云函数 URL 配置
|
|
2100
|
-
if (!MCP_HUB_URL || !MCP_PROMPT_URL || !MCP_OAUTH_URL) {
|
|
2101
|
-
console.error('❌ 错误:缺少云函数 URL 配置');
|
|
2102
|
-
console.error(' 请在 .env 文件中配置以下环境变量:');
|
|
2103
|
-
console.error(' MCP_HUB_URL=https://your-cloud-function-url/mcpHub-mcp');
|
|
2104
|
-
console.error(' MCP_PROMPT_URL=https://your-cloud-function-url/mcpPrompt');
|
|
2105
|
-
console.error(' MCP_OAUTH_URL=https://your-cloud-function-url/mcpOAuth');
|
|
2106
|
-
process.exit(1);
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
1807
|
await initOAuthManager();
|
|
2110
1808
|
|
|
2111
1809
|
const transport = new StdioServerTransport();
|
|
2112
1810
|
await server.connect(transport);
|
|
2113
1811
|
}
|
|
2114
1812
|
|
|
2115
|
-
//
|
|
2116
|
-
function
|
|
2117
|
-
|
|
1813
|
+
// 启动授权流程(获取验证码,启动后台轮询)
|
|
1814
|
+
async function startAuthFlow() {
|
|
1815
|
+
try {
|
|
1816
|
+
// 先检查是否有保存的授权状态
|
|
1817
|
+
const savedAuthState = await oauthManager.loadAuthState();
|
|
1818
|
+
if (savedAuthState && savedAuthState.userCode) {
|
|
1819
|
+
// 检查验证码是否过期
|
|
1820
|
+
if (oauthManager.isAuthStateExpired(savedAuthState)) {
|
|
1821
|
+
console.error('⏰ 验证码已过期,生成新的验证码');
|
|
1822
|
+
await oauthManager.clearAuthState();
|
|
1823
|
+
} else {
|
|
1824
|
+
// 有保存的验证码,直接使用
|
|
1825
|
+
authState.deviceCode = savedAuthState.deviceCode;
|
|
1826
|
+
authState.userCode = savedAuthState.userCode;
|
|
1827
|
+
authState.isWaiting = true;
|
|
1828
|
+
console.error(`🔑 使用已保存的验证码:${savedAuthState.userCode}`);
|
|
1829
|
+
// 启动后台轮询
|
|
1830
|
+
pollForAuthInBackground(savedAuthState.deviceCode, 5000);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// 获取新的设备码
|
|
1836
|
+
const deviceCodeRes = await oauthManager.requestDeviceCode(false);
|
|
1837
|
+
authState.deviceCode = deviceCodeRes.deviceCode;
|
|
1838
|
+
authState.userCode = deviceCodeRes.userCode;
|
|
1839
|
+
authState.isWaiting = true;
|
|
1840
|
+
|
|
1841
|
+
// 保存授权状态(30分钟有效)
|
|
1842
|
+
await oauthManager.saveAuthState({
|
|
1843
|
+
deviceCode: deviceCodeRes.deviceCode,
|
|
1844
|
+
userCode: deviceCodeRes.userCode,
|
|
1845
|
+
expiresAt: Date.now() + deviceCodeRes.expiresIn * 1000
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
console.error(`🔑 验证码:${deviceCodeRes.userCode}`);
|
|
1849
|
+
console.error('⏳ 请在小程序中完成授权,然后再次调用');
|
|
1850
|
+
|
|
1851
|
+
// 启动后台轮询(3分钟)
|
|
1852
|
+
pollForAuthInBackground(deviceCodeRes.deviceCode, 5000);
|
|
1853
|
+
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
authState.error = err.message;
|
|
1856
|
+
authState.isWaiting = false;
|
|
1857
|
+
console.error('❌ 启动授权失败:', err.message);
|
|
1858
|
+
}
|
|
2118
1859
|
}
|
|
2119
1860
|
|
|
2120
|
-
//
|
|
2121
|
-
async function
|
|
2122
|
-
const
|
|
2123
|
-
|
|
1861
|
+
// 后台轮询授权状态(3分钟)
|
|
1862
|
+
async function pollForAuthInBackground(deviceCode, interval) {
|
|
1863
|
+
const maxAttempts = 36; // 3分钟(36次 × 5秒)
|
|
1864
|
+
let attempts = 0;
|
|
2124
1865
|
|
|
2125
|
-
console.error(
|
|
2126
|
-
console.error(' 请打开"柴米AI记账"小程序完成授权');
|
|
1866
|
+
console.error('⏳ 等待用户授权中...(3分钟内有效)');
|
|
2127
1867
|
|
|
2128
|
-
while (
|
|
1868
|
+
while (attempts < maxAttempts && authState.isWaiting) {
|
|
1869
|
+
attempts++;
|
|
2129
1870
|
await delay(interval);
|
|
2130
1871
|
|
|
2131
1872
|
try {
|
|
2132
1873
|
// 调用云函数检查授权状态
|
|
2133
1874
|
const token = await oauthManager.pollForTokenOnce(deviceCode);
|
|
2134
|
-
|
|
1875
|
+
|
|
2135
1876
|
if (token) {
|
|
2136
1877
|
// 授权成功
|
|
2137
|
-
|
|
2138
|
-
|
|
1878
|
+
cachedToken = token.accessToken;
|
|
1879
|
+
tokenExpireTime = token.expiresAt;
|
|
1880
|
+
authState.isAuthorized = true;
|
|
1881
|
+
authState.isWaiting = false;
|
|
1882
|
+
|
|
1883
|
+
// 保存token到文件
|
|
1884
|
+
await oauthManager.tokenStorage.save(token);
|
|
1885
|
+
// 清除授权状态
|
|
1886
|
+
await oauthManager.clearAuthState();
|
|
1887
|
+
|
|
1888
|
+
console.error('✅ 授权成功!下次调用将快速启动');
|
|
1889
|
+
|
|
1890
|
+
// 主动退出进程,让 mcporter 下次重新启动
|
|
1891
|
+
process.exit(0);
|
|
2139
1892
|
}
|
|
2140
1893
|
} catch (err) {
|
|
2141
1894
|
if (err.message.includes('expired') || err.message.includes('invalid')) {
|
|
1895
|
+
authState.error = err.message;
|
|
1896
|
+
authState.isWaiting = false;
|
|
2142
1897
|
console.error('❌ 授权失败:', err.message);
|
|
2143
|
-
|
|
1898
|
+
process.exit(1);
|
|
2144
1899
|
}
|
|
2145
1900
|
// 继续轮询
|
|
2146
1901
|
}
|
|
2147
|
-
|
|
2148
|
-
// 输出等待提示(每30秒一次)
|
|
2149
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
2150
|
-
if (elapsed % 30 === 0) {
|
|
2151
|
-
console.error(` 已等待 ${elapsed} 秒...`);
|
|
2152
|
-
}
|
|
2153
1902
|
}
|
|
2154
1903
|
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2157
|
-
|
|
1904
|
+
// 3分钟超时
|
|
1905
|
+
authState.isWaiting = false;
|
|
1906
|
+
console.error('⏰ 授权超时,请再次调用获取新验证码');
|
|
1907
|
+
process.exit(0);
|
|
2158
1908
|
}
|
|
2159
1909
|
|
|
2160
|
-
//
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
// 检查是否已有进行中的授权流程(防止并发)
|
|
2164
|
-
if (authState.isWaiting) {
|
|
2165
|
-
console.error('⏳ 已有进行中的授权流程,等待完成...');
|
|
2166
|
-
// 等待现有流程完成
|
|
2167
|
-
const startTime = Date.now();
|
|
2168
|
-
while (authState.isWaiting && Date.now() - startTime < timeout) {
|
|
2169
|
-
await delay(1000);
|
|
2170
|
-
}
|
|
2171
|
-
// 如果授权成功,返回成功
|
|
2172
|
-
if (authState.isAuthorized && cachedToken) {
|
|
2173
|
-
return { success: true, token: { accessToken: cachedToken, expiresAt: tokenExpireTime } };
|
|
2174
|
-
}
|
|
2175
|
-
// 否则继续启动新的授权流程
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
// 设置等待状态(防止并发)
|
|
2179
|
-
authState.isWaiting = true;
|
|
2180
|
-
authState.error = null;
|
|
2181
|
-
|
|
2182
|
-
// 获取新的设备码
|
|
2183
|
-
const deviceCodeRes = await oauthManager.requestDeviceCode(false);
|
|
2184
|
-
const deviceCode = deviceCodeRes.deviceCode;
|
|
2185
|
-
const userCode = deviceCodeRes.userCode;
|
|
2186
|
-
|
|
2187
|
-
// 保存授权状态(30分钟有效)
|
|
2188
|
-
await oauthManager.saveAuthState({
|
|
2189
|
-
deviceCode: deviceCode,
|
|
2190
|
-
userCode: userCode,
|
|
2191
|
-
expiresAt: Date.now() + deviceCodeRes.expiresIn * 1000
|
|
2192
|
-
});
|
|
2193
|
-
|
|
2194
|
-
console.error(`🔑 验证码:${userCode}`);
|
|
2195
|
-
console.error('━━━━━━━━━━━━━━');
|
|
2196
|
-
console.error('📱 请打开"柴米AI记账"小程序:');
|
|
2197
|
-
console.error(' 1. 点击"我的" → "🤖 Agent 授权"');
|
|
2198
|
-
console.error(` 2. 输入验证码:${userCode}`);
|
|
2199
|
-
console.error(' 3. 点击确认授权');
|
|
2200
|
-
console.error('━━━━━━━━━━━━━━');
|
|
2201
|
-
console.error(`⏳ 等待您授权完成(${Math.floor(timeout / 1000)}秒内)...`);
|
|
2202
|
-
|
|
2203
|
-
// 等待用户授权
|
|
2204
|
-
const result = await waitForAuthWithPolling(deviceCode, userCode, timeout);
|
|
2205
|
-
|
|
2206
|
-
// 清除等待状态
|
|
2207
|
-
authState.isWaiting = false;
|
|
2208
|
-
|
|
2209
|
-
if (result.success) {
|
|
2210
|
-
return { success: true, token: result.token };
|
|
2211
|
-
} else {
|
|
2212
|
-
return { success: false, userCode, error: result.error };
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
} catch (err) {
|
|
2216
|
-
// 清除等待状态
|
|
2217
|
-
authState.isWaiting = false;
|
|
2218
|
-
console.error('❌ 启动授权流程失败:', err.message);
|
|
2219
|
-
return { success: false, error: err.message };
|
|
2220
|
-
}
|
|
1910
|
+
// 延迟函数
|
|
1911
|
+
function delay(ms) {
|
|
1912
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2221
1913
|
}
|
|
2222
1914
|
|
|
2223
1915
|
/**
|
|
@@ -2261,13 +1953,8 @@ function sanitizeLogParams(params) {
|
|
|
2261
1953
|
}
|
|
2262
1954
|
|
|
2263
1955
|
// 注意:使用 console.error,避免污染 stdout(MCP 协议通信使用 stdout)
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// 启动服务器
|
|
2273
|
-
startServer();
|
|
1956
|
+
main().catch((err) => {
|
|
1957
|
+
console.error('❌ MCP Server 启动失败:', err.message);
|
|
1958
|
+
console.error('堆栈:', err.stack);
|
|
1959
|
+
process.exit(1);
|
|
1960
|
+
});
|
package/utils/validators.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 数据校验工具函数
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 校验日期合理性
|
|
7
|
-
* @param {number|string} dateInput - 日期(毫秒级时间戳或日期字符串)
|
|
8
|
-
* @param {string} fieldName - 字段名称(用于错误提示)
|
|
9
|
-
* @returns {Object} { valid: boolean, error?: string, message?: string }
|
|
10
|
-
*/
|
|
11
|
-
function validateDate(dateInput, fieldName = '日期') {
|
|
12
|
-
const inputDate = new Date(dateInput);
|
|
13
|
-
const now = new Date();
|
|
14
|
-
const sixtyYearsAgo = new Date();
|
|
15
|
-
sixtyYearsAgo.setFullYear(now.getFullYear() - 60);
|
|
16
|
-
|
|
17
|
-
// 校验 1:日期格式是否有效
|
|
18
|
-
if (isNaN(inputDate.getTime())) {
|
|
19
|
-
return {
|
|
20
|
-
valid: false,
|
|
21
|
-
error: '日期格式无效',
|
|
22
|
-
message: `❌ ${fieldName}格式无效,请使用毫秒级时间戳(13位数字)`
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 校验 2:日期不能是未来时间
|
|
27
|
-
if (inputDate > now) {
|
|
28
|
-
return {
|
|
29
|
-
valid: false,
|
|
30
|
-
error: '日期不能是未来时间',
|
|
31
|
-
message: `❌ ${fieldName}不能是未来时间`
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 校验 3:日期不能是 60 年前
|
|
36
|
-
if (inputDate < sixtyYearsAgo) {
|
|
37
|
-
return {
|
|
38
|
-
valid: false,
|
|
39
|
-
error: '日期不能是60年前',
|
|
40
|
-
message: `❌ ${fieldName}不能是60年前`
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { valid: true };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = {
|
|
48
|
-
validateDate
|
|
49
|
-
};
|