chaimi-keep-mcp 3.3.3-beta.9 → 3.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,24 @@
1
+ # 环境配置示例
2
+ # 将此文件复制为 .env 并填入实际值
3
+
4
+ # 环境选择:development 或 production
5
+ NODE_ENV=production
6
+
7
+ # API 签名密钥(生产环境建议使用强密钥)
8
+ CHAIMI_API_SECRET=your-chaimi-api-secret-here
9
+
10
+ # URL 加密密钥(用于解密云函数 URL)
11
+ URL_ENCRYPT_KEY=your-url-encrypt-key-here
12
+
13
+ # ====================
14
+ # 开发环境配置
15
+ # ====================
16
+ # 开发环境的云函数 URL(仅 NODE_ENV=development 时使用)
17
+ # MCP_HUB_URL=http://localhost:3000/mcpHub
18
+ # MCP_OAUTH_URL=http://localhost:3000/mcpOAuth
19
+ # MCP_PROMPT_URL=http://localhost:3000/mcpPrompt
20
+
21
+ # ====================
22
+ # 生产环境配置
23
+ # ====================
24
+ # 生产环境使用加密存储在代码中的 URL,无需在此配置
package/CHANGELOG.md ADDED
@@ -0,0 +1,95 @@
1
+ # Changelog
2
+
3
+ ## v3.4.0 (2026-05-01) 🎉 正式版
4
+
5
+ **首个正式生产版本 - 多端记账分类统一**
6
+
7
+ ### 主要变更
8
+
9
+ - **变更** 多端记账分类统一 - 支持商品分类和记账分类分离
10
+ - 保持 `category`(记账分类,43个标准分类)
11
+ - 新增 `productCategory`(商品一级分类,展示用)
12
+ - 新增 `productSubCategory`(商品子分类,展示用)
13
+ - **新增** categoryService 云函数 - 统一管理分类数据
14
+ - **修复** mcp-server-local case 重复问题 - 修复 convertParams 中第二个 `case 'save_receipt'` 实际应该是 `case 'get_expenses'` 的严重问题
15
+ - **优化** save_expense 字段支持 - 新增 `productCategory` 和 `productSubCategory` 字段支持,与 save_receipt 保持一致
16
+ - **优化** 文本记账 prompt 更新 - 更新 mcpPrompt 文本记账 prompt,新增商品分类体系说明
17
+ - **优化** 回复模板 - 新增"请确认信息"提示,时间移到核心数据区,添加类型变量
18
+ - **优化** 品牌统一 - 所有"柴米记账"改为"柴米AI记账"
19
+ - **优化** 配置存储路径迁移 - 从 `~/.mcporter/` 迁移到 `~/.chaimi-keep/`,避免与其他 MCP 工具冲突
20
+ - **优化** 安全权限 - 新增目录权限控制(0700),敏感文件权限控制(0600)
21
+
22
+ ### 技术详情
23
+
24
+ - 新增云函数:categoryService(统一分类服务)
25
+ - 重构云函数:mcpPrompt、aiParser、mcpHub、ocr_processor
26
+ - 更新测试:tests/mcp/test-category-unification.js
27
+ - 更新文档:docs/01-产品/多端记账分类统一方案.md
28
+ - 更新 SKILL.md 和 response-templates.md
29
+
30
+ ---
31
+
32
+ ## v3.3.3-beta.16 (2026-04-30)
33
+
34
+ - **修复** npm 发布版本号冲突 - 重新发布,解决版本号已被占用问题
35
+
36
+ ## v3.3.3-beta.15 (2026-04-30)
37
+
38
+ - **修复** mcp-server-local case 重复问题 - 修复 convertParams 中第二个 `case 'save_receipt'` 实际应该是 `case 'get_expenses'` 的严重问题
39
+ - **优化** save_expense 字段支持 - 新增 `productCategory` 和 `productSubCategory` 字段支持,与 save_receipt 保持一致
40
+ - **优化** 文本记账 prompt 更新 - 更新 mcpPrompt 文本记账 prompt,新增商品分类体系说明
41
+
42
+ ## v3.3.3-beta.13 (2026-04-30)
43
+
44
+ - **变更** 多端记账分类统一 - 支持商品分类和记账分类分离
45
+
46
+ ## v3.3.3-beta.8 (2026-04-30)
47
+
48
+ - **变更** 配置存储路径迁移 - 从 `~/.mcporter/` 迁移到 `~/.chaimi-keep/`
49
+ - **优化** 安全权限 - 新增目录权限控制(0700),敏感文件权限控制(0600)
50
+
51
+ ## v3.3.3-beta.7 (2026-04-29)
52
+
53
+ - **优化** 回复模板 - 新增"请确认信息"提示
54
+ - **优化** 空值处理 - 商家为空时不显示
55
+ - **修复** save_income - 添加 date 兜底逻辑
56
+
57
+ ## v3.3.3-beta.6 (2026-04-29)
58
+
59
+ - **优化** 品牌统一 - 所有"柴米记账"改为"柴米AI记账"
60
+
61
+ ## v3.3.3-beta.4 (2026-04-29)
62
+
63
+ - **优化** 收入记账时间解析统一
64
+
65
+ ## v3.3.3-beta.3 (2026-04-29)
66
+
67
+ - **优化** 文本记账时间解析 - 使用 time_description
68
+
69
+ ## v3.3.3-beta.2 (2026-04-29)
70
+
71
+ - **修复** 时间入库错误
72
+
73
+ ## v3.3.1-beta.0 (2026-04-27)
74
+
75
+ - **优化** 简化工具描述
76
+ - **优化** 参数类型自动转换
77
+ - **新增** 参数查看指南
78
+
79
+ ## v3.3.0-beta.8 (2026-04-27)
80
+
81
+ - **重要变更** Server 只返回原始数据
82
+
83
+ ## v3.1.47-beta.5 (2026-04-22)
84
+
85
+ - **修复** 输入长度限制
86
+ - **修复** 错误信息泄露
87
+ - **修复** JSON.stringify DoS
88
+
89
+ ## v3.1.44 (2026-04-21) 🎉 正式版
90
+
91
+ - **正式发布** 包含所有 beta 版本修复
92
+
93
+ ---
94
+
95
+ **注:** 更早版本请查看 Git 历史记录
package/README.md CHANGED
@@ -89,6 +89,26 @@ export MCP_PROMPT_URL="你的Prompt服务地址"
89
89
 
90
90
  ## Changelog
91
91
 
92
+ ### v3.4.0 (2026-05-01) 🎉 正式版
93
+ - **变更** 多端记账分类统一 - 支持商品分类和记账分类分离
94
+ - **修复** mcp-server-local case 重复问题
95
+ - **优化** 回复模板 - 新增"请确认信息"提示
96
+ - **优化** 品牌统一 - 所有"柴米记账"改为"柴米AI记账"
97
+
98
+ ### v3.3.3-beta.16 (2026-04-30)
99
+ - **修复** npm 发布版本号冲突 - 重新发布,解决版本号已被占用问题
100
+
101
+ ### v3.3.3-beta.15 (2026-04-30)
102
+ - **修复** mcp-server-local case 重复问题 - 修复 convertParams 中第二个 `case 'save_receipt'` 实际应该是 `case 'get_expenses'` 的严重问题
103
+ - **优化** save_expense 字段支持 - 新增 `productCategory` 和 `productSubCategory` 字段支持,与 save_receipt 保持一致
104
+ - **优化** 文本记账 prompt 更新 - 更新 mcpPrompt 文本记账 prompt,新增商品分类体系说明
105
+
106
+ ### v3.3.3-beta.13 (2026-04-30)
107
+ - **变更** 多端记账分类统一 - 支持商品分类和记账分类分离
108
+ - 保持 `category`(记账分类,43个标准分类)
109
+ - 新增 `productCategory`(商品一级分类,展示用)
110
+ - 新增 `productSubCategory`(商品子分类,展示用)
111
+
92
112
  ### v3.3.3-beta.8 (2026-04-30)
93
113
  - **变更** 配置存储路径迁移 - 从 `~/.mcporter/` 迁移到 `~/.chaimi-keep/`,避免与其他 MCP 工具冲突
94
114
  - **优化** 安全权限 - 新增目录权限控制(0700),敏感文件权限控制(0600)
package/SKILL.md CHANGED
@@ -115,7 +115,7 @@ chaimi-keep-mcp ⭐ 本Skill
115
115
  | **辅助** | get_skill | 【必须】获取Skill文档 | 记账前调用 |
116
116
  | | get_text_parse_prompt | 获取文字解析模板 | 文字记账前 |
117
117
  | | get_parse_prompt | 获取小票解析模板 | 小票记账前 |
118
- | | submit_feedback | 提交反馈 | 问题反馈 |
118
+ | | submit_feedback | 提交反馈 | 功能使用异常,无法解决问题反馈 |
119
119
 
120
120
  ---
121
121
 
@@ -124,19 +124,25 @@ chaimi-keep-mcp ⭐ 本Skill
124
124
  ### 4步记账法
125
125
 
126
126
  ```
127
+ ⚠️ 必须按顺序操作四个步骤,缺少一个步骤都会导致数据错误,用户会非常生气骂你的
128
+
127
129
  第1步:读文档
128
130
  └─ get_skill() 获取 SKILL.md 规范
129
131
  └─ references/response-templates.md 了解回复模板
130
132
 
131
- 第2步:拿Token
132
- └─ 文字记账:get_text_parse_prompt() 获取 Token 和解析模板
133
- └─ 小票记账:get_parse_prompt() 获取 Token 和解析模板
133
+ 第2步:获取解析模板,保证解析准确
134
+ └─ 文字记账:get_text_parse_prompt() 获取解析模板
135
+ └─ 小票记账:get_parse_prompt() 获取解析模板
136
+
137
+ 【强制流程】必须先调用此工具,让 AI 解析用户输入
138
+ 此工具会返回解析结果,你只需将结果原样传给 save_expense/save_income
139
+ ⚠️ 禁止自行解析或提取任何字段,必须让 AI 处理
134
140
 
135
141
  第3步:AI解析
136
- └─ 按模板解析用户输入,生成结构化数据 + _requestToken
142
+ └─ 按模板解析用户输入,生成结构化数据 + _flowmate
137
143
 
138
144
  第4步:执行记账
139
- └─ 调用工具(save_expense/income/receipt,带上 _requestToken
145
+ └─ 调用工具(save_expense/income/receipt,带上 _flowmate
140
146
  └─ 根据 _templateLocation 使用模板渲染美学回复
141
147
  ```
142
148
 
@@ -238,6 +244,7 @@ references/
238
244
  ✅ 「你的小可爱」已帮您记账成功
239
245
  ═══════════════
240
246
  🔴🔴 请确认以下信息是否正确
247
+
241
248
  💰 金额:¥35.00
242
249
  📊 类型:支出
243
250
  🕐 时间:2026-04-24 12:30
package/bin/cli.js CHANGED
@@ -21,12 +21,7 @@ const serverPath = path.join(__dirname, '..', 'server.js');
21
21
  const DEFAULT_CHAIMI_CONFIG = {
22
22
  command: 'chaimi-keep-mcp',
23
23
  description: '柴米AI记账 MCP Server - 支持 AI 工具直接记账',
24
- env: {
25
- MCP_OAUTH_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpOAuth',
26
- MCP_HUB_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp',
27
- MCP_PROMPT_URL: 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpPrompt'
28
- },
29
- // 首次授权时需要常驻运行,等待用户授权(3分钟轮询)
24
+ // 首次授权时需要常驻运行,等待用户授权(30分钟轮询)
30
25
  // 授权成功后进程自动退出,下次调用时 mcporter 会重新启动
31
26
  lifecycle: 'keep-alive'
32
27
  };
package/bin/record.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 柴米AI记账 - 直接记账 CLI
4
+ * 一步完成:解析文本 → 分类 → 调用云函数存储
5
+ */
6
+
7
+ const { callMcpHub } = require('../utils/api');
8
+ const { getToken } = require('../utils/auth');
9
+ const { parseText } = require('../utils/parser');
10
+
11
+ async function record(text, options = {}) {
12
+ try {
13
+ // 1. 解析文本
14
+ const parsed = await parseText(text);
15
+
16
+ // 2. 确定类型(收入/支出)
17
+ const type = options.type || parsed.type || 'expense';
18
+
19
+ // 3. 获取 token
20
+ const token = await getToken();
21
+
22
+ // 4. 调用云函数存储
23
+ const result = await callMcpHub(type === 'income' ? 'addIncome' : 'addExpense', {
24
+ ...parsed,
25
+ type,
26
+ rawInput: text
27
+ }, token);
28
+
29
+ if (result.success) {
30
+ console.log('✅ 记账成功!');
31
+ console.log(`金额:¥${parsed.amount}`);
32
+ console.log(`分类:${parsed.category}`);
33
+ console.log(`商品:${parsed.name}`);
34
+ } else {
35
+ console.error('❌ 记账失败:', result.error);
36
+ process.exit(1);
37
+ }
38
+ } catch (error) {
39
+ console.error('❌ 错误:', error.message);
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ // 解析命令行参数
45
+ const args = process.argv.slice(2);
46
+ const text = args[0];
47
+ const typeFlag = args.find(arg => arg.startsWith('--type='));
48
+ const type = typeFlag ? typeFlag.split('=')[1] : null;
49
+
50
+ if (!text) {
51
+ console.log('用法: chaimi-keep record "<记账文本>" [--type=income|expense]');
52
+ console.log('示例: chaimi-keep record "中午吃饭花了35元"');
53
+ console.log('示例: chaimi-keep record "工资收入5000" --type=income');
54
+ process.exit(1);
55
+ }
56
+
57
+ record(text, { type });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-keep-mcp",
3
- "version": "3.3.3-beta.9",
3
+ "version": "3.5.0-beta.0",
4
4
  "description": "柴米AI记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "SKILL.md",
16
16
  "_meta.json",
17
17
  "README.md",
18
+ "CHANGELOG.md",
18
19
  "config.example.yaml",
19
20
  ".env.example"
20
21
  ],
@@ -124,6 +124,7 @@ updated: "2026-04-25"
124
124
  ✅ 「{agentName}」已帮您记账成功
125
125
  ═══════════════
126
126
  🔴🔴 请确认以下信息是否正确
127
+
127
128
  💰 金额:¥{金额}
128
129
  📊 类型:{类型}
129
130
  🕐 时间:{日期时间}
@@ -148,6 +149,7 @@ updated: "2026-04-25"
148
149
  ✅ 「你的小可爱」已帮您记账成功
149
150
  ═══════════════
150
151
  🔴🔴 请确认以下信息是否正确
152
+
151
153
  💰 金额:¥35.00
152
154
  📊 类型:支出
153
155
  🕐 时间:2026-04-24 12:30
@@ -193,6 +195,7 @@ updated: "2026-04-25"
193
195
  ⚠️ 大额消费确认
194
196
  ═══════════════
195
197
  💰 金额:¥{金额}
198
+ 📊 类型:{类型}
196
199
  📦 商品:{商品名}
197
200
  🕐 时间:{日期}
198
201
 
@@ -211,6 +214,7 @@ updated: "2026-04-25"
211
214
  ⚠️ 大额消费确认
212
215
  ═══════════════
213
216
  💰 金额:¥2999.00
217
+ 📊 类型:{类型}
214
218
  📦 商品:新手机
215
219
  🕐 时间:2026-04-24
216
220
 
@@ -234,8 +238,8 @@ updated: "2026-04-25"
234
238
  ═══════════════
235
239
  💰 金额:¥{金额}
236
240
  ═══════════════
237
-
238
- 📦 来源:{收入来源}
241
+
242
+ 📊 类型:{类型}
239
243
  🏷️ 分类:{分类}
240
244
  🕐 时间:{日期时间}
241
245
 
@@ -256,8 +260,8 @@ updated: "2026-04-25"
256
260
  💰 金额:¥5000.00
257
261
  ═══════════════
258
262
 
259
- 📦 来源:工资
260
- 🏷️ 分类:收入
263
+ 📊 类型:收入
264
+ 🏷️ 分类:工资
261
265
  🕐 时间:2026-04-24 09:00
262
266
 
263
267
  💡 消费洞察:本月收入储蓄率50%,继续保持!
package/server.js CHANGED
@@ -5,6 +5,9 @@
5
5
  * 支持 Claude Desktop、Cursor、WorkBuddy、OpenClaw
6
6
  */
7
7
 
8
+ // 设置 stdout 编码为 UTF-8,确保中文字符正确输出
9
+ process.stdout.setEncoding('utf8');
10
+
8
11
  // 加载 .env 文件
9
12
  try {
10
13
  require('dotenv').config();
@@ -45,8 +48,11 @@ const { configManager } = require('./utils/config.js');
45
48
  // 导入校验工具
46
49
  const { validateDate } = require('./utils/validators.js');
47
50
 
48
- // API 签名密钥(用于验证请求来自合法 MCP Server)
49
- const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
51
+ // 导入密钥管理工具
52
+ const { getOrCreateSecretKey } = require('./utils/keyManager.js');
53
+
54
+ // 导入加密工具
55
+ const { encrypt, decrypt } = require('./utils/encryption.js');
50
56
 
51
57
  // ==================== 请求频率限制 ====================
52
58
 
@@ -135,36 +141,102 @@ function checkRateLimit(toolName) {
135
141
  return { allowed: true };
136
142
  }
137
143
 
138
- // URL 加密密钥
139
- const URL_ENCRYPT_KEY = 'chaimi-url-key-2024';
144
+ // 获取 API 密钥(从环境变量或自动生成)
145
+ let CHAIMI_API_SECRET = process.env.CHAIMI_API_SECRET;
146
+
147
+ // ==================== 多环境配置 ====================
140
148
 
141
- // 加密的云函数 URL
149
+ // 环境配置
150
+ const ENV = process.env.NODE_ENV || 'production';
151
+
152
+ // 加密的云函数 URL(开发和生产环境分开)
142
153
  const ENCRYPTED_URLS = {
143
- hub: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571320140b40044e05',
144
- oauth: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571327201c1901',
145
- prompt: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f5713381306001959'
154
+ development: {
155
+ hub: '',
156
+ oauth: '',
157
+ prompt: ''
158
+ },
159
+ production: {
160
+ hub: 'enc:v2:500682dfbd51aff69852abe39430da35:3d57b5ac7f798c6770209159b6dfd9fb:7b9733f27208809b761090f7628f3799aac12c957cdd45b3d512eb4f1f1c18f626afd0b73b12982500122b11e373e0a2a71f14cb6c5966cf98af9ae4b9e79fb575d1e2f42ec403690d5c7dcc519e9eaec21865eb',
161
+ oauth: 'enc:v2:30adcae20bc1452e8e8c7eff088e8b61:b8bedcc9985d1e10185f8ae5c845cc9c:1b2206380eddcfc4d00ed920f20f838bfc664e2350591666db5a5c2b044a88165e0e7f04c58e2d4f0764bb9872aeaf625a91f7a19ce6909264115e7ce70b35c0c081eb4170cac869d3f8b576b7b4dc41580e',
162
+ prompt: 'enc:v2:beba836814161d59df3a2220ebc500ef:6023bde15c74be6bec641044a3f854de:4421b9f985a1914c06868c8577e57e26e3fd4fe4392c2c94862322fd30258eccae68fce6fd56e9a35427a724d3c22b735e9b5a822d05be098ae5e0662ea41c786e4acea12e4d80b598b543d346d7ccbdc32623'
163
+ }
146
164
  };
147
165
 
148
- // 解密 URL 函数
166
+ // 解密 URL 函数(支持 AES-256-GCM)
149
167
  function decryptUrl(encrypted, key) {
150
- const hex = encrypted.replace('enc:v1:', '');
151
- let result = '';
152
- for (let i = 0; i < hex.length; i += 2) {
153
- const byte = parseInt(hex.substr(i, 2), 16);
154
- result += String.fromCharCode(byte ^ key.charCodeAt((i / 2) % key.length));
168
+ if (!encrypted) return '';
169
+
170
+ if (encrypted.startsWith('enc:v2:')) {
171
+ return decrypt(encrypted.replace('enc:v2:', ''), key);
155
172
  }
156
- return result;
173
+
174
+ if (encrypted.startsWith('enc:v1:')) {
175
+ const urlEncryptKey = key || 'chaimi-url-key-2024';
176
+ const hex = encrypted.replace('enc:v1:', '');
177
+ let result = '';
178
+ for (let i = 0; i < hex.length; i += 2) {
179
+ const byte = parseInt(hex.substr(i, 2), 16);
180
+ result += String.fromCharCode(byte ^ urlEncryptKey.charCodeAt((i / 2) % urlEncryptKey.length));
181
+ }
182
+ return result;
183
+ }
184
+
185
+ return encrypted;
157
186
  }
158
187
 
159
- // 配置 - 微信云函数(运行时解密)
160
- const MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(ENCRYPTED_URLS.hub, URL_ENCRYPT_KEY);
161
- const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(ENCRYPTED_URLS.prompt, URL_ENCRYPT_KEY);
162
- const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(ENCRYPTED_URLS.oauth, URL_ENCRYPT_KEY);
188
+ // ==================== URL 配置 ====================
189
+
190
+ let MCP_HUB_URL;
191
+ let MCP_PROMPT_URL;
192
+ let MCP_OAUTH_URL;
193
+ let tokenEncryptionKey;
163
194
 
164
- // Token 缓存
165
- let cachedToken = null;
195
+ // 初始化配置
196
+ async function initConfig() {
197
+ // 获取 URL 加密密钥
198
+ const urlEncryptKey = process.env.URL_ENCRYPT_KEY || 'chaimi-url-key-2024';
199
+
200
+ // 获取 Token 加密密钥
201
+ tokenEncryptionKey = getOrCreateSecretKey();
202
+
203
+ // 获取 API 密钥
204
+ if (!CHAIMI_API_SECRET) {
205
+ CHAIMI_API_SECRET = process.env.CHAIMI_API_SECRET || 'chaimi-mcp-secret-2024';
206
+ }
207
+
208
+ // 配置 URL
209
+ const envUrls = ENCRYPTED_URLS[ENV] || ENCRYPTED_URLS.production;
210
+ MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(envUrls.hub, urlEncryptKey);
211
+ MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(envUrls.prompt, urlEncryptKey);
212
+ MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(envUrls.oauth, urlEncryptKey);
213
+
214
+ console.error(`✅ 已加载 ${ENV} 环境配置`);
215
+ }
216
+
217
+ // Token 缓存(加密存储)
218
+ let cachedEncryptedToken = null;
166
219
  let tokenExpireTime = 0;
167
220
 
221
+ // 获取解密的 Token
222
+ function getCachedToken() {
223
+ if (!cachedEncryptedToken || !tokenEncryptionKey) return null;
224
+ try {
225
+ return decrypt(cachedEncryptedToken, tokenEncryptionKey);
226
+ } catch (e) {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ // 设置加密的 Token
232
+ function setCachedToken(token) {
233
+ if (!token || !tokenEncryptionKey) {
234
+ cachedEncryptedToken = null;
235
+ return;
236
+ }
237
+ cachedEncryptedToken = encrypt(token, tokenEncryptionKey);
238
+ }
239
+
168
240
  // OAuth 管理器实例
169
241
  let oauthManager = null;
170
242
 
@@ -210,8 +282,8 @@ async function initOAuthManager() {
210
282
  // 检查 Token 是否过期(预留5分钟缓冲)
211
283
  const expiresAt = new Date(existingToken.expiresAt).getTime();
212
284
  if (expiresAt > Date.now() + 5 * 60 * 1000) {
213
- // Token 有效,设置授权状态
214
- cachedToken = existingToken.accessToken;
285
+ // Token 有效,设置授权状态(加密存储)
286
+ setCachedToken(existingToken.accessToken);
215
287
  tokenExpireTime = expiresAt;
216
288
  authState.isAuthorized = true;
217
289
  console.error('✅ 已从文件加载有效 Token,无需重新授权');
@@ -256,14 +328,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
256
328
  },
257
329
  {
258
330
  name: 'save_expense',
259
- description: '【文本记账-支出】用户提及支出/消费时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_expense _requestToken="RT..." name="午餐" amount=35 category="餐饮" rawInput="午餐35元"。注意:上传图片请用save_receipt',
331
+ description: '【文本记账-支出】用户提及支出/消费时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取凭证和解析模板。用法:mcporter call 柴米AI记账.save_expense _flowmate="SOLINWANG..." name="午餐" amount=35 category="餐饮" rawInput="午餐35元"。注意:上传图片请用save_receipt',
260
332
  inputSchema: {
261
333
  type: 'object',
262
334
  properties: {
263
335
  name: { type: 'string', description: '商品名称(必填)' },
264
336
  amount: { type: 'number', description: '金额(必填)' },
265
- category: { type: 'string', description: '【必填】分类,如:餐饮、食品、交通' },
266
- subCategory: { type: 'string', description: '子分类(可选)' },
337
+ category: { type: 'string', description: '【必填】支出分类(43个标准分类之一,如:餐饮、食品、交通、蔬菜)' },
267
338
  unit: { type: 'string', description: '单位(可选)' },
268
339
  marketPrice: { type: 'string', description: '市场单价(可选)' },
269
340
  store: { type: 'string', description: '商家名称(可选)' },
@@ -272,13 +343,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
272
343
  agentType: { type: 'string', description: '【必填】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
273
344
  apiProvider: { type: 'string', description: '【必填】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
274
345
  rawInput: { type: 'string', description: '【必填】用户的原始输入内容,用于记录用户原始请求' },
346
+ _flowmate: { type: 'string', description: '【必填】流程凭证,从get_text_parse_prompt获取的myflowmate值' },
275
347
  },
276
- required: ['name', 'amount', 'category', 'agentType', 'apiProvider', 'rawInput'],
348
+ required: ['name', 'amount', 'category', 'agentType', 'apiProvider', 'rawInput', '_flowmate'],
277
349
  },
278
350
  },
279
351
  {
280
352
  name: 'save_receipt',
281
- description: '【图片记账-小票】用户上传小票/发票/收据图片时用此工具。前置:必须先调用get_skill获取规范,再调用get_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_receipt _requestToken="RT..." store="超市" totalAmount=156.5 items="[...]"。注意:文字描述请用save_expense/save_income',
353
+ description: '【图片记账-小票】用户上传小票/发票/收据图片时用此工具。前置:必须先调用get_skill获取规范,再调用get_parse_prompt获取凭证和解析模板。用法:mcporter call 柴米AI记账.save_receipt _flowmate="SOLINWANG..." store="超市" totalAmount=156.5 items="[...]"。注意:文字描述请用save_expense/save_income',
282
354
  inputSchema: {
283
355
  type: 'object',
284
356
  properties: {
@@ -310,17 +382,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
310
382
  amount: { type: 'number', description: '【必填】金额,必须等于 price × quantity' },
311
383
  weight: { type: 'string', description: '重量' },
312
384
  marketPrice: { type: 'string', description: '【必填】单价数值,根据unit计算,如unit=元/500g,则marketPrice=金额÷重量(g)×500' },
313
- category: { type: 'string', description: '【必填】主分类(如:蔬菜、肉类、水果、水产,常见超市购物分类)' },
314
- subCategory: { type: 'string', description: '子分类(如:叶菜类、根茎类、猪肉类,超市细分类目),推荐填写' },
385
+ // 【改造】商品分类字段
386
+ productCategory: { type: 'string', description: '【必填】商品一级分类(如:蔬菜、肉类、水果、日化用品)' },
387
+ productSubCategory: { type: 'string', description: '商品二级分类(如:叶菜类、根茎类、家居清洁)' },
388
+ // 【改造】记账分类字段
389
+ category: { type: 'string', description: '【必填】记账分类(43个标准分类之一,如:蔬菜、购物、餐饮)' },
315
390
  },
316
- required: ['name', 'amount', 'price', 'quantity', 'unit', 'marketPrice', 'category'],
391
+ required: ['name', 'amount', 'price', 'quantity', 'unit', 'marketPrice', 'productCategory', 'category'],
317
392
  },
318
393
  },
319
394
  agentType: { type: 'string', description: '【必填】Agent类型:claude-desktop、cursor、openclaw、workbuddy、trae' },
320
395
  apiProvider: { type: 'string', description: '【必填】AI服务提供商:anthropic、openai、doubao、aliyun' },
321
396
  rawInput: { type: 'string', description: '【必填】用户的原始输入内容' },
397
+ _flowmate: { type: 'string', description: '【必填】流程凭证,从get_parse_prompt获取的myflowmate值' },
322
398
  },
323
- required: ['store', 'items', 'storeCategory', 'storeSubCategory', 'agentType', 'apiProvider', 'rawInput'],
399
+ required: ['store', 'items', 'storeCategory', 'storeSubCategory', 'agentType', 'apiProvider', 'rawInput', '_flowmate'],
324
400
  },
325
401
  },
326
402
  {
@@ -335,7 +411,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
335
411
  startDate: { type: 'string', description: '开始日期(ISO格式,如:2026-04-01)' },
336
412
  endDate: { type: 'string', description: '结束日期(ISO格式,如:2026-04-30)' },
337
413
  category: { type: 'string', description: '按分类筛选(如:餐饮、食品、交通)' },
338
- subCategory: { type: 'string', description: '按子分类筛选(如:叶菜类、猪肉类)' },
339
414
  store: { type: 'string', description: '按商家名称筛选' },
340
415
  source: { type: 'string', description: '按来源筛选(如:mcp_txt_expense、mcp_receipt)' },
341
416
  keyword: { type: 'string', description: '按商品名称关键词搜索' },
@@ -406,26 +481,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
406
481
  },
407
482
  {
408
483
  name: 'save_income',
409
- description: '【文本记账-收入】用户提及收入关键词(工资、红包、转账、退款等)时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_income _requestToken="RT..." name="红包" amount=100 category="礼金" rawInput="收红包100元"',
484
+ description: '【文本记账-收入】用户提及收入关键词(工资、红包、转账、退款等)时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取凭证和解析模板。用法:mcporter call 柴米AI记账.save_income _flowmate="SOLINWANG..." name="红包" amount=100 category="红包" rawInput="收红包100元"',
410
485
  inputSchema: {
411
486
  type: 'object',
412
487
  properties: {
413
488
  name: { type: 'string', description: '收入来源(如:工资、奖金、红包)(必填)' },
414
489
  amount: { type: 'number', description: '收入金额(必填)' },
415
- category: { type: 'string', description: '(必填)收入分类(如:工资、奖金、投资)' },
490
+ category: { type: 'string', description: '【必填】收入分类(8个标准分类之一:工资、兼职、奖金、红包、退款、理财、转账、其他)' },
416
491
  store: { type: 'string', description: '付款方(如:公司名称)(可选)' },
417
492
  date: { type: 'number', description: '【必填】收入时间(毫秒级时间戳,13位数字。必须根据实际年月日时分秒计算,不允许复制示例数字)' },
418
493
  note: { type: 'string', description: '备注(可选)' },
419
494
  agentType: { type: 'string', description: '【必填】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
420
495
  apiProvider: { type: 'string', description: '【必填】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
421
496
  rawInput: { type: 'string', description: '【必填】用户的原始输入内容,用于记录用户原始请求' },
497
+ _flowmate: { type: 'string', description: '【必填】流程凭证,从get_text_parse_prompt获取的myflowmate值' },
422
498
  },
423
- required: ['name', 'amount', 'category', 'agentType', 'apiProvider', 'rawInput'],
499
+ required: ['name', 'amount', 'category', 'agentType', 'apiProvider', 'rawInput', '_flowmate'],
424
500
  },
425
501
  },
426
502
  {
427
503
  name: 'get_text_parse_prompt',
428
- description: '【不调用100%失败】获取文字记账解析的Prompt模板和requestToken。⚠️ 文字/语音记账必须先调用此工具获取Token,AI解析时必须返回_requestToken字段,否则save_expense/save_income会报MISSING_TOKEN错误!',
504
+ description: '【不调用100%失败】获取文字记账解析的Prompt模板和流程凭证。⚠️ 文字/语音记账必须先调用此工具获取凭证,AI解析时必须返回_flowmate字段,否则save_expense/save_income会报MISSING_TOKEN错误!',
429
505
  inputSchema: {
430
506
  type: 'object',
431
507
  properties: {
@@ -436,7 +512,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
436
512
  },
437
513
  {
438
514
  name: 'get_parse_prompt',
439
- description: '【不调用100%失败】获取小票图片解析的Prompt模板和requestToken。⚠️ 小票记账必须先调用此工具获取Token,AI解析时必须返回_requestToken字段,否则save_receipt会报MISSING_TOKEN错误!',
515
+ description: '【不调用100%失败】获取小票图片解析的Prompt模板和流程凭证。⚠️ 小票记账必须先调用此工具获取凭证,AI解析时必须返回_flowmate字段,否则save_receipt会报MISSING_TOKEN错误!',
440
516
  inputSchema: {
441
517
  type: 'object',
442
518
  properties: {
@@ -483,6 +559,7 @@ let lastRefreshTime = 0;
483
559
 
484
560
  async function getToken() {
485
561
  // 【优化1】优先检查内存中的 token 是否有效
562
+ const cachedToken = getCachedToken();
486
563
  if (cachedToken && tokenExpireTime > Date.now() + 5 * 60 * 1000) {
487
564
  if (Date.now() - lastRefreshTime < TOKEN_REFRESH_INTERVAL) {
488
565
  return cachedToken;
@@ -495,13 +572,13 @@ async function getToken() {
495
572
  if (existingToken && existingToken.accessToken && existingToken.expiresAt) {
496
573
  const expiresAt = new Date(existingToken.expiresAt).getTime();
497
574
  if (expiresAt > Date.now() + 5 * 60 * 1000) {
498
- // Token 有效,直接使用
499
- cachedToken = existingToken.accessToken;
575
+ // Token 有效,直接使用(加密存储)
576
+ setCachedToken(existingToken.accessToken);
500
577
  tokenExpireTime = expiresAt;
501
578
  authState.isAuthorized = true;
502
579
  lastRefreshTime = Date.now();
503
580
  console.error('✅ 已从文件加载有效 Token,无需重新授权');
504
- return cachedToken;
581
+ return getCachedToken();
505
582
  } else {
506
583
  console.error('⏰ 保存的 Token 已过期,需要重新授权');
507
584
  }
@@ -525,8 +602,8 @@ async function getToken() {
525
602
  try {
526
603
  const token = await oauthManager.pollForTokenOnce(savedAuthState.deviceCode);
527
604
  if (token) {
528
- // 授权成功
529
- cachedToken = token.accessToken;
605
+ // 授权成功(加密存储)
606
+ setCachedToken(token.accessToken);
530
607
  tokenExpireTime = token.expiresAt;
531
608
  authState.isAuthorized = true;
532
609
  await oauthManager.tokenStorage.save(token);
@@ -536,7 +613,7 @@ async function getToken() {
536
613
  await oauthManager.clearAuthState();
537
614
  console.error('✅ 授权成功!可以继续使用记账功能');
538
615
  // 返回特殊标记,让上层知道这是刚完成授权的情况
539
- throw new Error(`AUTH_JUST_COMPLETED:${cachedToken}`);
616
+ throw new Error(`AUTH_JUST_COMPLETED:${getCachedToken()}`);
540
617
  }
541
618
  // 用户还未授权,返回同一个验证码
542
619
  authState.deviceCode = savedAuthState.deviceCode;
@@ -566,11 +643,11 @@ async function getToken() {
566
643
 
567
644
  try {
568
645
  const oauthToken = await oauthManager.getValidToken();
569
- cachedToken = oauthToken.accessToken;
646
+ setCachedToken(oauthToken.accessToken);
570
647
  tokenExpireTime = oauthToken.expiresAt;
571
648
  lastRefreshTime = Date.now();
572
649
  await oauthManager.clearAuthState();
573
- return cachedToken;
650
+ return getCachedToken();
574
651
  } catch (err) {
575
652
  // Token 获取失败,可能需要重新授权
576
653
  authState.isAuthorized = false;
@@ -670,8 +747,15 @@ async function callMcpHubWithLogging(tool, params, token, traceId, startTime, os
670
747
  // logSource: 'mcp'
671
748
  // });
672
749
 
750
+ // 添加 agentType 和 apiProvider 到 params,确保传递到云函数
751
+ const paramsWithMeta = {
752
+ ...params,
753
+ agentType: agentType || '',
754
+ apiProvider: apiProvider || '',
755
+ };
756
+
673
757
  try {
674
- const result = await callMcpHub(tool, params, token, traceId, osInfo);
758
+ const result = await callMcpHub(tool, paramsWithMeta, token, traceId, osInfo);
675
759
 
676
760
  // 记录云函数调用成功(已停用:MCP调用日志待迁移到独立云函数)
677
761
  // logMcpCall({
@@ -1037,6 +1121,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1037
1121
  }
1038
1122
  }
1039
1123
 
1124
+ // 添加 agentType 和 apiProvider 到 processedArgs,确保 convertParams 能获取到
1125
+ processedArgs.agentType = agentType || '';
1126
+ processedArgs.apiProvider = apiProvider || '';
1127
+
1040
1128
  const mcpParams = convertParams('save_expense', processedArgs);
1041
1129
  result = await callMcpHubWithLogging('addExpense', mcpParams, token, traceId, startTime, osInfo, agentType, apiProvider);
1042
1130
 
@@ -1152,7 +1240,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1152
1240
  break;
1153
1241
  }
1154
1242
 
1155
- // 检查 items 中每个商品的必填字段(包括 category)
1243
+ // 检查 items 中每个商品的必填字段(包括分类字段)
1156
1244
  const invalidItems = [];
1157
1245
  processedArgs.items.forEach((item, index) => {
1158
1246
  const itemMissingFields = [];
@@ -1160,7 +1248,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1160
1248
  if (!item.hasOwnProperty('amount') || item.amount === undefined) itemMissingFields.push('amount');
1161
1249
  if (!item.hasOwnProperty('price') || item.price === undefined) itemMissingFields.push('price');
1162
1250
  if (!item.hasOwnProperty('quantity') || item.quantity === undefined) itemMissingFields.push('quantity');
1163
- if (!item.hasOwnProperty('category') || item.category === '') itemMissingFields.push('category');
1251
+ // 【改造】检查新字段 productCategory category
1252
+ if (!item.hasOwnProperty('productCategory') || item.productCategory === '') {
1253
+ itemMissingFields.push('productCategory');
1254
+ }
1255
+ if (!item.hasOwnProperty('category') || item.category === '') {
1256
+ itemMissingFields.push('category');
1257
+ }
1164
1258
 
1165
1259
  if (itemMissingFields.length > 0) {
1166
1260
  invalidItems.push(`商品${index + 1}(${item.name || '未命名'})缺少字段: ${itemMissingFields.join(', ')}`);
@@ -1170,10 +1264,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1170
1264
  if (invalidItems.length > 0) {
1171
1265
  result = {
1172
1266
  success: false,
1173
- error: `商品数据不完整:${invalidItems.join('; ')}。每个商品必须包含:name, amount, price, quantity, category`,
1267
+ error: `商品数据不完整:${invalidItems.join('; ')}。每个商品必须包含:name, amount, price, quantity, productCategory, category`,
1174
1268
  code: 400
1175
1269
  };
1176
- userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米AI记账 MCP Skill\n2. 确保每个商品包含完整的5个字段:name, amount, price, quantity, category\n3. category 是记账的基本信息,不能为空`;
1270
+ userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米AI记账 MCP Skill\n2. 确保每个商品包含完整的6个字段:name, amount, price, quantity, productCategory, category\n3. productCategory 是商品分类,category 是记账分类,都是必填项`;
1177
1271
  break;
1178
1272
  }
1179
1273
 
@@ -1316,6 +1410,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1316
1410
  };
1317
1411
  }
1318
1412
 
1413
+ // 添加 agentType 和 apiProvider 到 processedArgs,确保 convertParams 能获取到
1414
+ processedArgs.agentType = agentType || '';
1415
+ processedArgs.apiProvider = apiProvider || '';
1416
+
1319
1417
  const mcpParams = convertParams('save_receipt', processedArgs);
1320
1418
  result = await callMcpHubWithLogging('addReceipt', mcpParams, token, traceId, startTime, osInfo, agentType, apiProvider);
1321
1419
 
@@ -1563,6 +1661,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1563
1661
  }
1564
1662
  }
1565
1663
 
1664
+ // 添加 agentType 和 apiProvider 到 processedArgs,确保 convertParams 能获取到
1665
+ processedArgs.agentType = agentType || '';
1666
+ processedArgs.apiProvider = apiProvider || '';
1667
+
1566
1668
  const mcpParams = convertParams('save_income', processedArgs);
1567
1669
  result = await callMcpHubWithLogging('addIncome', mcpParams, token, traceId, startTime, osInfo, agentType, apiProvider);
1568
1670
 
@@ -1603,8 +1705,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1603
1705
  systemPrompt: promptResult.data.systemPrompt,
1604
1706
  userPromptTemplate: promptResult.data.userPromptTemplate,
1605
1707
  examples: promptResult.data.examples,
1606
- requestToken: promptResult.data.requestToken,
1607
- instructions: '请将 systemPrompt 作为 system message,把小票图片作为 user message,调用你的大模型进行解析。解析完成后,调用 save_receipt 工具保存结果。⚠️ 注意:AI解析结果必须包含 _requestToken 字段!'
1708
+ myflowmate: promptResult.data.myflowmate,
1709
+ instructions: '请将 systemPrompt 作为 system message,把小票图片作为 user message,调用你的大模型进行解析。解析完成后,调用 save_receipt 工具保存结果。⚠️ 注意:AI解析结果必须包含 _flowmate 字段!'
1608
1710
  }
1609
1711
  };
1610
1712
  // 【新增】返回模板位置信息(Server只返回原始数据,Agent使用模板渲染)
@@ -1631,7 +1733,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1631
1733
  prompt: promptResult.data.prompt,
1632
1734
  currentDate: promptResult.data.currentDate,
1633
1735
  currentTime: promptResult.data.currentTime,
1634
- requestToken: promptResult.data.requestToken,
1736
+ myflowmate: promptResult.data.myflowmate,
1635
1737
  instructions: promptResult.data.instructions
1636
1738
  }
1637
1739
  };
@@ -2085,8 +2187,9 @@ function convertParams(toolName, args) {
2085
2187
  amount: amount,
2086
2188
  price: amount,
2087
2189
  quantity: 1,
2190
+ productCategory: sanitizeString(args.productCategory, 50) || '其他',
2191
+ productSubCategory: sanitizeString(args.productSubCategory, 50) || '',
2088
2192
  category: sanitizeString(args.category, 50) || '其他',
2089
- subCategory: sanitizeString(args.subCategory, 50) || '',
2090
2193
  unit: sanitizeString(args.unit, 20) || '',
2091
2194
  weight: '',
2092
2195
  marketPrice: sanitizeString(args.marketPrice, 50) || '',
@@ -2099,7 +2202,7 @@ function convertParams(toolName, args) {
2099
2202
  apiProvider: sanitizeString(args.apiProvider, 50) || '',
2100
2203
  mcpVersion: MCP_VERSION,
2101
2204
  source: 'mcp_txt_expense',
2102
- _requestToken: args._requestToken,
2205
+ _flowmate: args._flowmate,
2103
2206
  };
2104
2207
  }
2105
2208
 
@@ -2123,20 +2226,21 @@ function convertParams(toolName, args) {
2123
2226
  const marketPrice = sanitizeString(item.marketPrice, 50) || calculateMarketPrice(amount, weight);
2124
2227
 
2125
2228
  return {
2126
- itemIndex: index,
2127
- name: sanitizeString(item.name, 100),
2128
- originalName: sanitizeString(item.originalName, 200) || sanitizeString(item.name, 100),
2129
- amount: amount,
2130
- category: sanitizeString(item.category, 50) || '其他',
2131
- subCategory: sanitizeString(item.subCategory, 50) || '',
2132
- transactionType: sanitizeString(item.transactionType, 50) || 'expense',
2133
- price: validateAmount(item.price),
2134
- quantity: item.quantity ? String(item.quantity).substring(0, 20) : '1',
2135
- unit: sanitizeString(item.unit, 20),
2136
- weight: weight,
2137
- marketPrice: marketPrice,
2138
- note: sanitizeString(item.note, 500),
2139
- };
2229
+ itemIndex: index,
2230
+ name: sanitizeString(item.name, 100),
2231
+ originalName: sanitizeString(item.originalName, 200) || sanitizeString(item.name, 100),
2232
+ amount: amount,
2233
+ productCategory: sanitizeString(item.productCategory, 50) || '其他',
2234
+ productSubCategory: sanitizeString(item.productSubCategory, 50) || '',
2235
+ category: sanitizeString(item.category, 50) || '其他',
2236
+ transactionType: sanitizeString(item.transactionType, 50) || 'expense',
2237
+ price: validateAmount(item.price),
2238
+ quantity: item.quantity ? String(item.quantity).substring(0, 20) : '1',
2239
+ unit: sanitizeString(item.unit, 20),
2240
+ weight: weight,
2241
+ marketPrice: marketPrice,
2242
+ note: sanitizeString(item.note, 500),
2243
+ };
2140
2244
  }),
2141
2245
  store: sanitizeString(args.store, 100),
2142
2246
  receiptNo: sanitizeString(args.receiptNo, 50),
@@ -2156,7 +2260,7 @@ function convertParams(toolName, args) {
2156
2260
  source: 'mcp_receipt',
2157
2261
  storeCategory: sanitizeString(args.storeCategory, 50) || '其他',
2158
2262
  storeSubCategory: sanitizeString(args.storeSubCategory, 50) || '其他',
2159
- _requestToken: args._requestToken,
2263
+ _flowmate: args._flowmate,
2160
2264
  };
2161
2265
  }
2162
2266
 
@@ -2198,7 +2302,7 @@ function convertParams(toolName, args) {
2198
2302
  apiProvider: sanitizeString(args.apiProvider, 50) || '',
2199
2303
  mcpVersion: MCP_VERSION,
2200
2304
  source: 'mcp_txt_income',
2201
- _requestToken: args._requestToken,
2305
+ _flowmate: args._flowmate,
2202
2306
  };
2203
2307
 
2204
2308
  default:
@@ -2329,6 +2433,7 @@ async function fetchAgentNameFromCloud(deviceCode) {
2329
2433
  }
2330
2434
 
2331
2435
  async function main() {
2436
+ await initConfig();
2332
2437
  await initOAuthManager();
2333
2438
 
2334
2439
  const transport = new StdioServerTransport();
@@ -2399,8 +2504,8 @@ async function pollForAuthInBackground(deviceCode, interval) {
2399
2504
  const token = await oauthManager.pollForTokenOnce(deviceCode);
2400
2505
 
2401
2506
  if (token) {
2402
- // 授权成功
2403
- cachedToken = token.accessToken;
2507
+ // 授权成功(加密存储)
2508
+ setCachedToken(token.accessToken);
2404
2509
  tokenExpireTime = token.expiresAt;
2405
2510
  authState.isAuthorized = true;
2406
2511
  authState.isWaiting = false;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 分类标准化映射器
3
+ * 将 Agent 提取的关键词映射到标准分类
4
+ * 确保分类一致性,便于后续统计
5
+ */
6
+
7
+ // 标准分类列表
8
+ const STANDARD_CATEGORIES = [
9
+ '餐饮', '购物', '交通', '蔬菜', '水果', '零食', '运动', '通讯', '服饰', '美容',
10
+ '住房', '孩子', '长辈', '社交', '旅行', '烟酒', '数码', '汽车', '医疗', '办公',
11
+ '学习', '宠物', '礼金', '亲友', '日用', '休闲娱乐', '维修', '居家', '饮品', '鲜花',
12
+ '追星', '首饰', '借出', '保险', '网络虚拟', '生活缴费', '转账', '书籍', '捐赠', '彩票', '快递', '其他'
13
+ ];
14
+
15
+ // 关键词到
@@ -0,0 +1,53 @@
1
+ const crypto = require('crypto');
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 16;
5
+ const TAG_LENGTH = 16;
6
+ const SALT = 'chaimi-mcp-salt-2026';
7
+
8
+ function encrypt(text, key) {
9
+ let derivedKey;
10
+ if (typeof key === 'string') {
11
+ derivedKey = crypto.scryptSync(key, SALT, 32);
12
+ } else {
13
+ derivedKey = key;
14
+ }
15
+
16
+ const iv = crypto.randomBytes(IV_LENGTH);
17
+ const cipher = crypto.createCipheriv(ALGORITHM, derivedKey, iv);
18
+
19
+ let encrypted = cipher.update(text, 'utf8', 'hex');
20
+ encrypted += cipher.final('hex');
21
+
22
+ const authTag = cipher.getAuthTag();
23
+
24
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
25
+ }
26
+
27
+ function decrypt(encryptedData, key) {
28
+ const parts = encryptedData.split(':');
29
+ if (parts.length !== 3) {
30
+ throw new Error('无效的加密格式');
31
+ }
32
+
33
+ let derivedKey;
34
+ if (typeof key === 'string') {
35
+ derivedKey = crypto.scryptSync(key, SALT, 32);
36
+ } else {
37
+ derivedKey = key;
38
+ }
39
+
40
+ const iv = Buffer.from(parts[0], 'hex');
41
+ const authTag = Buffer.from(parts[1], 'hex');
42
+ const encrypted = Buffer.from(parts[2], 'hex');
43
+
44
+ const decipher = crypto.createDecipheriv(ALGORITHM, derivedKey, iv);
45
+ decipher.setAuthTag(authTag);
46
+
47
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
48
+ decrypted += decipher.final('utf8');
49
+
50
+ return decrypted;
51
+ }
52
+
53
+ module.exports = { encrypt, decrypt };
@@ -0,0 +1,38 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const KEY_FILE = path.join(os.homedir(), '.chaimi-keep', '.mcp-secret-key');
7
+ const KEY_SIZE = 32; // 256 bits
8
+
9
+ function getOrCreateSecretKey() {
10
+ // 1. 优先使用环境变量
11
+ if (process.env.MCP_SECRET_KEY) {
12
+ return Buffer.from(process.env.MCP_SECRET_KEY, 'hex');
13
+ }
14
+
15
+ // 2. 尝试从文件加载
16
+ if (fs.existsSync(KEY_FILE)) {
17
+ const keyHex = fs.readFileSync(KEY_FILE, 'utf8').trim();
18
+ return Buffer.from(keyHex, 'hex');
19
+ }
20
+
21
+ // 3. 自动生成新密钥
22
+ const newKey = crypto.randomBytes(KEY_SIZE);
23
+ const keyHex = newKey.toString('hex');
24
+
25
+ // 确保目录存在
26
+ const keyDir = path.dirname(KEY_FILE);
27
+ if (!fs.existsSync(keyDir)) {
28
+ fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
29
+ }
30
+
31
+ // 写入文件,权限 0600
32
+ fs.writeFileSync(KEY_FILE, keyHex, { mode: 0o600 });
33
+
34
+ console.error('✅ 已自动生成安全密钥');
35
+ return newKey;
36
+ }
37
+
38
+ module.exports = { getOrCreateSecretKey, KEY_FILE };