chaimi-keep-mcp 3.2.0 → 3.2.1-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 +6 -0
- package/oauth.js +3 -2
- package/package.json +1 -1
- package/references/response-templates-test.md +243 -0
- package/references/response-templates.md +4 -4
- package/server.js +31 -6
- package/utils/machine-id.js +165 -0
package/README.md
CHANGED
|
@@ -148,6 +148,12 @@ AI 会自动调用 `save_income` 工具记录收入。
|
|
|
148
148
|
|
|
149
149
|
## 更新日志
|
|
150
150
|
|
|
151
|
+
### v3.2.1-beta.0 (2026-04-26)
|
|
152
|
+
- **新增** 设备指纹机制,防止同一设备重复授权
|
|
153
|
+
- 基于机器ID + Agent类型生成唯一设备指纹
|
|
154
|
+
- 同一设备重复授权时复用已有记录,避免生成多个agentName
|
|
155
|
+
- 支持设备指纹查重,自动识别已授权设备
|
|
156
|
+
|
|
151
157
|
### v3.1.49-beta.2 (2026-04-24)
|
|
152
158
|
- **新增** period 参数支持
|
|
153
159
|
- getStatistics: 支持 this_week/last_week/this_month/last_month 周期筛选
|
package/oauth.js
CHANGED
|
@@ -146,7 +146,7 @@ class OAuthManager {
|
|
|
146
146
|
await execPromise(command);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
async requestDeviceCode(useUrlScheme = false) {
|
|
149
|
+
async requestDeviceCode(useUrlScheme = false, extraParams = {}) {
|
|
150
150
|
const response = await fetch(this.mcpOAuthUrl, {
|
|
151
151
|
method: 'POST',
|
|
152
152
|
headers: {
|
|
@@ -156,7 +156,8 @@ class OAuthManager {
|
|
|
156
156
|
tool: 'deviceCode',
|
|
157
157
|
params: {
|
|
158
158
|
clientId: 'chaihuo-mcp-client',
|
|
159
|
-
useUrlScheme: useUrlScheme
|
|
159
|
+
useUrlScheme: useUrlScheme,
|
|
160
|
+
...extraParams
|
|
160
161
|
}
|
|
161
162
|
})
|
|
162
163
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "chaimi-keep-response-templates-test"
|
|
3
|
+
description: 记账回复模板测试方案 - 供Agent测试不同格式的显示效果
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
updated: "2026-04-26"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 记账回复模板测试方案
|
|
9
|
+
|
|
10
|
+
> 用于测试不同分隔符和排版在手机/PC端的显示效果
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 测试说明
|
|
15
|
+
|
|
16
|
+
将以下各方案的模板复制到Agent的skill中使用,观察:
|
|
17
|
+
1. 换行是否正常
|
|
18
|
+
2. 分隔线显示效果
|
|
19
|
+
3. 底部三行对齐效果
|
|
20
|
+
4. 整体美观度
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 方案1:双分隔线短版(15字符)
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
✅ 「{agentName}」已帮您记账成功
|
|
28
|
+
═══════════════
|
|
29
|
+
💰 金额:¥{金额}
|
|
30
|
+
═══════════════
|
|
31
|
+
|
|
32
|
+
📦 商品:{商品名}
|
|
33
|
+
🏷️ 分类:{分类}
|
|
34
|
+
🏪 商家:{商家}
|
|
35
|
+
🕐 时间:{日期时间}
|
|
36
|
+
|
|
37
|
+
───────────────
|
|
38
|
+
🎉 {正能量祝福语}!
|
|
39
|
+
柴米AI记账
|
|
40
|
+
chaimi-keep-mcp v{版本号}
|
|
41
|
+
───────────────
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**填充示例:**
|
|
45
|
+
```
|
|
46
|
+
✅ 「你的小可爱」已帮您记账成功
|
|
47
|
+
═══════════════
|
|
48
|
+
💰 金额:¥35.00
|
|
49
|
+
═══════════════
|
|
50
|
+
|
|
51
|
+
📦 商品:午餐
|
|
52
|
+
🏷️ 分类:餐饮
|
|
53
|
+
🏪 商家:麦当劳
|
|
54
|
+
🕐 时间:2026-04-24 12:30
|
|
55
|
+
|
|
56
|
+
───────────────
|
|
57
|
+
🎉 美食为梦想充电,继续向前冲!🍚
|
|
58
|
+
柴米AI记账
|
|
59
|
+
chaimi-keep-mcp v3.2.0
|
|
60
|
+
───────────────
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 方案2:单分隔线极简版
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
✅ 「{agentName}」已帮您记账成功
|
|
69
|
+
|
|
70
|
+
💰 金额:¥{金额}
|
|
71
|
+
|
|
72
|
+
📦 商品:{商品名}
|
|
73
|
+
🏷️ 分类:{分类}
|
|
74
|
+
🏪 商家:{商家}
|
|
75
|
+
🕐 时间:{日期时间}
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
🎉 {正能量祝福语}!
|
|
79
|
+
柴米AI记账
|
|
80
|
+
chaimi-keep-mcp v{版本号}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**填充示例:**
|
|
84
|
+
```
|
|
85
|
+
✅ 「你的小可爱」已帮您记账成功
|
|
86
|
+
|
|
87
|
+
💰 金额:¥35.00
|
|
88
|
+
|
|
89
|
+
📦 商品:午餐
|
|
90
|
+
🏷️ 分类:餐饮
|
|
91
|
+
🏪 商家:麦当劳
|
|
92
|
+
🕐 时间:2026-04-24 12:30
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
🎉 美食为梦想充电,继续向前冲!🍚
|
|
96
|
+
柴米AI记账
|
|
97
|
+
chaimi-keep-mcp v3.2.0
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 方案3:信息紧凑单行版(已优化)
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
✅ 「{agentName}」已帮您记账成功 💰¥{金额}
|
|
106
|
+
|
|
107
|
+
📦{商品名} · 🏷️{分类} · 🏪{商家} · 🕐{日期时间}
|
|
108
|
+
|
|
109
|
+
═══════════════
|
|
110
|
+
🎉 {正能量祝福语}!
|
|
111
|
+
柴米AI记账
|
|
112
|
+
chaimi-keep-mcp v{版本号}
|
|
113
|
+
═══════════════
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**填充示例:**
|
|
117
|
+
```
|
|
118
|
+
✅ 「你的小可爱」已帮您记账成功 💰¥35.00
|
|
119
|
+
|
|
120
|
+
📦午餐 · 🏷️餐饮 · 🏪麦当劳 · 🕐12:30
|
|
121
|
+
|
|
122
|
+
═══════════════
|
|
123
|
+
🎉 祝您用餐愉快!
|
|
124
|
+
柴米AI记账
|
|
125
|
+
chaimi-keep-mcp v3.2.0
|
|
126
|
+
═══════════════
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 测试记录表
|
|
132
|
+
|
|
133
|
+
| 方案 | 手机端效果 | PC端效果 | 推荐场景 |
|
|
134
|
+
|:-----|:-----------|:---------|:---------|
|
|
135
|
+
| 方案1:双分隔线短版 | 待测试 | 待测试 | 默认推荐 |
|
|
136
|
+
| 方案2:单分隔线极简版 | 待测试 | 待测试 | 快速记账 |
|
|
137
|
+
| 方案3:信息紧凑单行版 | 待测试 | 待测试 | 空间受限 |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 关键检查点
|
|
142
|
+
|
|
143
|
+
### 1. 分隔线显示
|
|
144
|
+
- [ ] 方案1/5的 `═` 符号是否正常显示
|
|
145
|
+
- [ ] 方案2的 `---` 是否被识别为分隔线
|
|
146
|
+
|
|
147
|
+
### 2. 底部对齐
|
|
148
|
+
- [ ] 底部三行文字是否左对齐(🎉 不占对齐位)
|
|
149
|
+
- [ ] "祝"、"柴"、"c" 三字符是否在同一列
|
|
150
|
+
|
|
151
|
+
### 3. 换行效果
|
|
152
|
+
- [ ] 空行是否被正确保留
|
|
153
|
+
- [ ] 信息项之间是否有适当间距
|
|
154
|
+
|
|
155
|
+
### 4. Emoji显示
|
|
156
|
+
- [ ] ✅ 💰 📦 🏷️ 🏪 🕐 🎉 是否正常显示
|
|
157
|
+
- [ ] 是否有乱码或方框
|
|
158
|
+
|
|
159
|
+
### 5. 方案5特殊检查
|
|
160
|
+
- [ ] 单行信息是否可读
|
|
161
|
+
- [ ] 分隔符 `·` 是否正常显示
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 正能量情绪词库
|
|
166
|
+
|
|
167
|
+
根据消费分类自动匹配正能量结束语:
|
|
168
|
+
|
|
169
|
+
### 餐饮类
|
|
170
|
+
| 场景 | 结束语 |
|
|
171
|
+
|:-----|:-------|
|
|
172
|
+
| 早餐 | 早餐吃得香,元气满满迎朝阳!☀️ |
|
|
173
|
+
| 午餐 | 美食为梦想充电,继续向前冲!🍚 |
|
|
174
|
+
| 晚餐 | 晚餐后放松身心,明天更精彩!🌙 |
|
|
175
|
+
| 零食 | 生活要有仪式感,小确幸也是幸福!😋 |
|
|
176
|
+
| 外卖 | 再忙也要爱自己,好好吃饭!💕 |
|
|
177
|
+
| 聚餐 | 朋友相聚是福气,友谊地久天长!🎉 |
|
|
178
|
+
| 咖啡 | 一杯咖啡提神醒脑,效率倍增!☕ |
|
|
179
|
+
| 奶茶 | 甜蜜相伴,工作生活都精彩!🧋 |
|
|
180
|
+
|
|
181
|
+
### 交通类
|
|
182
|
+
| 场景 | 结束语 |
|
|
183
|
+
|:-----|:-------|
|
|
184
|
+
| 地铁 | 城市动脉连接梦想,未来可期!🚇 |
|
|
185
|
+
| 打车 | 一路顺风,事事顺心!🚗 |
|
|
186
|
+
| 加油 | 加满油向梦想出发,勇往直前!⛽ |
|
|
187
|
+
| 停车 | 车位到手,好运相伴!🅿️ |
|
|
188
|
+
| 公交 | 绿色出行低碳环保,为地球助力!🚌 |
|
|
189
|
+
| 高铁 | 速度与梦想同行,前程似锦!🚄 |
|
|
190
|
+
|
|
191
|
+
### 购物类
|
|
192
|
+
| 场景 | 结束语 |
|
|
193
|
+
|:-----|:-------|
|
|
194
|
+
| 日用品 | 精明消费品质生活,越买越聪明!✨ |
|
|
195
|
+
| 服装 | 新衣新气象,自信每一天!👗 |
|
|
196
|
+
| 电子产品 | 科技赋能,效率翻倍!💻 |
|
|
197
|
+
| 超市 | 满载而归,生活美满!🛒 |
|
|
198
|
+
| 网购 | 好物即将到家,期待美好!📦 |
|
|
199
|
+
| 美妆 | 美丽自信,由内而外散发光彩!💄 |
|
|
200
|
+
|
|
201
|
+
### 居住类
|
|
202
|
+
| 场景 | 结束语 |
|
|
203
|
+
|:-----|:-------|
|
|
204
|
+
| 房租 | 投资未来,房子是温馨的港湾!🏠 |
|
|
205
|
+
| 水电 | 节约环保,绿色生活从我做起!💡 |
|
|
206
|
+
| 物业 | 安心居住,幸福生活有保障!🏢 |
|
|
207
|
+
| 装修 | 亲手打造温馨小窝,成就感满满!🔨 |
|
|
208
|
+
| 家具 | 新家具新生活,每一天都舒心!🛋️ |
|
|
209
|
+
|
|
210
|
+
### 娱乐类
|
|
211
|
+
| 场景 | 结束语 |
|
|
212
|
+
|:-----|:-------|
|
|
213
|
+
| 电影 | 光影世界丰富心灵,生活更精彩!🎬 |
|
|
214
|
+
| 游戏 | 适度娱乐放松身心,工作生活两不误!🎮 |
|
|
215
|
+
| 旅游 | 读万卷书行万里路,拓宽人生视野!✈️ |
|
|
216
|
+
| 健身 | 投资健康是最大的财富,活力满满!💪 |
|
|
217
|
+
| 书籍 | 知识改变命运,智慧点亮人生!📚 |
|
|
218
|
+
|
|
219
|
+
### 收入类
|
|
220
|
+
| 场景 | 结束语 |
|
|
221
|
+
|:-----|:-------|
|
|
222
|
+
| 工资 | 劳动创造价值,努力终有回报!🎉 |
|
|
223
|
+
| 奖金 | 努力被看见,未来更辉煌!💪 |
|
|
224
|
+
| 兼职 | 多元化发展,人生更多精彩!✨ |
|
|
225
|
+
| 理财 | 钱生钱利滚利,财富自由更进一步!📈 |
|
|
226
|
+
| 红包 | 好运连连,福气满满!🧧 |
|
|
227
|
+
|
|
228
|
+
### 通用类
|
|
229
|
+
| 情绪 | 结束语 |
|
|
230
|
+
|:-----|:-------|
|
|
231
|
+
| 鼓励 | 今天的你特别棒,继续发光发热!👍 |
|
|
232
|
+
| 温暖 | 记录生活点滴,美好永留存!📝 |
|
|
233
|
+
| 正能量 | 每一笔记录都是对生活的热爱!💕 |
|
|
234
|
+
| 轻松 | 记账完成,又是充实的一天!✨ |
|
|
235
|
+
| 感恩 | 感恩生活,感恩有你!🙏 |
|
|
236
|
+
| 期待 | 未来可期,一起加油!🌟 |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
**测试完成后的最佳方案将更新到 response-templates.md 正式文档中**
|
|
241
|
+
|
|
242
|
+
文档版本:v1.2.0
|
|
243
|
+
最后更新:2026-04-26
|
|
@@ -75,8 +75,8 @@ updated: "2026-04-25"
|
|
|
75
75
|
|
|
76
76
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
77
|
🎉 {情绪祝福语}!
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
柴米AI记账
|
|
79
|
+
chaimi-keep-mcp v{版本号}
|
|
80
80
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -95,8 +95,8 @@ updated: "2026-04-25"
|
|
|
95
95
|
|
|
96
96
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
97
|
🎉 祝您用餐愉快!
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
柴米AI记账
|
|
99
|
+
chaimi-keep-mcp v3.2.0
|
|
100
100
|
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
101
101
|
```
|
|
102
102
|
|
package/server.js
CHANGED
|
@@ -33,6 +33,9 @@ const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
|
33
33
|
// 导入校验工具
|
|
34
34
|
const { validateDate } = require('./utils/validators.js');
|
|
35
35
|
|
|
36
|
+
// 导入设备指纹模块
|
|
37
|
+
const { getDeviceFingerprint, getAgentType, saveAgentType, getMacAddresses } = require('./utils/machine-id.js');
|
|
38
|
+
|
|
36
39
|
// API 签名密钥(用于验证请求来自合法 MCP Server)
|
|
37
40
|
const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
|
|
38
41
|
|
|
@@ -487,6 +490,7 @@ async function getToken() {
|
|
|
487
490
|
authState.isAuthorized = true;
|
|
488
491
|
await oauthManager.tokenStorage.save(token);
|
|
489
492
|
// 【新增】保存默认 Agent 名称和 deviceCode
|
|
493
|
+
// 【修复】使用实际授权的 deviceCode
|
|
490
494
|
await oauthManager.tokenStorage.saveAgentName('柴米AI助手');
|
|
491
495
|
await oauthManager.tokenStorage.saveDeviceCode(savedAuthState.deviceCode);
|
|
492
496
|
await oauthManager.clearAuthState();
|
|
@@ -976,7 +980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
976
980
|
// 按 SKILL 规范格式:新的回复模板
|
|
977
981
|
const displayStoreText = displayStore ? `【${displayStore}】` : '';
|
|
978
982
|
const dateStr = result.data?.date ? new Date(result.data.date).toLocaleDateString('zh-CN') : new Date().toLocaleDateString('zh-CN');
|
|
979
|
-
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n
|
|
983
|
+
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n\n商品:${displayName}${displayStoreText}\n金额:¥${displayAmount}\n类型:支出\n分类:${result.data?.categoryName || displayCategory}\n时间:${dateStr}\n\n${friendlyEnding}${insightsText}${achievementsText}\n\n---\n柴米AI记账 v${MCP_VERSION}`;
|
|
980
984
|
|
|
981
985
|
if (!processedArgs.agentType || !processedArgs.apiProvider) {
|
|
982
986
|
userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
|
|
@@ -1220,7 +1224,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1220
1224
|
|
|
1221
1225
|
// 按 SKILL 规范格式:新的回复模板
|
|
1222
1226
|
const dateStr = result.data?.date ? new Date(result.data.date).toLocaleDateString('zh-CN') : new Date().toLocaleDateString('zh-CN');
|
|
1223
|
-
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n
|
|
1227
|
+
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n\n店名:${storeName}\n金额:¥${totalAmount}\n类型:支出\n分类:${result.data?.storeCategory || category}\n商品:${itemCount}件\n时间:${dateStr}\n\n${friendlyEnding}${insightsText}${achievementsText}\n\n---\n柴米AI记账 v${MCP_VERSION}`;
|
|
1224
1228
|
|
|
1225
1229
|
if (!processedArgs.agentType || !processedArgs.apiProvider) {
|
|
1226
1230
|
userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
|
|
@@ -1450,7 +1454,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1450
1454
|
|
|
1451
1455
|
// 按 SKILL 规范格式:新的回复模板
|
|
1452
1456
|
const dateStr = result.data?.date ? new Date(result.data.date).toLocaleDateString('zh-CN') : new Date().toLocaleDateString('zh-CN');
|
|
1453
|
-
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n
|
|
1457
|
+
userMessage = `✅ 「${agentName}」已帮您录入「柴米AI记账」\n\n商品:${displayName}${displayStoreText}\n金额:¥${displayAmount}\n类型:收入\n分类:${result.data?.categoryName || displayCategory}\n时间:${dateStr}\n\n入账顺利!💰\n\n---\n柴米AI记账 v${MCP_VERSION}`;
|
|
1454
1458
|
}
|
|
1455
1459
|
break;
|
|
1456
1460
|
}
|
|
@@ -2111,8 +2115,28 @@ async function startAuthFlow() {
|
|
|
2111
2115
|
}
|
|
2112
2116
|
}
|
|
2113
2117
|
|
|
2114
|
-
//
|
|
2115
|
-
|
|
2118
|
+
// 【新增】获取或设置Agent类型
|
|
2119
|
+
let agentType = await getAgentType();
|
|
2120
|
+
if (!agentType) {
|
|
2121
|
+
// 首次使用,尝试从记账参数中获取,或使用默认值
|
|
2122
|
+
agentType = process.env.AGENT_TYPE || 'unknown';
|
|
2123
|
+
await saveAgentType(agentType);
|
|
2124
|
+
console.error(`[Auth] 首次使用,设置Agent类型: ${agentType}`);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// 【新增】生成设备指纹
|
|
2128
|
+
const deviceFingerprint = await getDeviceFingerprint(agentType);
|
|
2129
|
+
const macAddresses = await getMacAddresses();
|
|
2130
|
+
console.error(`[Auth] 设备指纹: ${deviceFingerprint}`);
|
|
2131
|
+
console.error(`[Auth] MAC地址: ${macAddresses.join(', ') || 'unknown'}`);
|
|
2132
|
+
|
|
2133
|
+
// 获取新的设备码(带上设备指纹)
|
|
2134
|
+
const deviceCodeRes = await oauthManager.requestDeviceCode(false, {
|
|
2135
|
+
deviceFingerprint,
|
|
2136
|
+
agentType,
|
|
2137
|
+
macAddress: macAddresses[0] || 'unknown'
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2116
2140
|
authState.deviceCode = deviceCodeRes.deviceCode;
|
|
2117
2141
|
authState.userCode = deviceCodeRes.userCode;
|
|
2118
2142
|
authState.isWaiting = true;
|
|
@@ -2163,8 +2187,9 @@ async function pollForAuthInBackground(deviceCode, interval) {
|
|
|
2163
2187
|
await oauthManager.tokenStorage.save(token);
|
|
2164
2188
|
|
|
2165
2189
|
// 【新增】保存默认 Agent 名称和 deviceCode(首次记账时会获取真实名称)
|
|
2190
|
+
// 【修复】使用传入的 deviceCode,而不是 authState.deviceCode(可能不一致)
|
|
2166
2191
|
await oauthManager.tokenStorage.saveAgentName('柴米AI助手');
|
|
2167
|
-
await oauthManager.tokenStorage.saveDeviceCode(
|
|
2192
|
+
await oauthManager.tokenStorage.saveDeviceCode(deviceCode);
|
|
2168
2193
|
|
|
2169
2194
|
// 清除授权状态
|
|
2170
2195
|
await oauthManager.clearAuthState();
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 机器ID和设备指纹管理模块
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 生成并管理机器唯一ID(存储在 ~/.mcporter/machine-id)
|
|
6
|
+
* 2. 生成设备指纹(hash(machineId + agentType))
|
|
7
|
+
*
|
|
8
|
+
* 特点:
|
|
9
|
+
* - 首次运行时生成机器ID,后续复用
|
|
10
|
+
* - 网卡切换不影响(所有MAC参与计算)
|
|
11
|
+
* - 重装系统会重新生成(视为新设备)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
// 机器ID文件路径
|
|
20
|
+
const MACHINE_ID_FILE = path.join(
|
|
21
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
22
|
+
'.mcporter',
|
|
23
|
+
'machine-id'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Agent类型文件路径
|
|
27
|
+
const AGENT_TYPE_FILE = path.join(
|
|
28
|
+
process.env.HOME || process.env.USERPROFILE,
|
|
29
|
+
'.mcporter',
|
|
30
|
+
'agent-type.json'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取或生成机器ID
|
|
35
|
+
* @returns {Promise<string>} 16位机器ID
|
|
36
|
+
*/
|
|
37
|
+
async function getMachineId() {
|
|
38
|
+
try {
|
|
39
|
+
// 1. 尝试读取已有ID
|
|
40
|
+
const existing = await fs.readFile(MACHINE_ID_FILE, 'utf8');
|
|
41
|
+
if (existing.trim()) {
|
|
42
|
+
console.error('[MachineId] 使用已有机器ID:', existing.trim());
|
|
43
|
+
return existing.trim();
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// 文件不存在,继续生成
|
|
47
|
+
console.error('[MachineId] 首次使用,生成新机器ID');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. 生成新ID
|
|
51
|
+
const networkInterfaces = os.networkInterfaces();
|
|
52
|
+
const macAddresses = [];
|
|
53
|
+
|
|
54
|
+
for (const name in networkInterfaces) {
|
|
55
|
+
for (const iface of networkInterfaces[name]) {
|
|
56
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
57
|
+
macAddresses.push(iface.mac);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 排序后合并(避免网卡顺序影响)
|
|
63
|
+
macAddresses.sort();
|
|
64
|
+
const machineId = crypto
|
|
65
|
+
.createHash('sha256')
|
|
66
|
+
.update(macAddresses.join(':') + os.hostname())
|
|
67
|
+
.digest('hex')
|
|
68
|
+
.substring(0, 16);
|
|
69
|
+
|
|
70
|
+
console.error('[MachineId] 生成新机器ID:', machineId);
|
|
71
|
+
|
|
72
|
+
// 3. 保存到文件
|
|
73
|
+
try {
|
|
74
|
+
await fs.mkdir(path.dirname(MACHINE_ID_FILE), { recursive: true });
|
|
75
|
+
await fs.writeFile(MACHINE_ID_FILE, machineId, { mode: 0o600 });
|
|
76
|
+
console.error('[MachineId] 机器ID已保存到:', MACHINE_ID_FILE);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[MachineId] 保存机器ID失败:', err.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return machineId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取设备指纹
|
|
86
|
+
* @param {string} agentType - Agent类型(claude/cursor等)
|
|
87
|
+
* @returns {Promise<string>} 16位设备指纹
|
|
88
|
+
*/
|
|
89
|
+
async function getDeviceFingerprint(agentType) {
|
|
90
|
+
const machineId = await getMachineId();
|
|
91
|
+
const fingerprint = crypto
|
|
92
|
+
.createHash('sha256')
|
|
93
|
+
.update(`${machineId}:${agentType}`)
|
|
94
|
+
.digest('hex')
|
|
95
|
+
.substring(0, 16);
|
|
96
|
+
|
|
97
|
+
console.error('[MachineId] 设备指纹:', fingerprint, '(agentType:', agentType + ')');
|
|
98
|
+
return fingerprint;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取Agent类型
|
|
103
|
+
* @returns {Promise<string|null>} Agent类型,首次返回null
|
|
104
|
+
*/
|
|
105
|
+
async function getAgentType() {
|
|
106
|
+
try {
|
|
107
|
+
const data = await fs.readFile(AGENT_TYPE_FILE, 'utf8');
|
|
108
|
+
const parsed = JSON.parse(data);
|
|
109
|
+
if (parsed.agentType) {
|
|
110
|
+
console.error('[MachineId] 使用已有Agent类型:', parsed.agentType);
|
|
111
|
+
return parsed.agentType;
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('[MachineId] 首次使用,需要设置Agent类型');
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 保存Agent类型
|
|
121
|
+
* @param {string} agentType - Agent类型
|
|
122
|
+
*/
|
|
123
|
+
async function saveAgentType(agentType) {
|
|
124
|
+
try {
|
|
125
|
+
await fs.mkdir(path.dirname(AGENT_TYPE_FILE), { recursive: true });
|
|
126
|
+
await fs.writeFile(
|
|
127
|
+
AGENT_TYPE_FILE,
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
agentType,
|
|
130
|
+
updatedAt: new Date().toISOString()
|
|
131
|
+
}),
|
|
132
|
+
{ mode: 0o600 }
|
|
133
|
+
);
|
|
134
|
+
console.error('[MachineId] Agent类型已保存:', agentType);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('[MachineId] 保存Agent类型失败:', err.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取MAC地址列表(用于调试)
|
|
142
|
+
* @returns {Promise<string[]>} MAC地址列表
|
|
143
|
+
*/
|
|
144
|
+
async function getMacAddresses() {
|
|
145
|
+
const networkInterfaces = os.networkInterfaces();
|
|
146
|
+
const macAddresses = [];
|
|
147
|
+
|
|
148
|
+
for (const name in networkInterfaces) {
|
|
149
|
+
for (const iface of networkInterfaces[name]) {
|
|
150
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
151
|
+
macAddresses.push(iface.mac);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return macAddresses.sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
getMachineId,
|
|
161
|
+
getDeviceFingerprint,
|
|
162
|
+
getAgentType,
|
|
163
|
+
saveAgentType,
|
|
164
|
+
getMacAddresses
|
|
165
|
+
};
|