chaimi-keep-mcp 3.3.3-beta.1 → 3.3.3-beta.10
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 +24 -1
- package/SKILL.md +45 -15
- package/bin/cli.js +4 -4
- package/bin/get-auth-code.js +4 -2
- package/oauth.js +11 -3
- package/package.json +2 -2
- package/references/authentication.md +16 -16
- package/references/response-templates.md +67 -6
- package/references/troubleshooting.md +6 -6
- package/server.js +171 -29
- package/utils/config.js +229 -0
- package/utils/machine-id.js +10 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# 1. 安装 MCPorter(WorkBuddy/OpenClaw 等 OpenClaw 架构的 Agent 必需)
|
|
24
24
|
npm install -g mcporter
|
|
25
25
|
|
|
26
|
-
# 2.
|
|
26
|
+
# 2. 安装柴米AI记账 MCP
|
|
27
27
|
npm install -g chaimi-keep-mcp
|
|
28
28
|
|
|
29
29
|
# 3. 启动(自动配置 mcporter)
|
|
@@ -89,6 +89,29 @@ export MCP_PROMPT_URL="你的Prompt服务地址"
|
|
|
89
89
|
|
|
90
90
|
## Changelog
|
|
91
91
|
|
|
92
|
+
### v3.3.3-beta.8 (2026-04-30)
|
|
93
|
+
- **变更** 配置存储路径迁移 - 从 `~/.mcporter/` 迁移到 `~/.chaimi-keep/`,避免与其他 MCP 工具冲突
|
|
94
|
+
- **优化** 安全权限 - 新增目录权限控制(0700),敏感文件权限控制(0600)
|
|
95
|
+
|
|
96
|
+
### v3.3.3-beta.7 (2026-04-29)
|
|
97
|
+
- **优化** 回复模板 - 新增"请确认信息"提示,时间移到核心数据区,添加类型变量
|
|
98
|
+
- **优化** 空值处理 - 商家为空时不显示,timeNote 仅异常时显示
|
|
99
|
+
- **修复** save_income - 添加 date 兜底逻辑,不传时间时使用当前时间
|
|
100
|
+
- **修复** save_expense - 优化 timeNote 显示逻辑
|
|
101
|
+
|
|
102
|
+
### v3.3.3-beta.6 (2026-04-29)
|
|
103
|
+
- **优化** 品牌统一 - 所有"柴米记账"改为"柴米AI记账"
|
|
104
|
+
- **优化** 工具描述 - 重写三个记账工具描述,包含完整调用示例和格式要求
|
|
105
|
+
|
|
106
|
+
### v3.3.3-beta.4 (2026-04-29)
|
|
107
|
+
- **优化** 收入记账时间解析统一 - save_income 同样使用 time_description,修复 AI 计算时间戳错误
|
|
108
|
+
|
|
109
|
+
### v3.3.3-beta.3 (2026-04-29)
|
|
110
|
+
- **优化** 文本记账时间解析 - 使用 time_description 替代直接计算时间戳,解决 AI 计算时间错误的问题
|
|
111
|
+
|
|
112
|
+
### v3.3.3-beta.2 (2026-04-29)
|
|
113
|
+
- **修复** 时间入库错误 - 解决消费时间被错误替换为当前时间的问题
|
|
114
|
+
|
|
92
115
|
### v3.3.1-beta.0 (2026-04-27)
|
|
93
116
|
- **优化** 简化工具描述 - save_expense/save_receipt/save_income 描述更简洁,不暴露业务逻辑
|
|
94
117
|
- **优化** 参数类型自动转换 - 支持 CLI 工具的字符串传参自动转为数字
|
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ updated: "2026-04-27"
|
|
|
9
9
|
|
|
10
10
|
# 柴米AI记账 Skill
|
|
11
11
|
|
|
12
|
-
> ✨
|
|
12
|
+
> ✨ 柴米AI记账超级美学版 - 让记账变成享受
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -67,7 +67,7 @@ updated: "2026-04-27"
|
|
|
67
67
|
## 三、文档引用关系
|
|
68
68
|
|
|
69
69
|
```
|
|
70
|
-
|
|
70
|
+
用户使用柴米AI记账
|
|
71
71
|
↓
|
|
72
72
|
chaimi-keep-mcp ⭐ 本Skill
|
|
73
73
|
├── 记账成功 → 使用回复模板(见七、回复规范)
|
|
@@ -154,6 +154,17 @@ chaimi-keep-mcp ⭐ 本Skill
|
|
|
154
154
|
- ✅ 返回 `_templateHint` 提示使用哪个模板
|
|
155
155
|
- ✅ 返回 `_templateVariables` 提供模板变量数据
|
|
156
156
|
|
|
157
|
+
**回复简洁原则(重要)**:
|
|
158
|
+
- ❌ **不要输出记账过程** - 不要告诉用户"第1步确认信息、第2步解析时间..."等流程细节
|
|
159
|
+
- ❌ **不要刷屏** - 记账是高频操作,简洁回复是对用户的尊重
|
|
160
|
+
- ✅ **只输出最终结果** - 直接使用模板渲染记账成功信息
|
|
161
|
+
- ✅ **除非用户要求** - 仅当用户主动询问"你是怎么记账的"时才说明流程
|
|
162
|
+
|
|
163
|
+
**空值处理原则**:
|
|
164
|
+
- 模板变量中,如果某个维度的值为空(null/undefined/空字符串),Agent 应跳过该行不显示
|
|
165
|
+
- 必填字段(金额、类型、商品、分类)必须显示
|
|
166
|
+
- 可选字段(商家、时间、洞察)为空时不显示
|
|
167
|
+
|
|
157
168
|
### 7.2 模板文件位置
|
|
158
169
|
|
|
159
170
|
**主模板文件**: `references/response-templates.md`
|
|
@@ -175,14 +186,16 @@ references/
|
|
|
175
186
|
| `get_receipt_list` | `references/response-templates.md` | 2.5 查询结果模板 | 小票列表查询 |
|
|
176
187
|
| `get_statistics` | `references/response-templates.md` | 2.6 统计结果模板 | 消费统计分析 |
|
|
177
188
|
|
|
178
|
-
### 7.4
|
|
189
|
+
### 7.4 回复模板使用流程(Agent 内部操作,不要告诉用户)
|
|
190
|
+
|
|
191
|
+
> ⚠️ **注意**:这是 Agent 内部的处理流程,**不要输出给用户看**,只输出第 5 步的渲染结果。
|
|
179
192
|
|
|
180
193
|
```
|
|
181
194
|
1. 调用工具 → 返回原始数据 + _templateLocation + _templateHint + _templateVariables
|
|
182
195
|
2. 读取 _templateLocation 指定的模板文件
|
|
183
196
|
3. 根据 _templateHint 选择对应模板章节
|
|
184
197
|
4. 使用 _templateVariables 填充模板变量
|
|
185
|
-
5.
|
|
198
|
+
5. 渲染最终回复给用户(只展示这一步的结果)
|
|
186
199
|
```
|
|
187
200
|
|
|
188
201
|
### 7.5 5层视觉结构
|
|
@@ -191,6 +204,8 @@ references/
|
|
|
191
204
|
【第1层:成功标识】✅ 「Agent名称」已帮您记账成功
|
|
192
205
|
【第2层:分隔强调】═══════════════
|
|
193
206
|
【第3层:核心数据】💰 金额:¥xx.xx
|
|
207
|
+
📊 类型:支出/收入
|
|
208
|
+
═══════════════
|
|
194
209
|
【第4层:详细信息】
|
|
195
210
|
📦 商品:xxx
|
|
196
211
|
🏷️ 分类:xxx
|
|
@@ -207,23 +222,37 @@ references/
|
|
|
207
222
|
|
|
208
223
|
### 7.6 标准成功模板
|
|
209
224
|
|
|
225
|
+
> 📄 **完整模板定义** → 见 `references/response-templates.md` 第 2.1 节
|
|
226
|
+
>
|
|
227
|
+
> **模板要点:**
|
|
228
|
+
> - 第1层:成功标识 `✅ 「{agentName}」已帮您记账成功`
|
|
229
|
+
> - 第2层:分隔线 `═══════════════`
|
|
230
|
+
> - 第3层:核心数据(金额、类型、时间 + 🔴🔴确认提示)
|
|
231
|
+
> - 第4层:分隔线
|
|
232
|
+
> - 第5层:详细信息(商品、分类、商家)
|
|
233
|
+
> - 第6层:消费洞察(可选)
|
|
234
|
+
> - 第7层:底部祝福 + 品牌信息
|
|
235
|
+
|
|
236
|
+
**渲染示例:**
|
|
210
237
|
```markdown
|
|
211
|
-
✅
|
|
238
|
+
✅ 「你的小可爱」已帮您记账成功
|
|
212
239
|
═══════════════
|
|
213
|
-
|
|
240
|
+
🔴🔴 请确认以下信息是否正确
|
|
241
|
+
💰 金额:¥35.00
|
|
242
|
+
📊 类型:支出
|
|
243
|
+
🕐 时间:2026-04-24 12:30
|
|
214
244
|
═══════════════
|
|
215
245
|
|
|
216
|
-
📦
|
|
217
|
-
🏷️
|
|
218
|
-
🏪
|
|
219
|
-
🕐 时间:{日期时间}
|
|
246
|
+
📦 商品:午餐
|
|
247
|
+
🏷️ 分类:餐饮
|
|
248
|
+
🏪 商家:麦当劳
|
|
220
249
|
|
|
221
|
-
💡
|
|
250
|
+
💡 消费洞察:本月餐饮支出占比30%,建议控制
|
|
222
251
|
|
|
223
252
|
───────────────
|
|
224
|
-
🎉
|
|
253
|
+
🎉 美食为梦想充电,继续向前冲!🍚
|
|
225
254
|
柴米AI记账
|
|
226
|
-
chaimi-keep-mcp
|
|
255
|
+
chaimi-keep-mcp v3.2.0
|
|
227
256
|
───────────────
|
|
228
257
|
```
|
|
229
258
|
|
|
@@ -234,6 +263,7 @@ references/
|
|
|
234
263
|
| {agentName} | Agent显示名称 | 「你的小可爱」 |
|
|
235
264
|
| {商品名} | 商品或商家名称 | 午餐、麦当劳 |
|
|
236
265
|
| {金额} | 金额(带¥符号) | ¥35.00 |
|
|
266
|
+
| {类型} | 支出/收入类型 | 支出、收入 |
|
|
237
267
|
| {分类} | 消费分类 | 餐饮 |
|
|
238
268
|
| {商家} | 商家名称(可选) | 麦当劳 |
|
|
239
269
|
| {日期时间} | 格式化时间 | 2026-04-24 12:30 |
|
|
@@ -249,7 +279,7 @@ references/
|
|
|
249
279
|
|
|
250
280
|
## 八、记忆口诀
|
|
251
281
|
|
|
252
|
-
>
|
|
282
|
+
> **柴米AI记账超美学,五层结构要牢记。**
|
|
253
283
|
> **成功标识在顶部,金额分隔最醒目。**
|
|
254
284
|
> **商品分类商家时,底部祝福加品牌。**
|
|
255
285
|
> **场景路由先匹配,工具速查再执行。**
|
|
@@ -263,7 +293,7 @@ references/
|
|
|
263
293
|
|
|
264
294
|
| 错误 | 现象 | 解决方案 | 参考文档 |
|
|
265
295
|
|:-----|:-----|:---------|:---------|
|
|
266
|
-
| **授权失败** | "未找到有效授权" | 执行 `mcporter auth
|
|
296
|
+
| **授权失败** | "未找到有效授权" | 执行 `mcporter auth 柴米AI记账` 重新授权 | troubleshooting.md |
|
|
267
297
|
| **分类不明** | "无法识别分类" | 明确说分类:"午餐35元餐饮" | api-reference.md |
|
|
268
298
|
| **网络超时** | "请求超时" | 检查网络,稍后重试 | troubleshooting.md |
|
|
269
299
|
|
package/bin/cli.js
CHANGED
|
@@ -161,8 +161,8 @@ function installToAgent(agent) {
|
|
|
161
161
|
config.mcpServers = {};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
165
|
-
if (config.mcpServers['
|
|
164
|
+
// 检查是否已存在柴米AI记账配置
|
|
165
|
+
if (config.mcpServers['柴米AI记账']) {
|
|
166
166
|
return {
|
|
167
167
|
success: true,
|
|
168
168
|
action: 'skipped',
|
|
@@ -171,8 +171,8 @@ function installToAgent(agent) {
|
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
//
|
|
175
|
-
config.mcpServers['
|
|
174
|
+
// 添加柴米AI记账配置
|
|
175
|
+
config.mcpServers['柴米AI记账'] = JSON.parse(JSON.stringify(DEFAULT_CHAIMI_CONFIG));
|
|
176
176
|
|
|
177
177
|
// 写入配置
|
|
178
178
|
writeJSON(agent.configPath, config);
|
package/bin/get-auth-code.js
CHANGED
|
@@ -20,8 +20,9 @@ const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || 'https://cloud1-2gfe5jhjef06b
|
|
|
20
20
|
|
|
21
21
|
// 检查是否已有有效 Token
|
|
22
22
|
async function checkExistingToken() {
|
|
23
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
23
24
|
const tokenStorage = new FileTokenStorage(
|
|
24
|
-
path.join(os.homedir(), '.
|
|
25
|
+
path.join(os.homedir(), '.chaimi-keep', 'oauth-token.json')
|
|
25
26
|
);
|
|
26
27
|
|
|
27
28
|
try {
|
|
@@ -37,8 +38,9 @@ async function checkExistingToken() {
|
|
|
37
38
|
|
|
38
39
|
// 获取新的授权码
|
|
39
40
|
async function getAuthCode() {
|
|
41
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
40
42
|
const tokenStorage = new FileTokenStorage(
|
|
41
|
-
path.join(os.homedir(), '.
|
|
43
|
+
path.join(os.homedir(), '.chaimi-keep', 'oauth-token.json')
|
|
42
44
|
);
|
|
43
45
|
|
|
44
46
|
const oauthManager = new OAuthManager({
|
package/oauth.js
CHANGED
|
@@ -311,10 +311,14 @@ class OAuthManager {
|
|
|
311
311
|
try {
|
|
312
312
|
const fs = require('fs').promises;
|
|
313
313
|
const path = require('path');
|
|
314
|
-
const
|
|
314
|
+
const os = require('os');
|
|
315
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
316
|
+
const stateFile = path.join(os.homedir(), '.chaimi-keep', 'auth-state.json');
|
|
315
317
|
|
|
316
318
|
const dir = path.dirname(stateFile);
|
|
317
319
|
await fs.mkdir(dir, { recursive: true });
|
|
320
|
+
// 设置目录权限 0700
|
|
321
|
+
await fs.chmod(dir, 0o700).catch(() => {});
|
|
318
322
|
|
|
319
323
|
await fs.writeFile(
|
|
320
324
|
stateFile,
|
|
@@ -329,7 +333,9 @@ class OAuthManager {
|
|
|
329
333
|
try {
|
|
330
334
|
const fs = require('fs').promises;
|
|
331
335
|
const path = require('path');
|
|
332
|
-
const
|
|
336
|
+
const os = require('os');
|
|
337
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
338
|
+
const stateFile = path.join(os.homedir(), '.chaimi-keep', 'auth-state.json');
|
|
333
339
|
|
|
334
340
|
const data = await fs.readFile(stateFile, 'utf8');
|
|
335
341
|
return JSON.parse(data);
|
|
@@ -345,7 +351,9 @@ class OAuthManager {
|
|
|
345
351
|
try {
|
|
346
352
|
const fs = require('fs').promises;
|
|
347
353
|
const path = require('path');
|
|
348
|
-
const
|
|
354
|
+
const os = require('os');
|
|
355
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
356
|
+
const stateFile = path.join(os.homedir(), '.chaimi-keep', 'auth-state.json');
|
|
349
357
|
await fs.unlink(stateFile);
|
|
350
358
|
} catch (err) {
|
|
351
359
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chaimi-keep-mcp",
|
|
3
|
-
"version": "3.3.3-beta.
|
|
3
|
+
"version": "3.3.3-beta.10",
|
|
4
4
|
"description": "柴米AI记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"AI记账",
|
|
41
41
|
"model-context-protocol"
|
|
42
42
|
],
|
|
43
|
-
"author": "
|
|
43
|
+
"author": "柴米AI记账团队,songyangx@gmail.com",
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "chaimi-keep-authentication"
|
|
3
3
|
description: 柴米AI记账授权流程详解
|
|
4
|
-
version: "1.0.
|
|
5
|
-
updated: "2026-04-
|
|
4
|
+
version: "1.0.1"
|
|
5
|
+
updated: "2026-04-30"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# 柴米AI记账授权流程
|
|
@@ -58,7 +58,7 @@ updated: "2026-04-25"
|
|
|
58
58
|
|
|
59
59
|
1. **启动授权流程**
|
|
60
60
|
```bash
|
|
61
|
-
mcporter auth
|
|
61
|
+
mcporter auth 柴米AI记账
|
|
62
62
|
```
|
|
63
63
|
或
|
|
64
64
|
```bash
|
|
@@ -68,14 +68,14 @@ updated: "2026-04-25"
|
|
|
68
68
|
2. **查看验证码**
|
|
69
69
|
终端会显示:
|
|
70
70
|
```
|
|
71
|
-
🔐
|
|
71
|
+
🔐 柴米AI记账授权
|
|
72
72
|
═══════════════════════════
|
|
73
73
|
📱 验证码:GGCY-AC84
|
|
74
74
|
⏰ 有效期:5分钟
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
3. **小程序确认**
|
|
78
|
-
- 打开"
|
|
78
|
+
- 打开"柴米AI记账"小程序
|
|
79
79
|
- 点击"我的" → "AI 助手绑定"
|
|
80
80
|
- 输入验证码
|
|
81
81
|
- 点击确认
|
|
@@ -90,15 +90,15 @@ updated: "2026-04-25"
|
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
# 重新授权
|
|
93
|
-
mcporter auth
|
|
93
|
+
mcporter auth 柴米AI记账
|
|
94
94
|
|
|
95
95
|
# 强制清除缓存后重新授权
|
|
96
|
-
mcporter auth
|
|
96
|
+
mcporter auth 柴米AI记账 --reset
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
**然后:**
|
|
100
100
|
1. 查看终端显示的验证码
|
|
101
|
-
2. 打开"
|
|
101
|
+
2. 打开"柴米AI记账"小程序
|
|
102
102
|
3. 进入"我的" → "AI 助手绑定"
|
|
103
103
|
4. 输入验证码完成授权
|
|
104
104
|
|
|
@@ -124,7 +124,7 @@ mcporter auth 柴米记账 --reset
|
|
|
124
124
|
```
|
|
125
125
|
或
|
|
126
126
|
```bash
|
|
127
|
-
mcporter auth
|
|
127
|
+
mcporter auth 柴米AI记账
|
|
128
128
|
```
|
|
129
129
|
2. 查看终端显示的验证码
|
|
130
130
|
3. 在小程序"我的"→"AI 助手绑定"中输入验证码
|
|
@@ -134,12 +134,12 @@ mcporter auth 柴米记账 --reset
|
|
|
134
134
|
|
|
135
135
|
**macOS/Linux:**
|
|
136
136
|
```bash
|
|
137
|
-
rm ~/.
|
|
137
|
+
rm ~/.chaimi-keep/oauth-token.json
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
**Windows:**
|
|
141
141
|
```cmd
|
|
142
|
-
del %USERPROFILE%\.
|
|
142
|
+
del %USERPROFILE%\.chaimi-keep\oauth-token.json
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
然后完全退出并重新启动 Claude/Cursor
|
|
@@ -157,22 +157,22 @@ del %USERPROFILE%\.mcporter\oauth-token.json
|
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
159
|
# 查看 Token 文件是否存在
|
|
160
|
-
cat ~/.
|
|
160
|
+
cat ~/.chaimi-keep/oauth-token.json
|
|
161
161
|
|
|
162
162
|
# 或尝试调用工具测试
|
|
163
|
-
mcporter call
|
|
163
|
+
mcporter call 柴米AI记账.get_skill
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
**Token 文件位置:**
|
|
167
167
|
```
|
|
168
|
-
~/.
|
|
168
|
+
~/.chaimi-keep/oauth-token.json
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
### 3.2 Token刷新
|
|
172
172
|
|
|
173
173
|
Token 过期后需要重新授权:
|
|
174
174
|
```bash
|
|
175
|
-
mcporter auth
|
|
175
|
+
mcporter auth 柴米AI记账
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
---
|
|
@@ -192,7 +192,7 @@ mcporter auth 柴米记账
|
|
|
192
192
|
|
|
193
193
|
```bash
|
|
194
194
|
# 强制清除缓存后重新授权
|
|
195
|
-
mcporter auth
|
|
195
|
+
mcporter auth 柴米AI记账 --reset
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
**场景:**
|
|
@@ -47,6 +47,7 @@ updated: "2026-04-25"
|
|
|
47
47
|
|:-----|:------|:-----|
|
|
48
48
|
| 成功标识 | ✅ | 操作成功 |
|
|
49
49
|
| 金额 | 💰 | 核心数据 |
|
|
50
|
+
| 类型 | 📊 | 支出/收入 |
|
|
50
51
|
| 商品 | 📦 | 物品 |
|
|
51
52
|
| 分类 | 🏷️ | 标签分类 |
|
|
52
53
|
| 商家 | 🏪 | 商店 |
|
|
@@ -54,6 +55,63 @@ updated: "2026-04-25"
|
|
|
54
55
|
| 祝福 | 🎉 | 庆祝 |
|
|
55
56
|
| 警示 | ⚠️ | 需要关注 |
|
|
56
57
|
|
|
58
|
+
### 1.4 空值处理规范(重要)
|
|
59
|
+
|
|
60
|
+
**规则**:详细信息区(第4层)的维度,如果值为空,则不展示该行。
|
|
61
|
+
|
|
62
|
+
**处理逻辑**:
|
|
63
|
+
```
|
|
64
|
+
渲染前检查每个字段:
|
|
65
|
+
- 如果字段值存在且不为空 → 显示该行
|
|
66
|
+
- 如果字段值为空/null/undefined → 跳过该行
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**示例**:
|
|
70
|
+
```markdown
|
|
71
|
+
// 完整数据
|
|
72
|
+
📦 商品:午餐
|
|
73
|
+
🏷️ 分类:餐饮
|
|
74
|
+
🏪 商家:麦当劳
|
|
75
|
+
🕐 时间:2026-04-24 12:30
|
|
76
|
+
|
|
77
|
+
// 商家为空时
|
|
78
|
+
📦 商品:午餐
|
|
79
|
+
🏷️ 分类:餐饮
|
|
80
|
+
🕐 时间:2026-04-24 12:30
|
|
81
|
+
(🏪 商家:这一行不显示)
|
|
82
|
+
|
|
83
|
+
// 只有商品和金额时
|
|
84
|
+
📦 商品:午餐
|
|
85
|
+
(其他维度为空,都不显示)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**必填 vs 可选字段**:
|
|
89
|
+
| 字段 | 是否必填 | 为空时处理 |
|
|
90
|
+
|:-----|:---------|:-----------|
|
|
91
|
+
| 商品名 | ✅ 必填 | 必须显示 |
|
|
92
|
+
| 分类 | ✅ 必填 | 必须显示 |
|
|
93
|
+
| 类型 | ✅ 必填 | 必须显示 |
|
|
94
|
+
| 金额 | ✅ 必填 | 必须显示 |
|
|
95
|
+
| 商家 | ❌ 可选 | 为空不显示 |
|
|
96
|
+
| 时间 | ❌ 可选 | 为空不显示 |
|
|
97
|
+
| 洞察 | ❌ 可选 | 为空不显示 |
|
|
98
|
+
|
|
99
|
+
**规则2:避免重复显示**
|
|
100
|
+
- 如果商品名和分类相同 → 只显示分类行(避免重复)
|
|
101
|
+
- 如果商品名和分类不同 → 两行都显示
|
|
102
|
+
|
|
103
|
+
**示例**:
|
|
104
|
+
```markdown
|
|
105
|
+
// 用户说"交通 12"(商品名=分类)
|
|
106
|
+
🏷️ 分类:交通
|
|
107
|
+
(📦 商品:交通 不显示,避免重复)
|
|
108
|
+
|
|
109
|
+
// 用户说"午餐 35"(商品名=午餐,分类=餐饮)
|
|
110
|
+
📦 商品:午餐
|
|
111
|
+
🏷️ 分类:餐饮
|
|
112
|
+
(两行都显示)
|
|
113
|
+
```
|
|
114
|
+
|
|
57
115
|
---
|
|
58
116
|
|
|
59
117
|
## 二、标准模板
|
|
@@ -65,18 +123,20 @@ updated: "2026-04-25"
|
|
|
65
123
|
```markdown
|
|
66
124
|
✅ 「{agentName}」已帮您记账成功
|
|
67
125
|
═══════════════
|
|
126
|
+
🔴🔴 请确认以下信息是否正确
|
|
68
127
|
💰 金额:¥{金额}
|
|
128
|
+
📊 类型:{类型}
|
|
129
|
+
🕐 时间:{日期时间}
|
|
69
130
|
═══════════════
|
|
70
131
|
|
|
71
132
|
📦 商品:{商品名}
|
|
72
133
|
🏷️ 分类:{分类}
|
|
73
134
|
🏪 商家:{商家}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
💡 消费洞察:{洞察内容}(可选)
|
|
135
|
+
💡 消费洞察:{洞察内容}
|
|
136
|
+
⏰ 时间说明:{timeNote}
|
|
77
137
|
|
|
78
138
|
───────────────
|
|
79
|
-
🎉 {
|
|
139
|
+
🎉 {正能量情绪词}!
|
|
80
140
|
柴米AI记账
|
|
81
141
|
chaimi-keep-mcp v{版本号}
|
|
82
142
|
───────────────
|
|
@@ -87,14 +147,15 @@ updated: "2026-04-25"
|
|
|
87
147
|
```markdown
|
|
88
148
|
✅ 「你的小可爱」已帮您记账成功
|
|
89
149
|
═══════════════
|
|
150
|
+
🔴🔴 请确认以下信息是否正确
|
|
90
151
|
💰 金额:¥35.00
|
|
152
|
+
📊 类型:支出
|
|
153
|
+
🕐 时间:2026-04-24 12:30
|
|
91
154
|
═══════════════
|
|
92
155
|
|
|
93
156
|
📦 商品:午餐
|
|
94
157
|
🏷️ 分类:餐饮
|
|
95
158
|
🏪 商家:麦当劳
|
|
96
|
-
🕐 时间:2026-04-24 12:30
|
|
97
|
-
|
|
98
159
|
💡 消费洞察:本月餐饮支出占比30%,建议控制
|
|
99
160
|
|
|
100
161
|
───────────────
|
|
@@ -38,7 +38,7 @@ updated: "2026-04-25"
|
|
|
38
38
|
- 授权被撤销
|
|
39
39
|
|
|
40
40
|
**解决方案:**
|
|
41
|
-
1. 执行 `mcporter auth
|
|
41
|
+
1. 执行 `mcporter auth 柴米AI记账`
|
|
42
42
|
2. 查看终端显示的验证码
|
|
43
43
|
3. 在小程序"我的"→"AI 助手绑定"中输入验证码
|
|
44
44
|
4. 重新尝试记账
|
|
@@ -58,10 +58,10 @@ updated: "2026-04-25"
|
|
|
58
58
|
**解决方案:**
|
|
59
59
|
```bash
|
|
60
60
|
# 重新授权
|
|
61
|
-
mcporter auth
|
|
61
|
+
mcporter auth 柴米AI记账
|
|
62
62
|
|
|
63
63
|
# 或强制清除缓存后重新授权
|
|
64
|
-
mcporter auth
|
|
64
|
+
mcporter auth 柴米AI记账 --reset
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### 1.3 验证码错误
|
|
@@ -76,7 +76,7 @@ mcporter auth 柴米记账 --reset
|
|
|
76
76
|
- 验证码超过5分钟有效期
|
|
77
77
|
|
|
78
78
|
**解决方案:**
|
|
79
|
-
1. 重新执行 `mcporter auth
|
|
79
|
+
1. 重新执行 `mcporter auth 柴米AI记账` 生成新验证码
|
|
80
80
|
2. 在5分钟内在小程序"我的"→"AI 助手绑定"中输入
|
|
81
81
|
3. 注意区分大小写
|
|
82
82
|
|
|
@@ -218,7 +218,7 @@ mcporter auth 柴米记账 --reset
|
|
|
218
218
|
|
|
219
219
|
**解决方案:**
|
|
220
220
|
1. 等待几分钟后重试
|
|
221
|
-
2.
|
|
221
|
+
2. 检查柴米AI记账官方公告
|
|
222
222
|
3. 提交反馈:调用 `submit_feedback`
|
|
223
223
|
|
|
224
224
|
---
|
|
@@ -269,7 +269,7 @@ cat /mcp-server-local/references/response-templates.md
|
|
|
269
269
|
chaimi-keep-mcp --version
|
|
270
270
|
|
|
271
271
|
# 重新授权
|
|
272
|
-
mcporter auth
|
|
272
|
+
mcporter auth 柴米AI记账
|
|
273
273
|
|
|
274
274
|
# 测试记账连接
|
|
275
275
|
curl -X POST https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp \
|
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();
|
|
@@ -39,6 +42,9 @@ const MCP_VERSION = getVersion();
|
|
|
39
42
|
// 导入 OAuth 模块
|
|
40
43
|
const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
41
44
|
|
|
45
|
+
// 导入配置管理工具
|
|
46
|
+
const { configManager } = require('./utils/config.js');
|
|
47
|
+
|
|
42
48
|
// 导入校验工具
|
|
43
49
|
const { validateDate } = require('./utils/validators.js');
|
|
44
50
|
|
|
@@ -167,9 +173,9 @@ let oauthManager = null;
|
|
|
167
173
|
|
|
168
174
|
// 初始化 OAuth 管理器
|
|
169
175
|
async function initOAuthManager() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
176
|
+
// 使用新的配置路径 ~/.chaimi-keep/
|
|
177
|
+
const tokenPath = configManager.getPath('oauth-token.json');
|
|
178
|
+
const tokenStorage = new FileTokenStorage(tokenPath);
|
|
173
179
|
|
|
174
180
|
oauthManager = new OAuthManager({
|
|
175
181
|
mcpOAuthUrl: MCP_OAUTH_URL,
|
|
@@ -243,7 +249,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
243
249
|
tools: [
|
|
244
250
|
{
|
|
245
251
|
name: 'get_skill',
|
|
246
|
-
description: '
|
|
252
|
+
description: '【记账前必须调用】获取柴米AI记账最新 Skill 定义。每次记账前必须先调用此工具,获取当前版本的完整 Skill 文档,然后严格按照 Skill 定义执行。如果不调用此工具直接记账,操作将被拒绝',
|
|
247
253
|
inputSchema: {
|
|
248
254
|
type: 'object',
|
|
249
255
|
properties: {
|
|
@@ -253,7 +259,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
253
259
|
},
|
|
254
260
|
{
|
|
255
261
|
name: 'save_expense',
|
|
256
|
-
description: '
|
|
262
|
+
description: '【文本记账-支出】用户提及支出/消费时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_expense _requestToken="RT..." name="午餐" amount=35 category="餐饮" rawInput="午餐35元"。注意:上传图片请用save_receipt',
|
|
257
263
|
inputSchema: {
|
|
258
264
|
type: 'object',
|
|
259
265
|
properties: {
|
|
@@ -275,7 +281,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
275
281
|
},
|
|
276
282
|
{
|
|
277
283
|
name: 'save_receipt',
|
|
278
|
-
description: '
|
|
284
|
+
description: '【图片记账-小票】用户上传小票/发票/收据图片时用此工具。前置:必须先调用get_skill获取规范,再调用get_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_receipt _requestToken="RT..." store="超市" totalAmount=156.5 items="[...]"。注意:文字描述请用save_expense/save_income',
|
|
279
285
|
inputSchema: {
|
|
280
286
|
type: 'object',
|
|
281
287
|
properties: {
|
|
@@ -403,7 +409,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
403
409
|
},
|
|
404
410
|
{
|
|
405
411
|
name: 'save_income',
|
|
406
|
-
description: '
|
|
412
|
+
description: '【文本记账-收入】用户提及收入关键词(工资、红包、转账、退款等)时用此工具。前置:必须先调用get_skill获取规范,再调用get_text_parse_prompt获取Token和解析模板。用法:mcporter call 柴米AI记账.save_income _requestToken="RT..." name="红包" amount=100 category="礼金" rawInput="收红包100元"',
|
|
407
413
|
inputSchema: {
|
|
408
414
|
type: 'object',
|
|
409
415
|
properties: {
|
|
@@ -667,8 +673,15 @@ async function callMcpHubWithLogging(tool, params, token, traceId, startTime, os
|
|
|
667
673
|
// logSource: 'mcp'
|
|
668
674
|
// });
|
|
669
675
|
|
|
676
|
+
// 添加 agentType 和 apiProvider 到 params,确保传递到云函数
|
|
677
|
+
const paramsWithMeta = {
|
|
678
|
+
...params,
|
|
679
|
+
agentType: agentType || '',
|
|
680
|
+
apiProvider: apiProvider || '',
|
|
681
|
+
};
|
|
682
|
+
|
|
670
683
|
try {
|
|
671
|
-
const result = await callMcpHub(tool,
|
|
684
|
+
const result = await callMcpHub(tool, paramsWithMeta, token, traceId, osInfo);
|
|
672
685
|
|
|
673
686
|
// 记录云函数调用成功(已停用:MCP调用日志待迁移到独立云函数)
|
|
674
687
|
// logMcpCall({
|
|
@@ -977,7 +990,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
977
990
|
success: false,
|
|
978
991
|
error: `必填字段缺失:${missingFields.join(', ')}`,
|
|
979
992
|
code: 400,
|
|
980
|
-
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list
|
|
993
|
+
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list 柴米AI记账 --schema 查看完整参数结构',
|
|
981
994
|
debug: {
|
|
982
995
|
missing: missingFields,
|
|
983
996
|
received: Object.keys(processedArgs),
|
|
@@ -999,6 +1012,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
999
1012
|
break;
|
|
1000
1013
|
}
|
|
1001
1014
|
|
|
1015
|
+
// 【新增】处理 time_description(文本记账新时间格式)
|
|
1016
|
+
let timeNote = null;
|
|
1017
|
+
if (processedArgs.time_description) {
|
|
1018
|
+
const parsedDate = parseTimeDescription(processedArgs.time_description, Date.now());
|
|
1019
|
+
processedArgs.date = parsedDate;
|
|
1020
|
+
// 正常解析时不显示 timeNote,只有兜底或异常时才显示
|
|
1021
|
+
console.log(`[save_expense] 时间描述解析: ${processedArgs.time_description} → ${parsedDate} (${new Date(parsedDate).toLocaleString('zh-CN')})`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1002
1024
|
// P1: 日期合理性检查
|
|
1003
1025
|
if (processedArgs.date) {
|
|
1004
1026
|
const validation = validateDate(processedArgs.date, '消费日期');
|
|
@@ -1011,6 +1033,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1011
1033
|
userMessage = validation.message;
|
|
1012
1034
|
break;
|
|
1013
1035
|
}
|
|
1036
|
+
|
|
1037
|
+
// 【新增】校验时间是否在合理范围内(±1年)
|
|
1038
|
+
const dateObj = new Date(processedArgs.date);
|
|
1039
|
+
const now = new Date();
|
|
1040
|
+
const oneYearAgo = new Date(now.getFullYear() - 1, 0, 1);
|
|
1041
|
+
const oneYearLater = new Date(now.getFullYear() + 1, 11, 31);
|
|
1042
|
+
|
|
1043
|
+
if (dateObj < oneYearAgo || dateObj > oneYearLater) {
|
|
1044
|
+
console.warn(`[save_expense] 时间异常:${processedArgs.date} (${dateObj.toLocaleString('zh-CN')}),使用当前时间`);
|
|
1045
|
+
processedArgs.date = Date.now();
|
|
1046
|
+
timeNote = '⏰ 时间说明:检测到时间异常,已使用当前时间';
|
|
1047
|
+
}
|
|
1014
1048
|
}
|
|
1015
1049
|
|
|
1016
1050
|
const mcpParams = convertParams('save_expense', processedArgs);
|
|
@@ -1050,14 +1084,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1050
1084
|
result._templateHint = '请使用references/response-templates.md中的2.1标准成功模板渲染回复';
|
|
1051
1085
|
result._templateVariables = {
|
|
1052
1086
|
agentName: await getAgentName(),
|
|
1087
|
+
类型: '支出',
|
|
1053
1088
|
商品名: displayName,
|
|
1054
|
-
商家: displayStore ||
|
|
1089
|
+
商家: displayStore || null,
|
|
1055
1090
|
金额: displayAmount,
|
|
1056
1091
|
分类: result.data?.categoryName || displayCategory,
|
|
1057
|
-
日期: result.data?.date ? new Date(result.data.date).
|
|
1092
|
+
日期: result.data?.date ? new Date(result.data.date).toLocaleString('zh-CN') : new Date().toLocaleString('zh-CN'),
|
|
1058
1093
|
正能量祝福语: friendlyEnding,
|
|
1059
1094
|
insightsText,
|
|
1060
|
-
achievementsText
|
|
1095
|
+
achievementsText,
|
|
1096
|
+
timeNote: timeNote || null
|
|
1061
1097
|
};
|
|
1062
1098
|
}
|
|
1063
1099
|
break;
|
|
@@ -1079,7 +1115,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1079
1115
|
success: false,
|
|
1080
1116
|
error: `必填字段缺失:${missingFields.join(', ')}`,
|
|
1081
1117
|
code: 400,
|
|
1082
|
-
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list
|
|
1118
|
+
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list 柴米AI记账 --schema 查看完整参数结构',
|
|
1083
1119
|
debug: {
|
|
1084
1120
|
missing: missingFields,
|
|
1085
1121
|
received: Object.keys(processedArgs),
|
|
@@ -1147,7 +1183,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1147
1183
|
error: `商品数据不完整:${invalidItems.join('; ')}。每个商品必须包含:name, amount, price, quantity, category`,
|
|
1148
1184
|
code: 400
|
|
1149
1185
|
};
|
|
1150
|
-
userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1.
|
|
1186
|
+
userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米AI记账 MCP Skill\n2. 确保每个商品包含完整的5个字段:name, amount, price, quantity, category\n3. category 是记账的基本信息,不能为空`;
|
|
1151
1187
|
break;
|
|
1152
1188
|
}
|
|
1153
1189
|
|
|
@@ -1459,8 +1495,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1459
1495
|
}
|
|
1460
1496
|
|
|
1461
1497
|
case 'save_income': {
|
|
1462
|
-
// P0: 数据完整性检查 -
|
|
1463
|
-
const requiredFields = ['name', 'amount', 'category', '
|
|
1498
|
+
// P0: 数据完整性检查 - 必填字段验证(date 改为 time_description,不强制检查)
|
|
1499
|
+
const requiredFields = ['name', 'amount', 'category', 'rawInput'];
|
|
1464
1500
|
const missingFields = [];
|
|
1465
1501
|
|
|
1466
1502
|
for (const field of requiredFields) {
|
|
@@ -1474,14 +1510,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1474
1510
|
success: false,
|
|
1475
1511
|
error: `必填字段缺失:${missingFields.join(', ')}`,
|
|
1476
1512
|
code: 400,
|
|
1477
|
-
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list
|
|
1513
|
+
hint: '解决方法:1.查看 SKILL.md 中"六.5 如何查看工具参数" 2.运行 mcporter list 柴米AI记账 --schema 查看完整参数结构',
|
|
1478
1514
|
debug: {
|
|
1479
1515
|
missing: missingFields,
|
|
1480
1516
|
received: Object.keys(processedArgs),
|
|
1481
1517
|
docs: '调用 get_skill() 获取详细使用指南'
|
|
1482
1518
|
}
|
|
1483
1519
|
};
|
|
1484
|
-
userMessage = `❌ 收入记录失败\n\n错误:缺少必填字段:${missingFields.join(', ')}\n\n💡 解决方案:\n1. 请检查是否从用户输入中提取了所有信息\n2. 确保传递以下字段:name(收入来源)、amount(金额)、category(分类)、
|
|
1520
|
+
userMessage = `❌ 收入记录失败\n\n错误:缺少必填字段:${missingFields.join(', ')}\n\n💡 解决方案:\n1. 请检查是否从用户输入中提取了所有信息\n2. 确保传递以下字段:name(收入来源)、amount(金额)、category(分类)、rawInput(原始输入)\n3. 参考 SKILL.md 中的"调用前检查清单"`;
|
|
1485
1521
|
break;
|
|
1486
1522
|
}
|
|
1487
1523
|
|
|
@@ -1496,6 +1532,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1496
1532
|
break;
|
|
1497
1533
|
}
|
|
1498
1534
|
|
|
1535
|
+
// 【新增】处理 time_description(收入记账新时间格式)
|
|
1536
|
+
let timeNote = null;
|
|
1537
|
+
if (processedArgs.time_description) {
|
|
1538
|
+
const parsedDate = parseTimeDescription(processedArgs.time_description, Date.now());
|
|
1539
|
+
processedArgs.date = parsedDate;
|
|
1540
|
+
// 正常解析时不显示 timeNote,只有兜底或异常时才显示
|
|
1541
|
+
console.log(`[save_income] 时间描述解析: ${processedArgs.time_description} → ${parsedDate} (${new Date(parsedDate).toLocaleString('zh-CN')})`);
|
|
1542
|
+
}
|
|
1543
|
+
// 【新增】兜底:如果既没传 time_description 也没传 date,使用当前时间
|
|
1544
|
+
else if (!processedArgs.date) {
|
|
1545
|
+
processedArgs.date = Date.now();
|
|
1546
|
+
timeNote = '⏰ 时间说明:未指定时间,使用当前时间';
|
|
1547
|
+
console.log(`[save_income] 未传时间参数,使用当前时间: ${processedArgs.date}`);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1499
1550
|
// P1: 日期合理性检查
|
|
1500
1551
|
if (processedArgs.date) {
|
|
1501
1552
|
const validation = validateDate(processedArgs.date, '收入日期');
|
|
@@ -1508,6 +1559,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1508
1559
|
userMessage = validation.message;
|
|
1509
1560
|
break;
|
|
1510
1561
|
}
|
|
1562
|
+
|
|
1563
|
+
// 【新增】校验时间是否在合理范围内(±1年)
|
|
1564
|
+
const dateObj = new Date(processedArgs.date);
|
|
1565
|
+
const now = new Date();
|
|
1566
|
+
const oneYearAgo = new Date(now.getFullYear() - 1, 0, 1);
|
|
1567
|
+
const oneYearLater = new Date(now.getFullYear() + 1, 11, 31);
|
|
1568
|
+
|
|
1569
|
+
if (dateObj < oneYearAgo || dateObj > oneYearLater) {
|
|
1570
|
+
console.warn(`[save_income] 时间异常:${processedArgs.date} (${dateObj.toLocaleString('zh-CN')}),使用当前时间`);
|
|
1571
|
+
processedArgs.date = Date.now();
|
|
1572
|
+
timeNote = '⏰ 时间说明:检测到时间异常,已使用当前时间';
|
|
1573
|
+
}
|
|
1511
1574
|
}
|
|
1512
1575
|
|
|
1513
1576
|
const mcpParams = convertParams('save_income', processedArgs);
|
|
@@ -1524,12 +1587,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1524
1587
|
result._templateHint = '请使用references/response-templates.md中的2.1标准成功模板渲染回复';
|
|
1525
1588
|
result._templateVariables = {
|
|
1526
1589
|
agentName: await getAgentName(),
|
|
1590
|
+
类型: '收入',
|
|
1527
1591
|
商品名: displayName,
|
|
1528
|
-
商家: displayStore,
|
|
1592
|
+
商家: displayStore || null,
|
|
1529
1593
|
金额: displayAmount,
|
|
1530
1594
|
分类: result.data?.categoryName || displayCategory,
|
|
1531
|
-
日期: result.data?.date ? new Date(result.data.date).
|
|
1532
|
-
正能量祝福语: '入账顺利!💰'
|
|
1595
|
+
日期: result.data?.date ? new Date(result.data.date).toLocaleString('zh-CN') : new Date().toLocaleString('zh-CN'),
|
|
1596
|
+
正能量祝福语: '入账顺利!💰',
|
|
1597
|
+
timeNote: timeNote || null
|
|
1533
1598
|
};
|
|
1534
1599
|
}
|
|
1535
1600
|
break;
|
|
@@ -1659,7 +1724,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1659
1724
|
content: [
|
|
1660
1725
|
{
|
|
1661
1726
|
type: 'text',
|
|
1662
|
-
text: `⚠️ 【注意】Server 只返回原始数据,不返回格式化消息。\n请使用 _templateLocation 指定的模板自行渲染回复。\n\n---\n📦
|
|
1727
|
+
text: `⚠️ 【注意】Server 只返回原始数据,不返回格式化消息。\n请使用 _templateLocation 指定的模板自行渲染回复。\n\n---\n📦 柴米AI记账 MCP v${MCP_VERSION}\n\n## 完整数据\n\`\`\`json\n${safeStringify(responseData)}\n\`\`\``,
|
|
1663
1728
|
},
|
|
1664
1729
|
],
|
|
1665
1730
|
};
|
|
@@ -1855,13 +1920,7 @@ function formatDateWithTimezone(dateInput) {
|
|
|
1855
1920
|
timestamp = parseInt(dateInput, 10);
|
|
1856
1921
|
}
|
|
1857
1922
|
|
|
1858
|
-
//
|
|
1859
|
-
if (timestamp && timestamp % 1000 === 0 && String(timestamp).length === 13) {
|
|
1860
|
-
console.error(`⚠️ 警告:检测到可能错误的时间戳(秒级转毫秒级):${timestamp},使用当前时间替代`);
|
|
1861
|
-
return Date.now();
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
// 如果是数字时间戳且没问题,直接返回
|
|
1923
|
+
// 如果是数字时间戳,直接返回
|
|
1865
1924
|
if (timestamp) {
|
|
1866
1925
|
return timestamp;
|
|
1867
1926
|
}
|
|
@@ -1899,6 +1958,89 @@ function formatDateWithTimezone(dateInput) {
|
|
|
1899
1958
|
}
|
|
1900
1959
|
}
|
|
1901
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* 解析时间描述为时间戳
|
|
1963
|
+
* @param {string} timeDesc - 时间描述(如 'yesterday', 'today')
|
|
1964
|
+
* @param {number} currentTimestamp - 当前时间戳
|
|
1965
|
+
* @returns {number} 时间戳
|
|
1966
|
+
*/
|
|
1967
|
+
function parseTimeDescription(timeDesc, currentTimestamp) {
|
|
1968
|
+
const now = new Date(currentTimestamp);
|
|
1969
|
+
const year = now.getFullYear();
|
|
1970
|
+
const month = now.getMonth();
|
|
1971
|
+
const day = now.getDate();
|
|
1972
|
+
|
|
1973
|
+
switch(timeDesc) {
|
|
1974
|
+
case 'just_now':
|
|
1975
|
+
case 'today':
|
|
1976
|
+
return currentTimestamp;
|
|
1977
|
+
|
|
1978
|
+
case 'yesterday':
|
|
1979
|
+
return new Date(year, month, day - 1, 12, 0, 0).getTime();
|
|
1980
|
+
|
|
1981
|
+
case 'yesterday_evening':
|
|
1982
|
+
return new Date(year, month, day - 1, 19, 0, 0).getTime();
|
|
1983
|
+
|
|
1984
|
+
case 'this_morning':
|
|
1985
|
+
return new Date(year, month, day, 8, 0, 0).getTime();
|
|
1986
|
+
|
|
1987
|
+
case 'this_noon':
|
|
1988
|
+
return new Date(year, month, day, 12, 0, 0).getTime();
|
|
1989
|
+
|
|
1990
|
+
case 'this_afternoon':
|
|
1991
|
+
return new Date(year, month, day, 14, 0, 0).getTime();
|
|
1992
|
+
|
|
1993
|
+
case 'this_evening':
|
|
1994
|
+
return new Date(year, month, day, 19, 0, 0).getTime();
|
|
1995
|
+
|
|
1996
|
+
default:
|
|
1997
|
+
// 尝试解析具体日期格式:2026-03-15 或 2026-03-15T15:00
|
|
1998
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(timeDesc)) {
|
|
1999
|
+
const [y, m, d] = timeDesc.split('-').map(Number);
|
|
2000
|
+
return new Date(y, m - 1, d, 12, 0, 0).getTime();
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(timeDesc)) {
|
|
2004
|
+
return new Date(timeDesc).getTime();
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// 兜底:返回当前时间
|
|
2008
|
+
return currentTimestamp;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* 获取时间说明文本
|
|
2014
|
+
* @param {string} timeDesc - 时间描述
|
|
2015
|
+
* @returns {string} 时间说明
|
|
2016
|
+
*/
|
|
2017
|
+
function getTimeNote(timeDesc) {
|
|
2018
|
+
const descMap = {
|
|
2019
|
+
'just_now': '刚刚',
|
|
2020
|
+
'today': '今天',
|
|
2021
|
+
'yesterday': '昨天',
|
|
2022
|
+
'yesterday_evening': '昨晚',
|
|
2023
|
+
'this_morning': '今早',
|
|
2024
|
+
'this_noon': '今天中午',
|
|
2025
|
+
'this_afternoon': '今天下午',
|
|
2026
|
+
'this_evening': '今晚'
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
if (descMap[timeDesc]) {
|
|
2030
|
+
return `⏰ 时间说明:系统使用"${descMap[timeDesc]}"作为默认时间`;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
if (timeDesc && timeDesc.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
2034
|
+
return `⏰ 时间说明:记录日期为 ${timeDesc}`;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (timeDesc && timeDesc.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)) {
|
|
2038
|
+
return `⏰ 时间说明:记录精确时间为 ${timeDesc}`;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
return '';
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1902
2044
|
function extractWeightInGrams(weightStr) {
|
|
1903
2045
|
if (!weightStr || typeof weightStr !== 'string') return 0;
|
|
1904
2046
|
|
package/utils/config.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 柴米记账 MCP 配置管理工具
|
|
3
|
+
* 统一管理配置文件存储路径,支持从旧路径迁移
|
|
4
|
+
*
|
|
5
|
+
* 存储位置:~/.chaimi-keep/
|
|
6
|
+
* 旧位置:~/.mcporter/(兼容迁移)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
|
|
13
|
+
// 配置目录名称
|
|
14
|
+
const CONFIG_DIR_NAME = '.chaimi-keep';
|
|
15
|
+
const LEGACY_DIR_NAME = '.mcporter';
|
|
16
|
+
|
|
17
|
+
class ConfigManager {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.configDir = path.join(os.homedir(), CONFIG_DIR_NAME);
|
|
20
|
+
this.legacyDir = path.join(os.homedir(), LEGACY_DIR_NAME);
|
|
21
|
+
this.migrationChecked = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取配置文件路径
|
|
26
|
+
* @param {string} filename - 文件名
|
|
27
|
+
* @returns {string} 完整路径
|
|
28
|
+
*/
|
|
29
|
+
getPath(filename) {
|
|
30
|
+
return path.join(this.configDir, filename);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取旧配置文件路径
|
|
35
|
+
* @param {string} filename - 文件名
|
|
36
|
+
* @returns {string} 完整路径
|
|
37
|
+
*/
|
|
38
|
+
getLegacyPath(filename) {
|
|
39
|
+
return path.join(this.legacyDir, filename);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 确保配置目录存在,并设置正确权限
|
|
44
|
+
*/
|
|
45
|
+
async ensureDir() {
|
|
46
|
+
try {
|
|
47
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
48
|
+
// 设置目录权限 0700(仅用户可读写执行)
|
|
49
|
+
await fs.chmod(this.configDir, 0o700);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// 如果权限设置失败,继续执行
|
|
52
|
+
if (err.code !== 'EEXIST') {
|
|
53
|
+
console.error('创建配置目录失败:', err.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 检查文件是否存在于新位置
|
|
60
|
+
* @param {string} filename - 文件名
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
async exists(filename) {
|
|
64
|
+
try {
|
|
65
|
+
await fs.access(this.getPath(filename));
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 检查文件是否存在于旧位置
|
|
74
|
+
* @param {string} filename - 文件名
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
async existsInLegacy(filename) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(this.getLegacyPath(filename));
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从旧位置迁移文件到新位置
|
|
88
|
+
* @param {string} filename - 文件名
|
|
89
|
+
* @returns {boolean} 是否成功迁移
|
|
90
|
+
*/
|
|
91
|
+
async migrateFile(filename) {
|
|
92
|
+
const legacyPath = this.getLegacyPath(filename);
|
|
93
|
+
const newPath = this.getPath(filename);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// 检查旧文件是否存在
|
|
97
|
+
await fs.access(legacyPath);
|
|
98
|
+
|
|
99
|
+
// 确保新目录存在
|
|
100
|
+
await this.ensureDir();
|
|
101
|
+
|
|
102
|
+
// 读取旧文件
|
|
103
|
+
const data = await fs.readFile(legacyPath, 'utf8');
|
|
104
|
+
|
|
105
|
+
// 写入新位置,设置权限 0600
|
|
106
|
+
await fs.writeFile(newPath, data, { mode: 0o600 });
|
|
107
|
+
|
|
108
|
+
// 删除旧文件
|
|
109
|
+
await fs.unlink(legacyPath);
|
|
110
|
+
|
|
111
|
+
console.error(`✅ 已迁移 ${filename} 到新位置`);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err.code !== 'ENOENT') {
|
|
115
|
+
console.error(`迁移 ${filename} 失败:`, err.message);
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 读取配置文件(自动处理迁移)
|
|
123
|
+
* @param {string} filename - 文件名
|
|
124
|
+
* @returns {any} 文件内容(JSON 解析后)或 null
|
|
125
|
+
*/
|
|
126
|
+
async read(filename) {
|
|
127
|
+
// 确保目录存在
|
|
128
|
+
await this.ensureDir();
|
|
129
|
+
|
|
130
|
+
// 优先从新位置读取
|
|
131
|
+
const newPath = this.getPath(filename);
|
|
132
|
+
try {
|
|
133
|
+
const data = await fs.readFile(newPath, 'utf8');
|
|
134
|
+
return JSON.parse(data);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code !== 'ENOENT') {
|
|
137
|
+
console.error(`读取 ${filename} 失败:`, err.message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 新位置不存在,尝试从旧位置迁移
|
|
142
|
+
const migrated = await this.migrateFile(filename);
|
|
143
|
+
if (migrated) {
|
|
144
|
+
try {
|
|
145
|
+
const data = await fs.readFile(newPath, 'utf8');
|
|
146
|
+
return JSON.parse(data);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(`读取迁移后的 ${filename} 失败:`, err.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 写入配置文件
|
|
157
|
+
* @param {string} filename - 文件名
|
|
158
|
+
* @param {any} data - 数据(会被 JSON 序列化)
|
|
159
|
+
* @param {boolean} isSensitive - 是否为敏感文件(设置 0600 权限)
|
|
160
|
+
*/
|
|
161
|
+
async write(filename, data, isSensitive = true) {
|
|
162
|
+
await this.ensureDir();
|
|
163
|
+
const filePath = this.getPath(filename);
|
|
164
|
+
|
|
165
|
+
const options = isSensitive ? { mode: 0o600 } : {};
|
|
166
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 删除配置文件
|
|
171
|
+
* @param {string} filename - 文件名
|
|
172
|
+
*/
|
|
173
|
+
async delete(filename) {
|
|
174
|
+
const filePath = this.getPath(filename);
|
|
175
|
+
try {
|
|
176
|
+
await fs.unlink(filePath);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err.code !== 'ENOENT') {
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 删除旧位置的文件(清理用)
|
|
186
|
+
* @param {string} filename - 文件名
|
|
187
|
+
*/
|
|
188
|
+
async deleteLegacy(filename) {
|
|
189
|
+
const legacyPath = this.getLegacyPath(filename);
|
|
190
|
+
try {
|
|
191
|
+
await fs.unlink(legacyPath);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// 忽略不存在的错误
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取所有相关文件的路径信息
|
|
199
|
+
* @returns {Object} 路径映射
|
|
200
|
+
*/
|
|
201
|
+
getPaths() {
|
|
202
|
+
const files = [
|
|
203
|
+
'oauth-token.json',
|
|
204
|
+
'auth-state.json',
|
|
205
|
+
'agent-name.json',
|
|
206
|
+
'device-code.json',
|
|
207
|
+
'machine-id'
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const result = {};
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
result[file] = {
|
|
213
|
+
new: this.getPath(file),
|
|
214
|
+
legacy: this.getLegacyPath(file)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 导出单例实例
|
|
222
|
+
const configManager = new ConfigManager();
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
ConfigManager,
|
|
226
|
+
configManager,
|
|
227
|
+
CONFIG_DIR_NAME,
|
|
228
|
+
LEGACY_DIR_NAME
|
|
229
|
+
};
|
package/utils/machine-id.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 机器ID和设备指纹管理模块
|
|
3
3
|
*
|
|
4
4
|
* 功能:
|
|
5
|
-
* 1. 生成并管理机器唯一ID(存储在 ~/.
|
|
5
|
+
* 1. 生成并管理机器唯一ID(存储在 ~/.chaimi-keep/machine-id)
|
|
6
6
|
* 2. 生成设备指纹(hash(machineId + agentType))
|
|
7
7
|
*
|
|
8
8
|
* 特点:
|
|
@@ -16,9 +16,16 @@ const path = require('path');
|
|
|
16
16
|
const os = require('os');
|
|
17
17
|
const crypto = require('crypto');
|
|
18
18
|
|
|
19
|
-
// 机器ID
|
|
19
|
+
// 机器ID文件路径(新位置 ~/.chaimi-keep/)
|
|
20
20
|
const MACHINE_ID_FILE = path.join(
|
|
21
|
-
|
|
21
|
+
os.homedir(),
|
|
22
|
+
'.chaimi-keep',
|
|
23
|
+
'machine-id'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// 旧机器ID文件路径(用于迁移)
|
|
27
|
+
const LEGACY_MACHINE_ID_FILE = path.join(
|
|
28
|
+
os.homedir(),
|
|
22
29
|
'.mcporter',
|
|
23
30
|
'machine-id'
|
|
24
31
|
);
|