clash-linux commited on
Commit
12cb430
·
verified ·
1 Parent(s): 720114a

Upload 27 files

Browse files
.env.example CHANGED
@@ -20,4 +20,8 @@ PROXY_SERVER_PORT=10655
20
  # tls代理服务器日志文件路径
21
  PROXY_SERVER_LOG_PATH="./proxy_server.log"
22
  # 是否启用tls代理服务器
23
- ENABLE_PROXY_SERVER=true
 
 
 
 
 
20
  # tls代理服务器日志文件路径
21
  PROXY_SERVER_LOG_PATH="./proxy_server.log"
22
  # 是否启用tls代理服务器
23
+ ENABLE_PROXY_SERVER=true
24
+
25
+ # 前端管理界面账号
26
+ ADMIN_USERNAME="admin"
27
+ ADMIN_PASSWORD="admin123"
.gitignore CHANGED
@@ -31,4 +31,17 @@ coverage/
31
  .idea/
32
  .vscode/
33
  *.swp
34
- *.swo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  .idea/
32
  .vscode/
33
  *.swp
34
+ *.swo
35
+
36
+ # 数据文件
37
+ data/
38
+ cookies-data.json
39
+ cookies-data.backup.json
40
+
41
+ # 加密密钥文件(绝对不要提交)
42
+ .cookie-key
43
+ data/.cookie-key
44
+
45
+ # 备份文件
46
+ *.backup
47
+ cookies.json.backup
docs/admin-guide.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Notion2API 管理界面使用指南
2
+
3
+ ## 概述
4
+
5
+ Notion2API 提供了一个美观、易用的Web管理界面,用于管理Cookie和Thread ID配置。
6
+
7
+ ## 访问管理界面
8
+
9
+ 1. 启动应用后,在浏览器中访问:
10
+ ```
11
+ http://localhost:3000/admin
12
+ ```
13
+
14
+ 2. 使用管理员账号登录:
15
+ - 默认用户名:`admin`
16
+ - 默认密码:您的 `ADMIN_PASSWORD` 环境变量值(如未设置,则使用 `AUTH_TOKEN` 的值)
17
+
18
+ 3. 可以通过环境变量自定义管理员凭据:
19
+ ```
20
+ ADMIN_USERNAME=your_username
21
+ ADMIN_PASSWORD=your_password
22
+ ```
23
+
24
+ 4. 登录成功后会保存会话,24小时内无需重复登录
25
+
26
+ ## 功能介绍
27
+
28
+ ### 1. 仪表板统计
29
+
30
+ 界面顶部显示三个关键指标:
31
+ - **总Cookie数**:系统中所有Cookie的总数
32
+ - **有效Cookie数**:当前可用的Cookie数量
33
+ - **已配置ThreadID**:已设置Thread ID的Cookie数量
34
+
35
+ ### 2. Cookie管理
36
+
37
+ #### 查看Cookie列表
38
+ - 表格显示所有Cookie的详细信息
39
+ - 包括用户ID、空间ID、状态、最后使用时间和Thread ID
40
+ - 有效的Cookie显示绿色状态指示器
41
+
42
+ #### 添加新Cookie
43
+ 1. 点击"添加Cookie"按钮
44
+ 2. 在弹出的对话框中输入Cookie内容
45
+ 3. 可选:输入Thread ID(必须手动获取,系统不会自动创建)
46
+ 4. 支持批量添加(使用 | 分隔多个Cookie)
47
+
48
+ #### 编辑Thread ID
49
+ 1. 点击Cookie行中的编辑按钮(铅笔图标)
50
+ 2. 在弹出的对话框中修改Thread ID
51
+ 3. 留空表示不使用特定的Thread ID
52
+
53
+ #### 删除Cookie
54
+ 1. 点击Cookie行中的删除按钮(垃圾桶图标)
55
+ 2. 确认删除操作
56
+
57
+ #### 刷新状态
58
+ - 点击"刷新状态"按钮重新验证所有Cookie的有效性
59
+
60
+ ### 3. 操作日志
61
+
62
+ 界面底部显示所有操作的实时日志,包括:
63
+ - Cookie的添加、删除、更新操作
64
+ - 系统状态变化
65
+ - 错误信息
66
+
67
+ ## Thread ID 管理
68
+
69
+ ### 什么是Thread ID?
70
+
71
+ Thread ID 是Notion AI对话的会话标识符。使用固定的Thread ID可以:
72
+ - 保持对话上下文的连续性
73
+ - 避免重复创建新会话
74
+ - 提高响应效率
75
+
76
+ ### 最佳实践
77
+
78
+ 1. **新用户**:Thread ID需要从现有的Notion对话中手动获取
79
+ 2. **保持上下文**:如果需要保持对话连续性,记录并复用Thread ID
80
+ 3. **重置对话**:不使用Thread ID即可开始新的对话会话
81
+
82
+ ## 安全注意事项
83
+
84
+ 1. **认证令牌**:请妥善保管您的AUTH_TOKEN,它是访问管理界面的唯一凭证
85
+ 2. **Cookie安全**:Cookie包含敏感信息,请勿在公共网络环境下使用管理界面
86
+ 3. **定期更新**:建议定期更新Cookie以确保服务的稳定性
87
+
88
+ ## 常见问题
89
+
90
+ ### Q: 为什么Cookie显示为无效?
91
+ A: Cookie可能已过期或被Notion撤销。请获取新的Cookie并更新。
92
+
93
+ ### Q: Thread ID应该多久更换一次?
94
+ A: 除非遇到问题或需要重置对话上下文,否则可以一直使用同一个Thread ID。
95
+
96
+ ### Q: 如何获取Thread ID?
97
+ A: Thread ID需要从Notion的现有对话中手动获取,系统不会自动创建。您可以通过浏览器开发者工具查看Notion AI对话的网络请求来获取。
98
+
99
+ ### Q: 可以同时使用多个Cookie吗?
100
+ A: 是的,系统支持多Cookie轮询,可以提高请求的稳定性和效率。
101
+
102
+ ### Q: 如何获取Notion Cookie?
103
+ A: 请参考主文档中的Cookie获取方法。
104
+
105
+ ## 移动端支持
106
+
107
+ 管理界面已针对移动设备进行了全面优化:
108
+
109
+ ### 移动端特性
110
+
111
+ 1. **响应式布局**
112
+ - 自适应屏幕大小
113
+ - 优化的触摸操作
114
+ - 简化的表格显示
115
+
116
+ 2. **移动端操作**
117
+ - 点击"三点"按钮查看更多操作
118
+ - 支持查看Cookie详情
119
+ - 底部浮动操作栏方便快速操作
120
+
121
+ 3. **优化的交互**
122
+ - 大尺寸触摸目标
123
+ - 防止输入框缩放
124
+ - 流畅的滚动体验
125
+
126
+ ### 移动端使用建议
127
+
128
+ 1. **横屏模式**:在查看完整信息时,可以使用横屏模式
129
+ 2. **快速操作**:使用底部操作栏进行刷新和添加操作
130
+ 3. **详情查看**:点击"更多操作"→"查看详情"查看完整Cookie信息
131
+
132
+ ## 技术支持
133
+
134
+ 如遇到问题,请查看操作日志中的错误信息,或查看服务器控制台输出。
package.json CHANGED
@@ -2,17 +2,20 @@
2
  "name": "notion2api-nodejs",
3
  "version": "1.0.0",
4
  "description": "Notion API client with lightweight browser-free option",
5
- "main": "src/lightweight-client-express.js",
6
  "type": "module",
7
  "bin": {
8
  "notion-cookie": "src/cookie-cli.js"
9
  },
10
  "scripts": {
11
- "start": "node src/lightweight-client-express.js",
12
- "dev": "nodemon src/lightweight-client-express.js",
13
- "original": "node src/index.js",
14
- "cookie": "node src/cookie-cli.js"
15
- },
 
 
 
16
  "keywords": [
17
  "notion",
18
  "openai",
 
2
  "name": "notion2api-nodejs",
3
  "version": "1.0.0",
4
  "description": "Notion API client with lightweight browser-free option",
5
+ "main": "src/app.js",
6
  "type": "module",
7
  "bin": {
8
  "notion-cookie": "src/cookie-cli.js"
9
  },
10
  "scripts": {
11
+ "start": "node src/app.js",
12
+ "dev": "nodemon src/app.js",
13
+ "start:legacy": "node src/lightweight-client-express.js",
14
+ "proxy-start": "node src/ProxyServer.js",
15
+ "test-proxy": "node src/test-proxy.js",
16
+ "cookie": "node src/cookie-cli.js",
17
+ "cli": "node src/cookie-cli.js"
18
+ },
19
  "keywords": [
20
  "notion",
21
  "openai",
public/admin.html ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
8
+ <title>Notion2API 管理面板</title>
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --primary-color: #0066cc;
14
+ --secondary-color: #6c757d;
15
+ --success-color: #28a745;
16
+ --danger-color: #dc3545;
17
+ --warning-color: #ffc107;
18
+ --light-bg: #f8f9fa;
19
+ --border-color: #dee2e6;
20
+ --mobile-navbar-height: 56px;
21
+ }
22
+
23
+ body {
24
+ background-color: var(--light-bg);
25
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
26
+ -webkit-font-smoothing: antialiased;
27
+ -webkit-tap-highlight-color: transparent;
28
+ }
29
+
30
+ /* 移动端优化 */
31
+ @media (max-width: 768px) {
32
+ body {
33
+ padding-bottom: 60px; /* 为底部操作栏留出空间 */
34
+ }
35
+
36
+ .container {
37
+ padding-left: 12px;
38
+ padding-right: 12px;
39
+ }
40
+
41
+ /* 移动端导航栏优化 */
42
+ .navbar {
43
+ padding: 0.5rem 0;
44
+ position: sticky;
45
+ top: 0;
46
+ z-index: 1030;
47
+ }
48
+
49
+ .navbar-brand {
50
+ font-size: 1.1rem;
51
+ }
52
+
53
+ .navbar-brand i {
54
+ display: none; /* 移动端隐藏图标 */
55
+ }
56
+
57
+ /* 用户信息优化 */
58
+ .user-info {
59
+ gap: 0.5rem !important;
60
+ }
61
+
62
+ .user-info .avatar {
63
+ width: 28px !important;
64
+ height: 28px !important;
65
+ font-size: 0.875rem;
66
+ }
67
+
68
+ .user-info span {
69
+ font-size: 0.875rem;
70
+ }
71
+
72
+ /* 统计卡片移动端优化 */
73
+ .stats-card {
74
+ padding: 1rem !important;
75
+ margin-bottom: 0.75rem;
76
+ }
77
+
78
+ .stats-card h3 {
79
+ font-size: 1.5rem !important;
80
+ }
81
+
82
+ .stats-card p {
83
+ font-size: 0.875rem;
84
+ }
85
+
86
+ /* 表格移动端优化 */
87
+ .table-container {
88
+ overflow-x: auto;
89
+ -webkit-overflow-scrolling: touch;
90
+ margin: 0 -12px;
91
+ padding: 0 12px;
92
+ }
93
+
94
+ .table {
95
+ font-size: 0.875rem;
96
+ white-space: nowrap;
97
+ }
98
+
99
+ .table td, .table th {
100
+ padding: 0.5rem;
101
+ }
102
+
103
+ /* 隐藏次要列 */
104
+ .mobile-hide {
105
+ display: none !important;
106
+ }
107
+
108
+ /* 操作按钮优化 */
109
+ .action-buttons {
110
+ gap: 0.25rem !important;
111
+ }
112
+
113
+ .btn-sm {
114
+ padding: 0.25rem 0.5rem;
115
+ font-size: 0.75rem;
116
+ }
117
+
118
+ /* 卡片移动端优化 */
119
+ .card {
120
+ margin-bottom: 1rem;
121
+ border-radius: 8px;
122
+ }
123
+
124
+ .card-header {
125
+ padding: 0.75rem 1rem;
126
+ font-size: 0.9rem;
127
+ }
128
+
129
+ .card-body {
130
+ padding: 1rem;
131
+ }
132
+
133
+ /* 模态框移动端优化 */
134
+ .modal-dialog {
135
+ margin: 0.5rem;
136
+ max-width: calc(100% - 1rem);
137
+ }
138
+
139
+ .modal-content {
140
+ border-radius: 12px;
141
+ }
142
+
143
+ /* 表单移动端优化 */
144
+ .form-control, .form-select {
145
+ font-size: 16px; /* 防止iOS缩放 */
146
+ padding: 0.75rem;
147
+ }
148
+
149
+ /* 底部浮动操作栏 */
150
+ .mobile-action-bar {
151
+ position: fixed;
152
+ bottom: 0;
153
+ left: 0;
154
+ right: 0;
155
+ background: white;
156
+ border-top: 1px solid var(--border-color);
157
+ padding: 0.75rem;
158
+ display: flex;
159
+ gap: 0.5rem;
160
+ z-index: 1020;
161
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
162
+ }
163
+
164
+ .mobile-action-bar .btn {
165
+ flex: 1;
166
+ font-size: 0.875rem;
167
+ }
168
+
169
+ /* 空状态优化 */
170
+ .empty-state {
171
+ padding: 2rem 1rem;
172
+ }
173
+
174
+ .empty-state i {
175
+ font-size: 2rem;
176
+ }
177
+
178
+ /* Toast移动端位置 */
179
+ .toast-container {
180
+ top: var(--mobile-navbar-height) !important;
181
+ right: 12px !important;
182
+ }
183
+ }
184
+
185
+ /* 通用样式 */
186
+ .navbar {
187
+ background-color: white !important;
188
+ box-shadow: 0 2px 4px rgba(0,0,0,.1);
189
+ }
190
+
191
+ .navbar-brand {
192
+ font-weight: 600;
193
+ color: var(--primary-color) !important;
194
+ }
195
+
196
+ .main-container {
197
+ margin-top: 2rem;
198
+ }
199
+
200
+ .card {
201
+ border: none;
202
+ box-shadow: 0 2px 8px rgba(0,0,0,.08);
203
+ border-radius: 12px;
204
+ margin-bottom: 1.5rem;
205
+ }
206
+
207
+ .card-header {
208
+ background-color: white;
209
+ border-bottom: 1px solid var(--border-color);
210
+ padding: 1.25rem;
211
+ font-weight: 600;
212
+ }
213
+
214
+ .table {
215
+ margin-bottom: 0;
216
+ }
217
+
218
+ .table th {
219
+ border-bottom: 2px solid var(--border-color);
220
+ font-weight: 600;
221
+ color: var(--secondary-color);
222
+ text-transform: uppercase;
223
+ font-size: 0.875rem;
224
+ letter-spacing: 0.5px;
225
+ }
226
+
227
+ .badge {
228
+ padding: 0.375rem 0.75rem;
229
+ font-weight: 500;
230
+ }
231
+
232
+ .btn {
233
+ border-radius: 8px;
234
+ padding: 0.5rem 1rem;
235
+ font-weight: 500;
236
+ transition: all 0.2s;
237
+ }
238
+
239
+ .btn-primary {
240
+ background-color: var(--primary-color);
241
+ border-color: var(--primary-color);
242
+ }
243
+
244
+ .btn-primary:hover {
245
+ background-color: #0056b3;
246
+ border-color: #0056b3;
247
+ transform: translateY(-1px);
248
+ box-shadow: 0 4px 8px rgba(0,102,204,.25);
249
+ }
250
+
251
+ .btn-sm {
252
+ padding: 0.375rem 0.75rem;
253
+ font-size: 0.875rem;
254
+ }
255
+
256
+ .status-indicator {
257
+ display: inline-block;
258
+ width: 8px;
259
+ height: 8px;
260
+ border-radius: 50%;
261
+ margin-right: 0.5rem;
262
+ }
263
+
264
+ .status-active {
265
+ background-color: var(--success-color);
266
+ animation: pulse 2s infinite;
267
+ }
268
+
269
+ .status-inactive {
270
+ background-color: var(--danger-color);
271
+ }
272
+
273
+ @keyframes pulse {
274
+ 0% {
275
+ box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.4);
276
+ }
277
+ 70% {
278
+ box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
279
+ }
280
+ 100% {
281
+ box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
282
+ }
283
+ }
284
+
285
+ .modal-header {
286
+ border-bottom: 1px solid var(--border-color);
287
+ background-color: var(--light-bg);
288
+ }
289
+
290
+ .form-label {
291
+ font-weight: 500;
292
+ color: var(--secondary-color);
293
+ margin-bottom: 0.5rem;
294
+ }
295
+
296
+ .form-control, .form-select {
297
+ border-radius: 8px;
298
+ border: 1px solid var(--border-color);
299
+ padding: 0.625rem 0.875rem;
300
+ }
301
+
302
+ .form-control:focus, .form-select:focus {
303
+ border-color: var(--primary-color);
304
+ box-shadow: 0 0 0 0.2rem rgba(0, 102, 204, 0.25);
305
+ }
306
+
307
+ .alert {
308
+ border-radius: 8px;
309
+ border: none;
310
+ }
311
+
312
+ .text-muted {
313
+ color: #8492a6 !important;
314
+ }
315
+
316
+ .empty-state {
317
+ text-align: center;
318
+ padding: 3rem;
319
+ color: var(--secondary-color);
320
+ }
321
+
322
+ .empty-state i {
323
+ font-size: 3rem;
324
+ color: var(--border-color);
325
+ margin-bottom: 1rem;
326
+ }
327
+
328
+ .stats-card {
329
+ background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
330
+ color: white;
331
+ border: none;
332
+ border-radius: 12px;
333
+ padding: 1.5rem;
334
+ }
335
+
336
+ .stats-card h3 {
337
+ margin-bottom: 0.5rem;
338
+ font-size: 2rem;
339
+ font-weight: 700;
340
+ }
341
+
342
+ .stats-card p {
343
+ margin-bottom: 0;
344
+ opacity: 0.9;
345
+ }
346
+
347
+ .loading-spinner {
348
+ display: none;
349
+ position: fixed;
350
+ top: 50%;
351
+ left: 50%;
352
+ transform: translate(-50%, -50%);
353
+ z-index: 9999;
354
+ }
355
+
356
+ .loading-spinner.active {
357
+ display: block;
358
+ }
359
+
360
+ .toast-container {
361
+ position: fixed;
362
+ top: 20px;
363
+ right: 20px;
364
+ z-index: 1050;
365
+ }
366
+
367
+ .cookie-item {
368
+ transition: background-color 0.2s;
369
+ }
370
+
371
+ .cookie-item:hover {
372
+ background-color: var(--light-bg);
373
+ }
374
+
375
+ .action-buttons {
376
+ display: flex;
377
+ gap: 0.5rem;
378
+ }
379
+
380
+ .thread-id-input {
381
+ max-width: 200px;
382
+ }
383
+
384
+ /* 登录页面样式 */
385
+ .login-container {
386
+ min-height: 100vh;
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
391
+ padding: 1rem;
392
+ }
393
+
394
+ .login-card {
395
+ width: 100%;
396
+ max-width: 400px;
397
+ padding: 2rem;
398
+ background: white;
399
+ border-radius: 16px;
400
+ box-shadow: 0 10px 25px rgba(0,0,0,.1);
401
+ }
402
+
403
+ .login-card .logo {
404
+ text-align: center;
405
+ margin-bottom: 2rem;
406
+ }
407
+
408
+ .login-card .logo i {
409
+ font-size: 3rem;
410
+ color: var(--primary-color);
411
+ }
412
+
413
+ .login-card h2 {
414
+ text-align: center;
415
+ margin-bottom: 1.5rem;
416
+ color: #333;
417
+ font-weight: 600;
418
+ }
419
+
420
+ .login-error {
421
+ display: none;
422
+ margin-bottom: 1rem;
423
+ }
424
+
425
+ #mainContent {
426
+ display: none;
427
+ }
428
+
429
+ .user-info {
430
+ display: flex;
431
+ align-items: center;
432
+ gap: 1rem;
433
+ }
434
+
435
+ .user-info .avatar {
436
+ width: 32px;
437
+ height: 32px;
438
+ border-radius: 50%;
439
+ background: var(--primary-color);
440
+ color: white;
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ font-weight: 600;
445
+ }
446
+
447
+ /* 表格响应式滚动 */
448
+ .table-responsive {
449
+ -webkit-overflow-scrolling: touch;
450
+ }
451
+
452
+ /* 移动端触摸优化 */
453
+ @media (hover: none) {
454
+ .btn:hover {
455
+ transform: none;
456
+ box-shadow: none;
457
+ }
458
+
459
+ .cookie-item:hover {
460
+ background-color: transparent;
461
+ }
462
+ }
463
+
464
+ /* 桌面端隐藏移动操作栏 */
465
+ @media (min-width: 769px) {
466
+ .mobile-action-bar {
467
+ display: none !important;
468
+ }
469
+
470
+ .mobile-only {
471
+ display: none !important;
472
+ }
473
+ }
474
+
475
+ /* 改进的响应式断点 */
476
+ @media (min-width: 576px) and (max-width: 768px) {
477
+ .col-sm-6 {
478
+ flex: 0 0 50%;
479
+ max-width: 50%;
480
+ }
481
+ }
482
+ </style>
483
+ </head>
484
+ <body>
485
+ <!-- 登录页面 -->
486
+ <div id="loginContainer" class="login-container">
487
+ <div class="login-card">
488
+ <div class="logo">
489
+ <i class="bi bi-cloud-arrow-up-fill"></i>
490
+ </div>
491
+ <h2>Notion2API 管理登录</h2>
492
+
493
+ <div class="alert alert-danger login-error" id="loginError" role="alert">
494
+ <i class="bi bi-exclamation-circle me-2"></i>
495
+ <span id="loginErrorText">用户名或密码错误</span>
496
+ </div>
497
+
498
+ <form id="loginForm">
499
+ <div class="mb-3">
500
+ <label for="username" class="form-label">用户名</label>
501
+ <input type="text" class="form-control" id="username" required
502
+ placeholder="请输入用户名" autocomplete="username">
503
+ </div>
504
+ <div class="mb-3">
505
+ <label for="password" class="form-label">密码</label>
506
+ <input type="password" class="form-control" id="password" required
507
+ placeholder="请输入密码" autocomplete="current-password">
508
+ </div>
509
+ <div class="mb-3 form-check">
510
+ <input type="checkbox" class="form-check-input" id="remember">
511
+ <label class="form-check-label" for="remember">
512
+ 记住我
513
+ </label>
514
+ </div>
515
+ <button type="submit" class="btn btn-primary w-100">
516
+ <i class="bi bi-box-arrow-in-right me-2"></i>
517
+ 登录
518
+ </button>
519
+ </form>
520
+
521
+ <div class="text-center mt-3 text-muted">
522
+ <small>默认用户名: admin</small>
523
+ </div>
524
+ </div>
525
+ </div>
526
+
527
+ <!-- 主内容区域 -->
528
+ <div id="mainContent">
529
+ <!-- 导航栏 -->
530
+ <nav class="navbar navbar-expand-lg navbar-light">
531
+ <div class="container">
532
+ <a class="navbar-brand" href="#">
533
+ <i class="bi bi-cloud-arrow-up-fill me-2"></i>
534
+ <span class="d-none d-sm-inline">Notion2API 管理面板</span>
535
+ <span class="d-inline d-sm-none">Notion2API</span>
536
+ </a>
537
+ <div class="ms-auto d-flex align-items-center">
538
+ <div class="user-info me-3">
539
+ <div class="avatar">
540
+ <span id="userAvatar">A</span>
541
+ </div>
542
+ <span class="text-muted d-none d-sm-inline" id="currentUser">admin</span>
543
+ </div>
544
+ <button class="btn btn-outline-secondary btn-sm" onclick="logout()">
545
+ <i class="bi bi-box-arrow-right me-1 d-none d-sm-inline"></i>
546
+ <span class="d-none d-sm-inline">退出</span>
547
+ <span class="d-inline d-sm-none">退出</span>
548
+ </button>
549
+ </div>
550
+ </div>
551
+ </nav>
552
+
553
+ <!-- 主容器 -->
554
+ <div class="container main-container">
555
+ <!-- 统计信息 -->
556
+ <div class="row mb-4">
557
+ <div class="col-12 col-sm-6 col-md-4 mb-3 mb-md-0">
558
+ <div class="card stats-card">
559
+ <div class="card-body">
560
+ <h3 id="totalCookies">0</h3>
561
+ <p>总Cookie数</p>
562
+ </div>
563
+ </div>
564
+ </div>
565
+ <div class="col-12 col-sm-6 col-md-4 mb-3 mb-md-0">
566
+ <div class="card stats-card">
567
+ <div class="card-body">
568
+ <h3 id="activeCookies">0</h3>
569
+ <p>有效Cookie数</p>
570
+ </div>
571
+ </div>
572
+ </div>
573
+ <div class="col-12 col-sm-6 col-md-4">
574
+ <div class="card stats-card">
575
+ <div class="card-body">
576
+ <h3 id="threadIdCount">0</h3>
577
+ <p>已配置ThreadID</p>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <!-- Cookie管理 -->
584
+ <div class="card">
585
+ <div class="card-header d-flex justify-content-between align-items-center">
586
+ <span>
587
+ <i class="bi bi-key-fill me-2"></i>
588
+ Cookie 管理
589
+ </span>
590
+ <div class="d-none d-md-block">
591
+ <button class="btn btn-success btn-sm me-2" onclick="refreshCookies()">
592
+ <i class="bi bi-arrow-clockwise me-1"></i>
593
+ <span class="d-none d-sm-inline">刷新状态</span>
594
+ <span class="d-inline d-sm-none">刷新</span>
595
+ </button>
596
+ <button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addCookieModal">
597
+ <i class="bi bi-plus-circle me-1"></i>
598
+ <span class="d-none d-sm-inline">添加Cookie</span>
599
+ <span class="d-inline d-sm-none">添加</span>
600
+ </button>
601
+ </div>
602
+ </div>
603
+ <div class="card-body p-0 p-md-3">
604
+ <div class="table-container">
605
+ <div class="table-responsive">
606
+ <table class="table table-hover mb-0">
607
+ <thead>
608
+ <tr>
609
+ <th width="40">
610
+ <i class="bi bi-toggle-on" title="启用/禁用"></i>
611
+ </th>
612
+ <th width="50">#</th>
613
+ <th>用户ID</th>
614
+ <th class="mobile-hide">空间ID</th>
615
+ <th class="mobile-hide">Cookie预览</th>
616
+ <th width="120">状态</th>
617
+ <th class="mobile-hide" width="150">最后使用</th>
618
+ <th class="mobile-hide" width="150">Thread ID</th>
619
+ <th width="100">操作</th>
620
+ </tr>
621
+ </thead>
622
+ <tbody id="cookieTableBody">
623
+ <!-- Cookie列表将通过JavaScript动态加载 -->
624
+ </tbody>
625
+ </table>
626
+ </div>
627
+ </div>
628
+ <div id="emptyCookieState" class="empty-state" style="display: none;">
629
+ <i class="bi bi-inbox d-block"></i>
630
+ <p>暂无Cookie数据</p>
631
+ <button class="btn btn-primary btn-sm mt-2" data-bs-toggle="modal" data-bs-target="#addCookieModal">
632
+ 添加第一个Cookie
633
+ </button>
634
+ </div>
635
+ </div>
636
+ </div>
637
+
638
+ <!-- 操作日志(桌面端显示) -->
639
+ <div class="card d-none d-md-block">
640
+ <div class="card-header">
641
+ <i class="bi bi-journal-text me-2"></i>
642
+ 操作日志
643
+ </div>
644
+ <div class="card-body">
645
+ <div id="logContainer" style="max-height: 300px; overflow-y: auto;">
646
+ <div class="text-muted text-center py-3">暂无操作日志</div>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ </div>
651
+
652
+ <!-- 移动端底部操作栏 -->
653
+ <div class="mobile-action-bar d-md-none">
654
+ <button class="btn btn-success btn-sm" onclick="refreshCookies()">
655
+ <i class="bi bi-arrow-clockwise"></i>
656
+ 刷新
657
+ </button>
658
+ <button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addCookieModal">
659
+ <i class="bi bi-plus-circle"></i>
660
+ 添加
661
+ </button>
662
+ </div>
663
+
664
+ <!-- 添加Cookie模态框 -->
665
+ <div class="modal fade" id="addCookieModal" tabindex="-1">
666
+ <div class="modal-dialog modal-lg modal-dialog-centered">
667
+ <div class="modal-content">
668
+ <div class="modal-header">
669
+ <h5 class="modal-title">
670
+ <i class="bi bi-plus-circle me-2"></i>
671
+ 添加新Cookie
672
+ </h5>
673
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
674
+ </div>
675
+ <div class="modal-body">
676
+ <form id="addCookieForm">
677
+ <div class="mb-3">
678
+ <label class="form-label">Cookie内容</label>
679
+ <textarea class="form-control" id="cookieContent" rows="4"
680
+ placeholder="请输入Cookie内容..." required></textarea>
681
+ <small class="text-muted">支持单个Cookie或使用 | 分隔的多个Cookie</small>
682
+ </div>
683
+ <div class="mb-3">
684
+ <label class="form-label">Thread ID(可选)</label>
685
+ <input type="text" class="form-control" id="cookieThreadId"
686
+ placeholder="如需指定Thread ID,请在此输入">
687
+ <small class="text-muted">Thread ID需要手动从Notion获取,系统不会自动创建</small>
688
+ </div>
689
+ </form>
690
+ </div>
691
+ <div class="modal-footer">
692
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
693
+ <button type="button" class="btn btn-primary" onclick="addCookie()">
694
+ <i class="bi bi-check-circle me-1"></i>
695
+ 添加
696
+ </button>
697
+ </div>
698
+ </div>
699
+ </div>
700
+ </div>
701
+
702
+ <!-- 编辑ThreadID模态框 -->
703
+ <div class="modal fade" id="editThreadIdModal" tabindex="-1">
704
+ <div class="modal-dialog modal-dialog-centered">
705
+ <div class="modal-content">
706
+ <div class="modal-header">
707
+ <h5 class="modal-title">
708
+ <i class="bi bi-pencil-square me-2"></i>
709
+ 编辑Thread ID
710
+ </h5>
711
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
712
+ </div>
713
+ <div class="modal-body">
714
+ <form id="editThreadIdForm">
715
+ <input type="hidden" id="editCookieIndex">
716
+ <div class="mb-3">
717
+ <label class="form-label">用户ID</label>
718
+ <input type="text" class="form-control" id="editUserId" readonly>
719
+ </div>
720
+ <div class="mb-3">
721
+ <label class="form-label">Thread ID</label>
722
+ <input type="text" class="form-control" id="editThreadId"
723
+ placeholder="输入Thread ID">
724
+ <small class="text-muted">留空表示不使用特定的Thread ID</small>
725
+ </div>
726
+ </form>
727
+ </div>
728
+ <div class="modal-footer">
729
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
730
+ <button type="button" class="btn btn-primary" onclick="saveThreadId()">
731
+ <i class="bi bi-check-circle me-1"></i>
732
+ 保存
733
+ </button>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ </div>
738
+
739
+ <!-- 加载动画 -->
740
+ <div class="loading-spinner">
741
+ <div class="spinner-border text-primary" role="status">
742
+ <span class="visually-hidden">加载中...</span>
743
+ </div>
744
+ </div>
745
+
746
+ <!-- Toast容器 -->
747
+ <div class="toast-container"></div>
748
+
749
+ <!-- Scripts -->
750
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
751
+ <script src="/admin.js"></script>
752
+ </div>
753
+ </body>
754
+ </html>
public/admin.js ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 全局变量
2
+ let cookies = [];
3
+ let authToken = '';
4
+ let currentUser = null;
5
+
6
+ // 页面初始化
7
+ document.addEventListener('DOMContentLoaded', function() {
8
+ // 检查是否已登录
9
+ checkLoginStatus();
10
+
11
+ // 绑定登录表单事件
12
+ const loginForm = document.getElementById('loginForm');
13
+ if (loginForm) {
14
+ loginForm.addEventListener('submit', handleLogin);
15
+ }
16
+
17
+ // 监听窗口大小变化
18
+ window.addEventListener('resize', debounce(updateUI, 250));
19
+ });
20
+
21
+ // 防抖函数
22
+ function debounce(func, wait) {
23
+ let timeout;
24
+ return function executedFunction(...args) {
25
+ const later = () => {
26
+ clearTimeout(timeout);
27
+ func(...args);
28
+ };
29
+ clearTimeout(timeout);
30
+ timeout = setTimeout(later, wait);
31
+ };
32
+ }
33
+
34
+ // 检查登录状态
35
+ function checkLoginStatus() {
36
+ const savedSession = localStorage.getItem('adminSession');
37
+ if (savedSession) {
38
+ try {
39
+ const session = JSON.parse(savedSession);
40
+ // 检查会话是否过期(24小时)
41
+ if (new Date().getTime() - session.timestamp < 24 * 60 * 60 * 1000) {
42
+ currentUser = session.user;
43
+ authToken = session.token;
44
+ showMainContent();
45
+ return;
46
+ }
47
+ } catch (e) {
48
+ console.error('Invalid session data');
49
+ }
50
+ }
51
+
52
+ // 检查记住的用户名
53
+ const rememberedUser = localStorage.getItem('rememberedUser');
54
+ if (rememberedUser) {
55
+ document.getElementById('username').value = rememberedUser;
56
+ document.getElementById('remember').checked = true;
57
+ }
58
+ }
59
+
60
+ // 处理登录
61
+ async function handleLogin(e) {
62
+ e.preventDefault();
63
+
64
+ const username = document.getElementById('username').value.trim();
65
+ const password = document.getElementById('password').value;
66
+ const remember = document.getElementById('remember').checked;
67
+
68
+ // 显示加载状态
69
+ const submitBtn = e.target.querySelector('button[type="submit"]');
70
+ const originalText = submitBtn.innerHTML;
71
+ submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>登录中...';
72
+ submitBtn.disabled = true;
73
+
74
+ try {
75
+ // 发送登录请求
76
+ const response = await fetch('/admin/login', {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json'
80
+ },
81
+ body: JSON.stringify({ username, password })
82
+ });
83
+
84
+ const result = await response.json();
85
+
86
+ if (response.ok && result.success) {
87
+ // 登录成功
88
+ currentUser = result.user;
89
+ authToken = result.token;
90
+
91
+ // 保存会话
92
+ const session = {
93
+ user: currentUser,
94
+ token: authToken,
95
+ timestamp: new Date().getTime()
96
+ };
97
+ localStorage.setItem('adminSession', JSON.stringify(session));
98
+
99
+ // 记住用户名
100
+ if (remember) {
101
+ localStorage.setItem('rememberedUser', username);
102
+ } else {
103
+ localStorage.removeItem('rememberedUser');
104
+ }
105
+
106
+ // 隐藏错误提示
107
+ document.getElementById('loginError').style.display = 'none';
108
+
109
+ // 显示主内容
110
+ showMainContent();
111
+ } else {
112
+ // 登录失败
113
+ document.getElementById('loginErrorText').textContent = result.message || '用户名或密码错误';
114
+ document.getElementById('loginError').style.display = 'block';
115
+ }
116
+ } catch (error) {
117
+ console.error('Login error:', error);
118
+ document.getElementById('loginErrorText').textContent = '登录失败,请稍后重试';
119
+ document.getElementById('loginError').style.display = 'block';
120
+ } finally {
121
+ // 恢复按钮状态
122
+ submitBtn.innerHTML = originalText;
123
+ submitBtn.disabled = false;
124
+ }
125
+ }
126
+
127
+ // 显示主内容
128
+ function showMainContent() {
129
+ // 隐藏登录页面
130
+ document.getElementById('loginContainer').style.display = 'none';
131
+
132
+ // 显示主内容
133
+ document.getElementById('mainContent').style.display = 'block';
134
+
135
+ // 更新用户信息
136
+ if (currentUser) {
137
+ document.getElementById('currentUser').textContent = currentUser.username;
138
+ document.getElementById('userAvatar').textContent = currentUser.username.charAt(0).toUpperCase();
139
+ }
140
+
141
+ // 初始化主页面
142
+ initMainPage();
143
+ }
144
+
145
+ // 初始化主页面
146
+ function initMainPage() {
147
+ // 更新时间
148
+ updateTime();
149
+ setInterval(updateTime, 1000);
150
+
151
+ // 加载Cookie数据
152
+ loadCookies();
153
+
154
+ // 定期刷新数据
155
+ setInterval(loadCookies, 30000); // 每30秒刷新一次
156
+ }
157
+
158
+ // 退出登录
159
+ function logout() {
160
+ if (confirm('确定要退出登录吗?')) {
161
+ // 清除会话
162
+ localStorage.removeItem('adminSession');
163
+ currentUser = null;
164
+ authToken = '';
165
+
166
+ // 重新加载页面
167
+ window.location.reload();
168
+ }
169
+ }
170
+
171
+ // 更新时间
172
+ function updateTime() {
173
+ const now = new Date();
174
+ const timeString = now.toLocaleString('zh-CN', {
175
+ year: 'numeric',
176
+ month: '2-digit',
177
+ day: '2-digit',
178
+ hour: '2-digit',
179
+ minute: '2-digit',
180
+ second: '2-digit'
181
+ });
182
+ const timeElement = document.getElementById('currentTime');
183
+ if (timeElement) {
184
+ timeElement.textContent = timeString;
185
+ }
186
+ }
187
+
188
+ // 显示加载动画
189
+ function showLoading() {
190
+ document.querySelector('.loading-spinner').classList.add('active');
191
+ }
192
+
193
+ // 隐藏加载动画
194
+ function hideLoading() {
195
+ document.querySelector('.loading-spinner').classList.remove('active');
196
+ }
197
+
198
+ // 显示提示消息
199
+ function showToast(message, type = 'success') {
200
+ const toastHtml = `
201
+ <div class="toast align-items-center text-white bg-${type} border-0" role="alert">
202
+ <div class="d-flex">
203
+ <div class="toast-body">
204
+ ${message}
205
+ </div>
206
+ <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
207
+ </div>
208
+ </div>
209
+ `;
210
+
211
+ const toastContainer = document.querySelector('.toast-container');
212
+ const toastElement = document.createElement('div');
213
+ toastElement.innerHTML = toastHtml;
214
+ toastContainer.appendChild(toastElement);
215
+
216
+ const toast = new bootstrap.Toast(toastElement.querySelector('.toast'));
217
+ toast.show();
218
+
219
+ setTimeout(() => {
220
+ toastElement.remove();
221
+ }, 5000);
222
+ }
223
+
224
+ // 添加日志
225
+ function addLog(message, type = 'info') {
226
+ const logContainer = document.getElementById('logContainer');
227
+ if (!logContainer) return; // 移动端可能没有日志容器
228
+
229
+ const timestamp = new Date().toLocaleTimeString('zh-CN');
230
+
231
+ const logEntry = document.createElement('div');
232
+ logEntry.className = `log-entry mb-2 p-2 rounded bg-light`;
233
+ logEntry.innerHTML = `
234
+ <small class="text-muted">[${timestamp}]</small>
235
+ <span class="text-${type === 'error' ? 'danger' : type === 'success' ? 'success' : 'dark'}">${message}</span>
236
+ `;
237
+
238
+ // 清空默认提示
239
+ if (logContainer.querySelector('.text-center')) {
240
+ logContainer.innerHTML = '';
241
+ }
242
+
243
+ logContainer.appendChild(logEntry);
244
+ logContainer.scrollTop = logContainer.scrollHeight;
245
+ }
246
+
247
+ // 加载Cookie数据
248
+ async function loadCookies() {
249
+ try {
250
+ const response = await fetch('/cookies/status', {
251
+ headers: {
252
+ 'Authorization': `Bearer ${authToken}`
253
+ }
254
+ });
255
+
256
+ if (!response.ok) {
257
+ if (response.status === 401) {
258
+ // 认证失败,重新登录
259
+ localStorage.removeItem('adminSession');
260
+ window.location.reload();
261
+ return;
262
+ }
263
+ throw new Error(`HTTP error! status: ${response.status}`);
264
+ }
265
+
266
+ const data = await response.json();
267
+ cookies = data.cookies || [];
268
+
269
+ updateUI();
270
+ addLog('成功加载Cookie数据', 'success');
271
+ } catch (error) {
272
+ console.error('加载Cookie失败:', error);
273
+ showToast('加载Cookie数据失败', 'danger');
274
+ addLog(`加载Cookie失败: ${error.message}`, 'error');
275
+ }
276
+ }
277
+
278
+ // 更新UI
279
+ function updateUI() {
280
+ // 更新统计信息
281
+ const totalCookies = cookies.length;
282
+ const activeCookies = cookies.filter(c => c.valid && c.enabled).length;
283
+ const threadIdCount = cookies.filter(c => c.threadId).length;
284
+
285
+ document.getElementById('totalCookies').textContent = totalCookies;
286
+ document.getElementById('activeCookies').textContent = activeCookies;
287
+ document.getElementById('threadIdCount').textContent = threadIdCount;
288
+
289
+ // 更新表格
290
+ const tbody = document.getElementById('cookieTableBody');
291
+ const emptyState = document.getElementById('emptyCookieState');
292
+
293
+ if (cookies.length === 0) {
294
+ tbody.innerHTML = '';
295
+ emptyState.style.display = 'block';
296
+ } else {
297
+ emptyState.style.display = 'none';
298
+
299
+ // 检测是否为移动设备
300
+ const isMobile = window.innerWidth <= 768;
301
+
302
+ tbody.innerHTML = cookies.map((cookie, index) => {
303
+ if (isMobile) {
304
+ // 移动端简化表格
305
+ return `
306
+ <tr class="cookie-item ${!cookie.enabled ? 'opacity-50' : ''}" data-index="${index}">
307
+ <td>
308
+ <div class="form-check">
309
+ <input class="form-check-input" type="checkbox"
310
+ ${cookie.enabled ? 'checked' : ''}
311
+ onchange="toggleCookieEnabled('${cookie.userId}', this.checked)"
312
+ title="${cookie.enabled ? '点击禁用' : '点击启用'}">
313
+ </div>
314
+ </td>
315
+ <td>${index + 1}</td>
316
+ <td>
317
+ <div>
318
+ <code style="font-size: 0.75rem; word-break: break-all;">${cookie.userId.substring(0, 8)}...</code>
319
+ ${cookie.threadId ? '<br><small class="text-muted" style="font-size: 0.7rem;"><i class="bi bi-link-45deg"></i> Thread已配置</small>' : ''}
320
+ </div>
321
+ </td>
322
+ <td>
323
+ <div>
324
+ ${cookie.valid
325
+ ? '<span class="badge bg-success" style="font-size: 0.7rem;"><i class="bi bi-check-circle"></i> 有效</span>'
326
+ : '<span class="badge bg-danger" style="font-size: 0.7rem;"><i class="bi bi-x-circle"></i> 无效</span>'}
327
+ ${!cookie.enabled
328
+ ? '<br><span class="badge bg-warning mt-1" style="font-size: 0.7rem;"><i class="bi bi-pause-circle"></i> 禁用</span>'
329
+ : ''}
330
+ </div>
331
+ </td>
332
+ <td>
333
+ <div class="action-buttons">
334
+ <button class="btn btn-sm btn-outline-primary p-1" onclick="showMobileActions(${index})" title="更多操作">
335
+ <i class="bi bi-three-dots-vertical" style="font-size: 0.875rem;"></i>
336
+ </button>
337
+ </div>
338
+ </td>
339
+ </tr>
340
+ `;
341
+ } else {
342
+ // 桌面端完整表格
343
+ return `
344
+ <tr class="cookie-item ${!cookie.enabled ? 'opacity-50' : ''}">
345
+ <td>
346
+ <div class="form-check">
347
+ <input class="form-check-input" type="checkbox"
348
+ ${cookie.enabled ? 'checked' : ''}
349
+ onchange="toggleCookieEnabled('${cookie.userId}', this.checked)"
350
+ title="${cookie.enabled ? '点击禁用' : '点击启用'}">
351
+ </div>
352
+ </td>
353
+ <td>${index + 1}</td>
354
+ <td>
355
+ <code>${cookie.userId}</code>
356
+ </td>
357
+ <td class="mobile-hide">
358
+ <code>${cookie.spaceId}</code>
359
+ </td>
360
+ <td class="mobile-hide">
361
+ <code class="text-muted small">${cookie.cookiePreview || '***'}</code>
362
+ </td>
363
+ <td>
364
+ ${cookie.valid
365
+ ? '<span class="badge bg-success"><i class="status-indicator status-active"></i>有效</span>'
366
+ : '<span class="badge bg-danger"><i class="status-indicator status-inactive"></i>无效</span>'}
367
+ ${!cookie.enabled
368
+ ? '<span class="badge bg-warning ms-1">已禁用</span>'
369
+ : ''}
370
+ </td>
371
+ <td class="mobile-hide">${cookie.lastUsed || '从未使用'}</td>
372
+ <td class="mobile-hide">
373
+ ${cookie.threadId
374
+ ? `<code>${cookie.threadId.substring(0, 12)}...</code>`
375
+ : '<span class="text-muted">未设置</span>'}
376
+ </td>
377
+ <td>
378
+ <div class="action-buttons">
379
+ <button class="btn btn-sm btn-outline-primary" onclick="editThreadId(${index})" title="编辑Thread ID">
380
+ <i class="bi bi-pencil"></i>
381
+ </button>
382
+ <button class="btn btn-sm btn-outline-danger" onclick="deleteCookie(${index})" title="删除">
383
+ <i class="bi bi-trash"></i>
384
+ </button>
385
+ </div>
386
+ </td>
387
+ </tr>
388
+ `;
389
+ }
390
+ }).join('');
391
+ }
392
+ }
393
+
394
+ // 移动端操作菜单
395
+ function showMobileActions(index) {
396
+ const cookie = cookies[index];
397
+
398
+ // 创建操作菜单模态框
399
+ const modalHtml = `
400
+ <div class="modal fade" id="mobileActionsModal" tabindex="-1">
401
+ <div class="modal-dialog modal-dialog-centered modal-sm">
402
+ <div class="modal-content">
403
+ <div class="modal-header">
404
+ <h6 class="modal-title">操作菜单</h6>
405
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
406
+ </div>
407
+ <div class="modal-body p-0">
408
+ <div class="list-group list-group-flush">
409
+ <button class="list-group-item list-group-item-action d-flex align-items-center" onclick="editThreadId(${index}); bootstrap.Modal.getInstance(document.getElementById('mobileActionsModal')).hide();">
410
+ <i class="bi bi-pencil text-primary me-3"></i>
411
+ <div>
412
+ <div class="fw-semibold">编辑Thread ID</div>
413
+ <small class="text-muted">${cookie.threadId ? '修改已有的Thread ID' : '设置新的Thread ID'}</small>
414
+ </div>
415
+ </button>
416
+ <button class="list-group-item list-group-item-action d-flex align-items-center" onclick="viewCookieDetails(${index}); bootstrap.Modal.getInstance(document.getElementById('mobileActionsModal')).hide();">
417
+ <i class="bi bi-info-circle text-info me-3"></i>
418
+ <div>
419
+ <div class="fw-semibold">查看详情</div>
420
+ <small class="text-muted">查看Cookie完整信息</small>
421
+ </div>
422
+ </button>
423
+ <button class="list-group-item list-group-item-action d-flex align-items-center text-danger" onclick="deleteCookie(${index}); bootstrap.Modal.getInstance(document.getElementById('mobileActionsModal')).hide();">
424
+ <i class="bi bi-trash me-3"></i>
425
+ <div>
426
+ <div class="fw-semibold">删除Cookie</div>
427
+ <small class="text-muted">此操作不可恢复</small>
428
+ </div>
429
+ </button>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ `;
436
+
437
+ // 移除旧的模态框
438
+ const oldModal = document.getElementById('mobileActionsModal');
439
+ if (oldModal) {
440
+ oldModal.remove();
441
+ }
442
+
443
+ // 添加新模态框
444
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
445
+
446
+ // 显示模态框
447
+ const modal = new bootstrap.Modal(document.getElementById('mobileActionsModal'));
448
+ modal.show();
449
+ }
450
+
451
+ // 查看Cookie详情(移动端)
452
+ function viewCookieDetails(index) {
453
+ const cookie = cookies[index];
454
+
455
+ const detailsHtml = `
456
+ <div class="modal fade" id="cookieDetailsModal" tabindex="-1">
457
+ <div class="modal-dialog modal-dialog-centered">
458
+ <div class="modal-content">
459
+ <div class="modal-header">
460
+ <h5 class="modal-title">Cookie详情</h5>
461
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
462
+ </div>
463
+ <div class="modal-body">
464
+ <dl class="row mb-0">
465
+ <dt class="col-4">用户ID:</dt>
466
+ <dd class="col-8"><code class="text-break">${cookie.userId}</code></dd>
467
+
468
+ <dt class="col-4">空间ID:</dt>
469
+ <dd class="col-8"><code class="text-break">${cookie.spaceId}</code></dd>
470
+
471
+ <dt class="col-4">状态:</dt>
472
+ <dd class="col-8">
473
+ ${cookie.valid ? '<span class="badge bg-success">有效</span>' : '<span class="badge bg-danger">无效</span>'}
474
+ ${!cookie.enabled ? ' <span class="badge bg-warning">已禁用</span>' : ''}
475
+ </dd>
476
+
477
+ <dt class="col-4">Thread ID:</dt>
478
+ <dd class="col-8">${cookie.threadId ? `<code class="text-break">${cookie.threadId}</code>` : '<span class="text-muted">未设置</span>'}</dd>
479
+
480
+ <dt class="col-4">最后使用:</dt>
481
+ <dd class="col-8">${cookie.lastUsed || '从未使用'}</dd>
482
+
483
+ <dt class="col-4">Cookie预览:</dt>
484
+ <dd class="col-8"><code class="text-break small">${cookie.cookiePreview || '***'}</code></dd>
485
+ </dl>
486
+ </div>
487
+ <div class="modal-footer">
488
+ <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">关闭</button>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ </div>
493
+ `;
494
+
495
+ // 移除旧的模态框
496
+ const oldModal = document.getElementById('cookieDetailsModal');
497
+ if (oldModal) {
498
+ oldModal.remove();
499
+ }
500
+
501
+ // 添加新模态框
502
+ document.body.insertAdjacentHTML('beforeend', detailsHtml);
503
+
504
+ // 显示模态框
505
+ const modal = new bootstrap.Modal(document.getElementById('cookieDetailsModal'));
506
+ modal.show();
507
+ }
508
+
509
+ // 切换Cookie启用状态
510
+ async function toggleCookieEnabled(userId, enabled) {
511
+ try {
512
+ const response = await fetch(`/cookies/${userId}/toggle`, {
513
+ method: 'PUT',
514
+ headers: {
515
+ 'Content-Type': 'application/json',
516
+ 'Authorization': `Bearer ${authToken}`
517
+ },
518
+ body: JSON.stringify({ enabled })
519
+ });
520
+
521
+ if (!response.ok) {
522
+ throw new Error(`HTTP error! status: ${response.status}`);
523
+ }
524
+
525
+ // 更新本地数据
526
+ const cookie = cookies.find(c => c.userId === userId);
527
+ if (cookie) {
528
+ cookie.enabled = enabled;
529
+ }
530
+
531
+ // 更新UI
532
+ updateUI();
533
+
534
+ showToast(`Cookie已${enabled ? '启用' : '禁用'}`, 'success');
535
+ addLog(`${enabled ? '启用' : '禁用'}了用户 ${userId} 的Cookie`, 'info');
536
+ } catch (error) {
537
+ console.error('切换Cookie状态失败:', error);
538
+ showToast('切换Cookie状态失败', 'danger');
539
+ addLog(`切换Cookie状态失败: ${error.message}`, 'error');
540
+
541
+ // 恢复原状态
542
+ await loadCookies();
543
+ }
544
+ }
545
+
546
+ // 刷新Cookie状态
547
+ async function refreshCookies() {
548
+ showLoading();
549
+ addLog('正在刷新Cookie状态...');
550
+
551
+ try {
552
+ const response = await fetch('/cookies/refresh', {
553
+ method: 'POST',
554
+ headers: {
555
+ 'Authorization': `Bearer ${authToken}`
556
+ }
557
+ });
558
+
559
+ if (!response.ok) {
560
+ throw new Error(`HTTP error! status: ${response.status}`);
561
+ }
562
+
563
+ await loadCookies();
564
+ showToast('Cookie状态已刷新', 'success');
565
+ addLog('Cookie状态刷新成功', 'success');
566
+ } catch (error) {
567
+ console.error('刷新失败:', error);
568
+ showToast('刷新Cookie状态失败', 'danger');
569
+ addLog(`刷新失败: ${error.message}`, 'error');
570
+ } finally {
571
+ hideLoading();
572
+ }
573
+ }
574
+
575
+ // 添加Cookie
576
+ async function addCookie() {
577
+ const cookieContent = document.getElementById('cookieContent').value.trim();
578
+ const threadId = document.getElementById('cookieThreadId').value.trim();
579
+
580
+ if (!cookieContent) {
581
+ showToast('请输入Cookie内容', 'warning');
582
+ return;
583
+ }
584
+
585
+ showLoading();
586
+ addLog('正在添加新Cookie...');
587
+
588
+ try {
589
+ const response = await fetch('/cookies/add', {
590
+ method: 'POST',
591
+ headers: {
592
+ 'Content-Type': 'application/json',
593
+ 'Authorization': `Bearer ${authToken}`
594
+ },
595
+ body: JSON.stringify({
596
+ cookies: cookieContent,
597
+ threadId: threadId || undefined
598
+ })
599
+ });
600
+
601
+ const result = await response.json();
602
+
603
+ if (!response.ok) {
604
+ throw new Error(result.error?.message || `HTTP error! status: ${response.status}`);
605
+ }
606
+
607
+ // 显示详细结果
608
+ console.log('添加Cookie结果:', result);
609
+
610
+ // 关闭模态框
611
+ bootstrap.Modal.getInstance(document.getElementById('addCookieModal')).hide();
612
+
613
+ // 清空表单
614
+ document.getElementById('cookieContent').value = '';
615
+ document.getElementById('cookieThreadId').value = '';
616
+
617
+ // 重新加载数据
618
+ await loadCookies();
619
+
620
+ // 显示详细的结果信息
621
+ if (result.added > 0) {
622
+ showToast(`成功添加 ${result.added} 个Cookie`, 'success');
623
+ addLog(`成功添加 ${result.added} 个Cookie`, 'success');
624
+ } else if (result.failed > 0) {
625
+ showToast(`添加失败: ${result.failed} 个Cookie无效`, 'danger');
626
+ addLog(`添加失败: ${result.failed} 个Cookie无效`, 'error');
627
+
628
+ // 如果有错误详情,显示它们
629
+ if (result.errors && result.errors.length > 0) {
630
+ result.errors.forEach(error => {
631
+ addLog(`错误详情: ${error}`, 'error');
632
+ });
633
+ }
634
+ } else {
635
+ showToast('未添加任何Cookie', 'warning');
636
+ addLog('未添加任何Cookie', 'warning');
637
+ }
638
+ } catch (error) {
639
+ console.error('添加Cookie失败:', error);
640
+ showToast(`添加Cookie失败: ${error.message}`, 'danger');
641
+ addLog(`添加Cookie失败: ${error.message}`, 'error');
642
+ } finally {
643
+ hideLoading();
644
+ }
645
+ }
646
+
647
+ // 编辑Thread ID
648
+ function editThreadId(index) {
649
+ const cookie = cookies[index];
650
+
651
+ document.getElementById('editCookieIndex').value = index;
652
+ document.getElementById('editUserId').value = cookie.userId;
653
+ document.getElementById('editThreadId').value = cookie.threadId || '';
654
+
655
+ const modal = new bootstrap.Modal(document.getElementById('editThreadIdModal'));
656
+ modal.show();
657
+ }
658
+
659
+ // 保存Thread ID
660
+ async function saveThreadId() {
661
+ const index = parseInt(document.getElementById('editCookieIndex').value);
662
+ const threadId = document.getElementById('editThreadId').value.trim();
663
+ const cookie = cookies[index];
664
+
665
+ showLoading();
666
+ addLog(`正在更新用户 ${cookie.userId} 的Thread ID...`);
667
+
668
+ try {
669
+ const response = await fetch('/cookies/thread', {
670
+ method: 'PUT',
671
+ headers: {
672
+ 'Content-Type': 'application/json',
673
+ 'Authorization': `Bearer ${authToken}`
674
+ },
675
+ body: JSON.stringify({
676
+ userId: cookie.userId,
677
+ threadId: threadId || null
678
+ })
679
+ });
680
+
681
+ if (!response.ok) {
682
+ throw new Error(`HTTP error! status: ${response.status}`);
683
+ }
684
+
685
+ // 关闭模态框
686
+ bootstrap.Modal.getInstance(document.getElementById('editThreadIdModal')).hide();
687
+
688
+ // 重新加载数据
689
+ await loadCookies();
690
+
691
+ showToast('Thread ID已更新', 'success');
692
+ addLog(`成功更新用户 ${cookie.userId} 的Thread ID`, 'success');
693
+ } catch (error) {
694
+ console.error('更新Thread ID失败:', error);
695
+ showToast('更新Thread ID失败', 'danger');
696
+ addLog(`更新Thread ID失败: ${error.message}`, 'error');
697
+ } finally {
698
+ hideLoading();
699
+ }
700
+ }
701
+
702
+ // 删除Cookie
703
+ async function deleteCookie(index) {
704
+ const cookie = cookies[index];
705
+
706
+ if (!confirm(`确定要删除用户 ${cookie.userId} 的Cookie吗?`)) {
707
+ return;
708
+ }
709
+
710
+ showLoading();
711
+ addLog(`正在删除用户 ${cookie.userId} 的Cookie...`);
712
+
713
+ try {
714
+ const response = await fetch(`/cookies/${cookie.userId}`, {
715
+ method: 'DELETE',
716
+ headers: {
717
+ 'Authorization': `Bearer ${authToken}`
718
+ }
719
+ });
720
+
721
+ if (!response.ok) {
722
+ throw new Error(`HTTP error! status: ${response.status}`);
723
+ }
724
+
725
+ // 重新加载数据
726
+ await loadCookies();
727
+
728
+ showToast('Cookie已删除', 'success');
729
+ addLog(`成功删除用户 ${cookie.userId} 的Cookie`, 'success');
730
+ } catch (error) {
731
+ console.error('删除Cookie失败:', error);
732
+ showToast('删除Cookie失败', 'danger');
733
+ addLog(`删除Cookie失败: ${error.message}`, 'error');
734
+ } finally {
735
+ hideLoading();
736
+ }
737
+ }
738
+
739
+ // 导出功能(可选)
740
+ function exportCookies() {
741
+ const dataStr = JSON.stringify(cookies, null, 2);
742
+ const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
743
+
744
+ const exportFileDefaultName = `cookies_${new Date().toISOString().split('T')[0]}.json`;
745
+
746
+ const linkElement = document.createElement('a');
747
+ linkElement.setAttribute('href', dataUri);
748
+ linkElement.setAttribute('download', exportFileDefaultName);
749
+ linkElement.click();
750
+
751
+ showToast('Cookie数据已导出', 'success');
752
+ addLog('导出Cookie数据', 'info');
753
+ }
754
+
755
+ // 触摸事件优化
756
+ if ('ontouchstart' in window) {
757
+ document.addEventListener('touchstart', function() {}, {passive: true});
758
+ }
src/CookieManager.js CHANGED
@@ -4,6 +4,7 @@ import fs from 'fs';
4
  import path from 'path';
5
  import { fileURLToPath } from 'url';
6
  import { dirname } from 'path';
 
7
 
8
  // 获取当前文件的目录路径
9
  const __filename = fileURLToPath(import.meta.url);
@@ -124,7 +125,9 @@ class CookieManager {
124
  spaceId: result.spaceId,
125
  userId: result.userId,
126
  valid: true,
127
- lastUsed: 0 // 记录上次使用时间戳
 
 
128
  });
129
  logger.success(`第 ${i+1} 个cookie验证成功`);
130
  } else {
@@ -142,6 +145,16 @@ class CookieManager {
142
  return false;
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
145
  logger.success(`成功初始化 ${this.cookieEntries.length}/${cookieArray.length} 个cookie`);
146
  this.initialized = true;
147
  this.currentIndex = 0;
@@ -354,11 +367,33 @@ class CookieManager {
354
  return null;
355
  }
356
 
357
- // 轮询选择下一个cookie
358
- const entry = this.cookieEntries[this.currentIndex];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  // 更新索引,实现轮询
361
- this.currentIndex = (this.currentIndex + 1) % this.cookieEntries.length;
362
 
363
  // 更新最后使用时间
364
  entry.lastUsed = Date.now();
@@ -366,7 +401,8 @@ class CookieManager {
366
  return {
367
  cookie: entry.cookie,
368
  spaceId: entry.spaceId,
369
- userId: entry.userId
 
370
  };
371
  }
372
 
@@ -395,7 +431,7 @@ class CookieManager {
395
  * @returns {number} - 有效cookie的数量
396
  */
397
  getValidCount() {
398
- return this.cookieEntries.filter(entry => entry.valid).length;
399
  }
400
 
401
  /**
@@ -405,12 +441,136 @@ class CookieManager {
405
  getStatus() {
406
  return this.cookieEntries.map((entry, index) => ({
407
  index,
408
- userId: entry.userId.substring(0, 8) + '...',
409
- spaceId: entry.spaceId.substring(0, 8) + '...',
410
  valid: entry.valid,
411
- lastUsed: entry.lastUsed ? new Date(entry.lastUsed).toLocaleString() : 'never'
 
 
 
412
  }));
413
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
415
 
416
- export const cookieManager = new CookieManager();
 
4
  import path from 'path';
5
  import { fileURLToPath } from 'url';
6
  import { dirname } from 'path';
7
+ import { storageManager } from './utils/storage.js';
8
 
9
  // 获取当前文件的目录路径
10
  const __filename = fileURLToPath(import.meta.url);
 
125
  spaceId: result.spaceId,
126
  userId: result.userId,
127
  valid: true,
128
+ enabled: true, // 新增enabled字段,默认启用
129
+ lastUsed: 0, // 记录上次使用时间戳
130
+ threadId: null // 新增threadId字段
131
  });
132
  logger.success(`第 ${i+1} 个cookie验证成功`);
133
  } else {
 
145
  return false;
146
  }
147
 
148
+ // 尝试加载之前保存的数据(如Thread ID等)
149
+ const savedData = storageManager.loadCookieData();
150
+ if (savedData) {
151
+ this.cookieEntries = storageManager.mergeCookieData(this.cookieEntries, savedData);
152
+ logger.info('已恢复保存的Cookie数据');
153
+ }
154
+
155
+ // 保存当前数据
156
+ storageManager.saveCookieData(this.cookieEntries);
157
+
158
  logger.success(`成功初始化 ${this.cookieEntries.length}/${cookieArray.length} 个cookie`);
159
  this.initialized = true;
160
  this.currentIndex = 0;
 
367
  return null;
368
  }
369
 
370
+ // 获取所有启用且有效的cookie
371
+ const enabledEntries = this.cookieEntries.filter(entry => entry.valid && entry.enabled);
372
+
373
+ if (enabledEntries.length === 0) {
374
+ logger.warning('没有启用的有效cookie');
375
+
376
+ // 检查是否有有效但被禁用的cookie
377
+ const disabledValidEntries = this.cookieEntries.filter(entry => entry.valid && !entry.enabled);
378
+ if (disabledValidEntries.length > 0) {
379
+ logger.warning(`发现 ${disabledValidEntries.length} 个有效但被禁用的cookie,自动启用第一个`);
380
+ // 自动启用第一个有效的cookie
381
+ disabledValidEntries[0].enabled = true;
382
+ // 保存更新后的数据
383
+ storageManager.saveCookieData(this.cookieEntries);
384
+
385
+ // 递归调用以返回启用的cookie
386
+ return this.getNext();
387
+ }
388
+
389
+ return null;
390
+ }
391
+
392
+ // 在启用的cookie中轮询
393
+ const entry = enabledEntries[this.currentIndex % enabledEntries.length];
394
 
395
  // 更新索引,实现轮询
396
+ this.currentIndex = (this.currentIndex + 1) % enabledEntries.length;
397
 
398
  // 更新最后使用时间
399
  entry.lastUsed = Date.now();
 
401
  return {
402
  cookie: entry.cookie,
403
  spaceId: entry.spaceId,
404
+ userId: entry.userId,
405
+ threadId: entry.threadId // 返回threadId
406
  };
407
  }
408
 
 
431
  * @returns {number} - 有效cookie的数量
432
  */
433
  getValidCount() {
434
+ return this.cookieEntries.filter(entry => entry.valid && entry.enabled).length;
435
  }
436
 
437
  /**
 
441
  getStatus() {
442
  return this.cookieEntries.map((entry, index) => ({
443
  index,
444
+ userId: entry.userId,
445
+ spaceId: entry.spaceId,
446
  valid: entry.valid,
447
+ enabled: entry.enabled !== false, // 确保兼容旧数据
448
+ lastUsed: entry.lastUsed ? new Date(entry.lastUsed).toLocaleString() : 'never',
449
+ threadId: entry.threadId,
450
+ cookiePreview: this.getCookiePreview(entry.cookie) // 添加cookie预览
451
  }));
452
  }
453
+
454
+ /**
455
+ * 获取cookie的预览(脱敏显示)
456
+ * @param {string} cookie - 完整的cookie字符串
457
+ * @returns {string} - 脱敏后的cookie预览
458
+ */
459
+ getCookiePreview(cookie) {
460
+ if (!cookie || cookie.length < 20) {
461
+ return '***';
462
+ }
463
+ // 显示前10个字符和后10个字符
464
+ return `${cookie.substring(0, 10)}...${cookie.substring(cookie.length - 10)}`;
465
+ }
466
+
467
+ /**
468
+ * 设置指定用户的threadId
469
+ * @param {string} userId - 用户ID
470
+ * @param {string|null} threadId - Thread ID
471
+ * @returns {boolean} - 是否设置成功
472
+ */
473
+ setThreadId(userId, threadId) {
474
+ const entry = this.cookieEntries.find(e => e.userId === userId);
475
+ if (entry) {
476
+ entry.threadId = threadId;
477
+ logger.info(`已为用户 ${userId} 设置Thread ID: ${threadId || '(null)'}`);
478
+
479
+ // 保存更新后的数据
480
+ storageManager.saveCookieData(this.cookieEntries);
481
+
482
+ return true;
483
+ }
484
+ return false;
485
+ }
486
+
487
+ /**
488
+ * 添加新的cookie
489
+ * @param {string} cookieString - cookie字符串
490
+ * @param {string|null} threadId - 可选的Thread ID
491
+ * @returns {Promise<Object>} - 添加结果
492
+ */
493
+ async addCookie(cookieString, threadId = null) {
494
+ try {
495
+ const result = await this.fetchNotionIds(cookieString);
496
+ if (result.success) {
497
+ // 检查是否已存在
498
+ const existing = this.cookieEntries.find(e => e.userId === result.userId);
499
+ if (existing) {
500
+ existing.cookie = cookieString;
501
+ existing.valid = true;
502
+ if (threadId !== undefined) {
503
+ existing.threadId = threadId;
504
+ }
505
+ logger.info(`更新了现有cookie: ${result.userId}`);
506
+ } else {
507
+ this.cookieEntries.push({
508
+ cookie: cookieString,
509
+ spaceId: result.spaceId,
510
+ userId: result.userId,
511
+ valid: true,
512
+ enabled: true,
513
+ lastUsed: 0,
514
+ threadId: threadId
515
+ });
516
+ logger.info(`添加了新cookie: ${result.userId}`);
517
+ }
518
+
519
+ // 保存更新后的数据
520
+ storageManager.saveCookieData(this.cookieEntries);
521
+
522
+ return { success: true, userId: result.userId };
523
+ } else {
524
+ return { success: false, error: result.error };
525
+ }
526
+ } catch (error) {
527
+ return { success: false, error: error.message };
528
+ }
529
+ }
530
+
531
+ /**
532
+ * 删除指定用户的cookie
533
+ * @param {string} userId - 用户ID
534
+ * @returns {boolean} - 是否删除成功
535
+ */
536
+ deleteCookie(userId) {
537
+ const index = this.cookieEntries.findIndex(e => e.userId === userId);
538
+ if (index !== -1) {
539
+ this.cookieEntries.splice(index, 1);
540
+ logger.info(`已删除用户 ${userId} 的cookie`);
541
+
542
+ // 重置当前索引
543
+ if (this.cookieEntries.length > 0 && this.currentIndex >= this.cookieEntries.length) {
544
+ this.currentIndex = 0;
545
+ }
546
+
547
+ // 保存更新后的数据
548
+ storageManager.saveCookieData(this.cookieEntries);
549
+
550
+ return true;
551
+ }
552
+ return false;
553
+ }
554
+
555
+ /**
556
+ * 切换cookie的启用状态
557
+ * @param {string} userId - 用户ID
558
+ * @param {boolean} enabled - 是否启用
559
+ * @returns {boolean} - 是否操作成功
560
+ */
561
+ toggleCookie(userId, enabled) {
562
+ const entry = this.cookieEntries.find(e => e.userId === userId);
563
+ if (entry) {
564
+ entry.enabled = enabled;
565
+ logger.info(`已${enabled ? '启用' : '禁用'}用户 ${userId} 的cookie`);
566
+
567
+ // 保存更新后的数据
568
+ storageManager.saveCookieData(this.cookieEntries);
569
+
570
+ return true;
571
+ }
572
+ return false;
573
+ }
574
  }
575
 
576
+ export const cookieManager = new CookieManager();
src/ProxyPool.js CHANGED
@@ -9,15 +9,15 @@ class ProxyPool {
9
  * @param {Object} options - 配置选项
10
  * @param {number} options.targetCount - 目标代理数量,默认20
11
  * @param {number} options.batchSize - 每次获取的代理数量,默认20
12
- * @param {number} options.testTimeout - 测试代理超时时间(毫秒),默认5000
13
- * @param {number} options.requestTimeout - 请求目标网站超时时间(毫秒),默认10000
14
  * @param {string} options.targetUrl - 目标网站URL,默认'https://www.notion.so'
15
- * @param {number} options.concurrentRequests - 并发请求数量,默认10
16
  * @param {number} options.minThreshold - 可用代理数量低于此阈值时自动补充,默认5
17
  * @param {number} options.checkInterval - 检查代理池状态的时间间隔(毫秒),默认30000
18
  * @param {string} options.proxyProtocol - 代理协议,默认'http'
19
- * @param {number} options.maxRefillAttempts - 最大补充尝试次数,默认20
20
- * @param {number} options.retryDelay - 重试延迟(毫秒),默认1000
21
  * @param {boolean} options.useCache - 是否使用缓存,默认true
22
  * @param {number} options.cacheExpiry - 缓存过期时间(毫秒),默认3600000 (1小时)
23
  * @param {string} options.logLevel - 日志级别,可选值:'debug', 'info', 'warn', 'error', 'none',默认'info'
@@ -27,15 +27,15 @@ class ProxyPool {
27
  // 配置参数
28
  this.targetCount = options.targetCount || 20;
29
  this.batchSize = options.batchSize || 20;
30
- this.testTimeout = options.testTimeout || 5000;
31
- this.requestTimeout = options.requestTimeout || 10000;
32
  this.targetUrl = options.targetUrl || 'https://www.notion.so';
33
- this.concurrentRequests = options.concurrentRequests || 10;
34
  this.minThreshold = options.minThreshold || 5;
35
  this.checkInterval = options.checkInterval || 30000; // 默认30秒检查一次
36
  this.proxyProtocol = options.proxyProtocol || 'http';
37
- this.maxRefillAttempts = options.maxRefillAttempts || 30; // 减少最大尝试次数
38
- this.retryDelay = options.retryDelay || 1000; // 减少重试延迟
39
  this.useCache = options.useCache !== undefined ? options.useCache : true;
40
  this.cacheExpiry = options.cacheExpiry || 3600000; // 默认1小时
41
  this.logLevel = options.logLevel || 'info'; // 默认日志级别为info
@@ -237,7 +237,7 @@ class ProxyPool {
237
 
238
  // 计算本次需要获取的批次大小
239
  const remainingNeeded = this.targetCount - this.availableProxies.length;
240
- const batchSizeNeeded = remainingNeeded; // 获取更多代理以提高成功率
241
 
242
  // 获取代理
243
  const proxies = await this.getProxiesFromProvider(batchSizeNeeded);
@@ -253,6 +253,7 @@ class ProxyPool {
253
 
254
  if (newProxies.length === 0) {
255
  this.log('debug', '所有获取的代理都已存在,继续获取新代理...');
 
256
  continue;
257
  }
258
 
@@ -274,9 +275,9 @@ class ProxyPool {
274
  break;
275
  }
276
 
277
- // 如果还没补充到足够的代理,等待一段时间再继续
278
  if (this.availableProxies.length < this.targetCount) {
279
- await new Promise(resolve => setTimeout(resolve, this.retryDelay));
280
  }
281
  }
282
  } catch (error) {
@@ -426,18 +427,36 @@ class ProxyPool {
426
  async getProxiesFromProvider(count = null) {
427
  try {
428
  const requestCount = count || this.batchSize;
429
- // 限制请求数量最大为10
430
  const actualCount = Math.min(requestCount, 10);
431
- const url = `https://proxy.doudouzi.me/random/${this.proxyCountry}?number=${actualCount}&protocol=${this.proxyProtocol}&type=json`;
432
- this.log('debug', `正在获取代理,URL: ${url}`);
433
 
434
- const response = await axios.get(url, {
435
- timeout: 10000,
436
- validateStatus: status => true
437
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- if (response.data) {
440
- let proxies = [];
 
 
 
 
 
 
441
 
442
  // 处理不同的返回格式
443
  if (typeof response.data === 'string') {
@@ -483,13 +502,10 @@ class ProxyPool {
483
  }
484
  }
485
  }
486
-
487
- this.log('debug', `成功获取 ${proxies.length} 个代理`);
488
- return proxies;
489
- } else {
490
- this.log('error', '获取代理失败: 返回数据格式不正确');
491
- return [];
492
  }
 
 
 
493
  } catch (error) {
494
  this.log('error', '获取代理出错:', error.message);
495
  return [];
@@ -506,7 +522,7 @@ class ProxyPool {
506
  const remainingNeeded = this.targetCount - this.availableProxies.length;
507
 
508
  // 增加并发数以加快处理速度
509
- const concurrentRequests = Math.min(this.concurrentRequests * 2, 20);
510
 
511
  // 分批处理代理
512
  for (let i = 0; i < proxies.length; i += concurrentRequests) {
@@ -586,9 +602,9 @@ class ProxyPool {
586
  'Connection': 'keep-alive',
587
  'Upgrade-Insecure-Requests': '1'
588
  },
589
- timeout: this.requestTimeout,
590
  validateStatus: status => true,
591
- maxRedirects: 10,
592
  followRedirect: true
593
  });
594
 
@@ -722,10 +738,12 @@ async function example() {
722
  minThreshold: 3, // 当可用代理少于3个时,自动补充
723
  checkInterval: 60000, // 每60秒检查一次
724
  targetUrl: 'https://www.notion.so',
725
- concurrentRequests: 15, // 增加并发请求数
726
  useCache: true, // 启用缓存
727
- maxRefillAttempts: 15, // 减少最大尝试次数
728
- retryDelay: 1000, // 减少重试延迟
 
 
729
  logLevel: 'info', // 设置日志级别
730
  showProgressBar: true // 启用进度条
731
  });
 
9
  * @param {Object} options - 配置选项
10
  * @param {number} options.targetCount - 目标代理数量,默认20
11
  * @param {number} options.batchSize - 每次获取的代理数量,默认20
12
+ * @param {number} options.testTimeout - 测试代理超时时间(毫秒),默认3000
13
+ * @param {number} options.requestTimeout - 请求目标网站超时时间(毫秒),默认5000
14
  * @param {string} options.targetUrl - 目标网站URL,默认'https://www.notion.so'
15
+ * @param {number} options.concurrentRequests - 并发请求数量,默认15
16
  * @param {number} options.minThreshold - 可用代理数量低于此阈值时自动补充,默认5
17
  * @param {number} options.checkInterval - 检查代理池状态的时间间隔(毫秒),默认30000
18
  * @param {string} options.proxyProtocol - 代理协议,默认'http'
19
+ * @param {number} options.maxRefillAttempts - 最大补充尝试次数,默认50
20
+ * @param {number} options.retryDelay - 重试延迟(毫秒),默认500
21
  * @param {boolean} options.useCache - 是否使用缓存,默认true
22
  * @param {number} options.cacheExpiry - 缓存过期时间(毫秒),默认3600000 (1小时)
23
  * @param {string} options.logLevel - 日志级别,可选值:'debug', 'info', 'warn', 'error', 'none',默认'info'
 
27
  // 配置参数
28
  this.targetCount = options.targetCount || 20;
29
  this.batchSize = options.batchSize || 20;
30
+ this.testTimeout = options.testTimeout || 3000; // 减少测试超时时间
31
+ this.requestTimeout = options.requestTimeout || 5000; // 减少请求超时时间
32
  this.targetUrl = options.targetUrl || 'https://www.notion.so';
33
+ this.concurrentRequests = options.concurrentRequests || 15; // 增加默认并发请求数
34
  this.minThreshold = options.minThreshold || 5;
35
  this.checkInterval = options.checkInterval || 30000; // 默认30秒检查一次
36
  this.proxyProtocol = options.proxyProtocol || 'http';
37
+ this.maxRefillAttempts = options.maxRefillAttempts || 50; // 增加最大尝试次数
38
+ this.retryDelay = options.retryDelay || 500; // 减少重试延迟
39
  this.useCache = options.useCache !== undefined ? options.useCache : true;
40
  this.cacheExpiry = options.cacheExpiry || 3600000; // 默认1小时
41
  this.logLevel = options.logLevel || 'info'; // 默认日志级别为info
 
237
 
238
  // 计算本次需要获取的批次大小
239
  const remainingNeeded = this.targetCount - this.availableProxies.length;
240
+ const batchSizeNeeded = Math.max(remainingNeeded * 2, 10); // 获取更多代理以提高成功率,至少10个
241
 
242
  // 获取代理
243
  const proxies = await this.getProxiesFromProvider(batchSizeNeeded);
 
253
 
254
  if (newProxies.length === 0) {
255
  this.log('debug', '所有获取的代理都已存在,继续获取新代理...');
256
+ // 减少等待时间,立即继续尝试
257
  continue;
258
  }
259
 
 
275
  break;
276
  }
277
 
278
+ // 如果还没补充到足够的代理,减少等待时间再继续
279
  if (this.availableProxies.length < this.targetCount) {
280
+ await new Promise(resolve => setTimeout(resolve, 500)); // 减少等待时间到500毫秒
281
  }
282
  }
283
  } catch (error) {
 
427
  async getProxiesFromProvider(count = null) {
428
  try {
429
  const requestCount = count || this.batchSize;
430
+ // 限制单次请求数量最大为10
431
  const actualCount = Math.min(requestCount, 10);
 
 
432
 
433
+ // 计算需要发送的请求数量,以获取足够的代理
434
+ const requestsNeeded = Math.ceil(requestCount / 1); // 假设每次请求只返回1个代理
435
+ const maxParallelRequests = 5; // 最大并行请求数
436
+ const actualRequests = Math.min(requestsNeeded, maxParallelRequests);
437
+
438
+ this.log('debug', `需要 ${requestCount} 个代理,将发送 ${actualRequests} 个并行请求`);
439
+
440
+ // 并行发送多个请求
441
+ const requestPromises = [];
442
+ for (let i = 0; i < actualRequests; i++) {
443
+ const url = `https://proxy.doudouzi.me/random/${this.proxyCountry}?number=${actualCount}&protocol=${this.proxyProtocol}&type=json`;
444
+ requestPromises.push(
445
+ axios.get(url, {
446
+ timeout: 10000,
447
+ validateStatus: status => true
448
+ })
449
+ );
450
+ }
451
 
452
+ // 等待所有请求完成
453
+ const responses = await Promise.all(requestPromises);
454
+
455
+ // 处理所有响应,合并代理列表
456
+ let proxies = [];
457
+
458
+ for (const response of responses) {
459
+ if (!response.data) continue;
460
 
461
  // 处理不同的返回格式
462
  if (typeof response.data === 'string') {
 
502
  }
503
  }
504
  }
 
 
 
 
 
 
505
  }
506
+
507
+ this.log('debug', `成功获取 ${proxies.length} 个代理`);
508
+ return proxies;
509
  } catch (error) {
510
  this.log('error', '获取代理出错:', error.message);
511
  return [];
 
522
  const remainingNeeded = this.targetCount - this.availableProxies.length;
523
 
524
  // 增加并发数以加快处理速度
525
+ const concurrentRequests = Math.min(this.concurrentRequests * 3, 30); // 增加到最多30个并发请求
526
 
527
  // 分批处理代理
528
  for (let i = 0; i < proxies.length; i += concurrentRequests) {
 
602
  'Connection': 'keep-alive',
603
  'Upgrade-Insecure-Requests': '1'
604
  },
605
+ timeout: Math.min(this.requestTimeout, 5000), // 减少超时时间,最多5秒
606
  validateStatus: status => true,
607
+ maxRedirects: 5, // 减少最大重定向次数
608
  followRedirect: true
609
  });
610
 
 
738
  minThreshold: 3, // 当可用代理少于3个时,自动补充
739
  checkInterval: 60000, // 每60秒检查一次
740
  targetUrl: 'https://www.notion.so',
741
+ concurrentRequests: 20, // 增加并发请求数
742
  useCache: true, // 启用缓存
743
+ maxRefillAttempts: 50, // 增加最大尝试次数
744
+ retryDelay: 500, // 减少重试延迟
745
+ testTimeout: 3000, // 减少测试超时时间
746
+ requestTimeout: 5000, // 减少请求超时时间
747
  logLevel: 'info', // 设置日志级别
748
  showProgressBar: true // 启用进度条
749
  });
src/ProxyServer.js CHANGED
@@ -26,7 +26,7 @@ class ProxyServer {
26
  this.proxyProcess = null;
27
  this.platform = process.env.PROXY_SERVER_PLATFORM || 'auto';
28
  this.port = process.env.PROXY_SERVER_PORT || 10655;
29
- this.logPath = process.env.PROXY_SERVER_LOG_PATH || '/tmp/proxy_server.log';
30
  this.enabled = process.env.ENABLE_PROXY_SERVER === 'true';
31
  this.proxyAuthToken = process.env.PROXY_AUTH_TOKEN || 'default_token';
32
  this.logStream = null;
@@ -95,14 +95,13 @@ class ProxyServer {
95
 
96
  try {
97
  // 确保可执行文件有执行权限(在Linux/Android上)
98
- // 在Hugging Face环境中,文件权限应在Dockerfile中设置,运行时修改可能会失败
99
- // if (this.detectPlatform() !== 'windows') {
100
- // try {
101
- // fs.chmodSync(proxyServerPath, 0o755);
102
- // } catch (err) {
103
- // logger.warning(`无法设置执行权限: ${err.message}`);
104
- // }
105
- // }
106
 
107
  // 创建日志文件
108
  this.logStream = fs.createWriteStream(this.logPath, { flags: 'a' });
 
26
  this.proxyProcess = null;
27
  this.platform = process.env.PROXY_SERVER_PLATFORM || 'auto';
28
  this.port = process.env.PROXY_SERVER_PORT || 10655;
29
+ this.logPath = process.env.PROXY_SERVER_LOG_PATH || './proxy_server.log';
30
  this.enabled = process.env.ENABLE_PROXY_SERVER === 'true';
31
  this.proxyAuthToken = process.env.PROXY_AUTH_TOKEN || 'default_token';
32
  this.logStream = null;
 
95
 
96
  try {
97
  // 确保可执行文件有执行权限(在Linux/Android上)
98
+ if (this.detectPlatform() !== 'windows') {
99
+ try {
100
+ fs.chmodSync(proxyServerPath, 0o755);
101
+ } catch (err) {
102
+ logger.warning(`无法设置执行权限: ${err.message}`);
103
+ }
104
+ }
 
105
 
106
  // 创建日志文件
107
  this.logStream = fs.createWriteStream(this.logPath, { flags: 'a' });
src/app.js ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { createLogger } from './utils/logger.js';
5
+ import { config, validateConfig } from './config/index.js';
6
+ import { notionClient } from './services/NotionClient.js';
7
+ import { streamManager } from './services/StreamManager.js';
8
+ import { proxyPool } from './ProxyPool.js';
9
+ import { proxyServer } from './ProxyServer.js';
10
+ import { requestLogger, errorHandler, requestLimits } from './middleware/auth.js';
11
+ import apiRouter from './routes/api.js';
12
+
13
+ // 获取当前目录
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ const logger = createLogger('App');
18
+
19
+ /**
20
+ * 应用程序类
21
+ * 负责初始化和管理整个应用
22
+ */
23
+ class Application {
24
+ constructor() {
25
+ this.app = express();
26
+ this.server = null;
27
+ }
28
+
29
+ /**
30
+ * 配置Express中间件
31
+ */
32
+ configureMiddleware() {
33
+ // 请求体解析
34
+ this.app.use(express.json(requestLimits.json));
35
+ this.app.use(express.urlencoded(requestLimits.urlencoded));
36
+
37
+ // 静态文件服务
38
+ const publicPath = join(dirname(__dirname), 'public');
39
+ this.app.use(express.static(publicPath));
40
+
41
+ // 管理界面路由
42
+ this.app.get('/admin', (req, res) => {
43
+ res.sendFile(join(publicPath, 'admin.html'));
44
+ });
45
+
46
+ // 请求日志
47
+ this.app.use(requestLogger);
48
+
49
+ // API路由
50
+ this.app.use(apiRouter);
51
+
52
+ // 错误处理(必须放在最后)
53
+ this.app.use(errorHandler);
54
+ }
55
+
56
+ /**
57
+ * 初始化服务
58
+ */
59
+ async initializeServices() {
60
+ // 验证配置
61
+ const configErrors = validateConfig();
62
+ if (configErrors.length > 0) {
63
+ throw new Error(`配置错误:\n${configErrors.join('\n')}`);
64
+ }
65
+
66
+ // 初始化代理服务器
67
+ if (config.proxy.enableServer) {
68
+ try {
69
+ await proxyServer.start();
70
+ logger.success('代理服务器启动成功');
71
+ } catch (error) {
72
+ logger.error(`启动代理服务器失败: ${error.message}`);
73
+ // 代理服务器启动失败不应该阻止应用启动
74
+ }
75
+ }
76
+
77
+ // 初始化Notion客户端
78
+ await notionClient.initialize();
79
+
80
+ // 初始化代理池
81
+ if (config.proxy.useNativePool) {
82
+ logger.info('正在初始化本地代理池...');
83
+ proxyPool.logLevel = 'info';
84
+ proxyPool.showProgressBar = true;
85
+ proxyPool.setCountry(config.proxy.country);
86
+ await proxyPool.initialize();
87
+ logger.success(`代理池初始化完成,当前代理国家: ${proxyPool.proxyCountry}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 启动应用
93
+ */
94
+ async start() {
95
+ try {
96
+ // 初始化服务
97
+ await this.initializeServices();
98
+
99
+ // 配置中间件
100
+ this.configureMiddleware();
101
+
102
+ // 启动服务器
103
+ this.server = this.app.listen(config.server.port, () => {
104
+ logger.info(`服务已启动 - 端口: ${config.server.port}`);
105
+ logger.info(`访问地址: http://localhost:${config.server.port}`);
106
+ logger.info(`管理界面: http://localhost:${config.server.port}/admin`);
107
+
108
+ const status = notionClient.getStatus();
109
+ if (status.initialized) {
110
+ logger.success('系统初始化状态: ✅');
111
+ logger.success(`可用cookie数量: ${status.validCookies}`);
112
+ } else {
113
+ logger.warning('系统初始化状态: ❌');
114
+ logger.warning('警告: 系统未成功初始化,API调用将无法正常工作');
115
+ logger.warning('请检查NOTION_COOKIE配置是否有效');
116
+ }
117
+ });
118
+
119
+ } catch (error) {
120
+ logger.error(`应用启动失败: ${error.message}`, error);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 优雅关闭应用
127
+ */
128
+ async shutdown() {
129
+ logger.info('正在关闭应用...');
130
+
131
+ // 关闭所有活跃流
132
+ streamManager.closeAll();
133
+
134
+ // 关闭代理服务器
135
+ if (proxyServer) {
136
+ try {
137
+ proxyServer.stop();
138
+ logger.info('代理服务器已关闭');
139
+ } catch (error) {
140
+ logger.error(`关闭代理服务器时出错: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ // 关闭Express服务器
145
+ if (this.server) {
146
+ await new Promise((resolve) => {
147
+ this.server.close(resolve);
148
+ });
149
+ logger.info('HTTP服务器已关闭');
150
+ }
151
+
152
+ logger.success('应用已优雅关闭');
153
+ }
154
+ }
155
+
156
+ // 创建应用实例
157
+ const application = new Application();
158
+
159
+ // 注册进程信号处理
160
+ process.on('SIGINT', handleShutdown);
161
+ process.on('SIGTERM', handleShutdown);
162
+ process.on('SIGQUIT', handleShutdown);
163
+
164
+ async function handleShutdown(signal) {
165
+ logger.info(`收到${signal}信号,正在关闭应用...`);
166
+ await application.shutdown();
167
+ process.exit(0);
168
+ }
169
+
170
+ // ��理未捕获的异常
171
+ process.on('uncaughtException', (error) => {
172
+ logger.error('未捕获的异常:', error);
173
+ process.exit(1);
174
+ });
175
+
176
+ process.on('unhandledRejection', (reason, promise) => {
177
+ logger.error('未处理的Promise拒绝:', reason);
178
+ process.exit(1);
179
+ });
180
+
181
+ application.start();
182
+
183
+ export { application };
src/config/index.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dotenv from 'dotenv';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ // 加载环境变量
9
+ dotenv.config({ path: join(dirname(dirname(__dirname)), '.env') });
10
+
11
+ /**
12
+ * 应用配置中心
13
+ * 集中管理所有配置项,提供类型安全的配置访问
14
+ */
15
+ export const config = {
16
+ // 服务器配置
17
+ server: {
18
+ port: parseInt(process.env.PORT || '7860', 10),
19
+ authToken: process.env.PROXY_AUTH_TOKEN || 'default_token',
20
+ },
21
+
22
+ // Notion API配置
23
+ notion: {
24
+ apiUrl: 'https://www.notion.so/api/v3/runInferenceTranscript',
25
+ clientVersion: '23.13.0.3686',
26
+ origin: 'https://www.notion.so',
27
+ referer: 'https://www.notion.so/chat',
28
+ },
29
+
30
+ // 代理配置
31
+ proxy: {
32
+ useNativePool: process.env.USE_NATIVE_PROXY_POOL === 'true',
33
+ enableServer: process.env.ENABLE_PROXY_SERVER === 'true',
34
+ url: process.env.PROXY_URL || '',
35
+ country: process.env.PROXY_COUNTRY || 'us',
36
+ serverPort: 10655,
37
+ },
38
+
39
+ // Cookie配置
40
+ cookie: {
41
+ filePath: process.env.COOKIE_FILE,
42
+ envCookies: process.env.NOTION_COOKIE,
43
+ },
44
+
45
+ // 请求超时配置
46
+ timeout: {
47
+ request: 30000, // 30秒
48
+ },
49
+
50
+ // 模型映射
51
+ modelMapping: {
52
+ 'google-gemini-2.5-pro': 'vertex-gemini-2.5-pro',
53
+ 'google-gemini-2.5-flash': 'vertex-gemini-2.5-flash',
54
+ },
55
+
56
+ // 可用模型列表
57
+ availableModels: [
58
+ 'openai-gpt-4.1',
59
+ 'anthropic-opus-4',
60
+ 'anthropic-sonnet-4',
61
+ 'anthropic-sonnet-3.x-stable',
62
+ 'google-gemini-2.5-pro',
63
+ 'google-gemini-2.5-flash',
64
+ ],
65
+ };
66
+
67
+ // 验证必要的配置
68
+ export function validateConfig() {
69
+ const errors = [];
70
+
71
+ if (!config.cookie.filePath && !config.cookie.envCookies) {
72
+ errors.push('必须设置 COOKIE_FILE 或 NOTION_COOKIE 环境变量');
73
+ }
74
+
75
+ if (config.proxy.useNativePool && !['us', 'uk', 'jp', 'de', 'fr', 'ca'].includes(config.proxy.country)) {
76
+ errors.push('PROXY_COUNTRY 必须是以下之一: us, uk, jp, de, fr, ca');
77
+ }
78
+
79
+ return errors;
80
+ }
src/lightweight-client-express.js CHANGED
@@ -321,7 +321,7 @@ app.get('/cookies/status', authenticate, (req, res) => {
321
  const PORT = process.env.PORT || 7860;
322
 
323
  // 设置代理池日志级别为warn,减少详细日志输出
324
- proxyPool.logLevel = 'error';
325
 
326
  // 初始化并启动服务器
327
  initialize().then(() => {
 
321
  const PORT = process.env.PORT || 7860;
322
 
323
  // 设置代理池日志级别为warn,减少详细日志输出
324
+ proxyPool.logLevel = 'info';
325
 
326
  // 初始化并启动服务器
327
  initialize().then(() => {
src/lightweight-client.js CHANGED
@@ -1,882 +1,38 @@
1
- import fetch from 'node-fetch';
2
- import { JSDOM } from 'jsdom';
3
- import dotenv from 'dotenv';
4
- import { randomUUID } from 'crypto';
5
- import { fileURLToPath } from 'url';
6
- import { dirname, join } from 'path';
7
- import { PassThrough } from 'stream';
8
- import chalk from 'chalk';
9
- import {
10
- NotionTranscriptConfigValue,
11
- NotionTranscriptContextValue, NotionTranscriptItem, NotionDebugOverrides,
12
- NotionRequestBody, ChoiceDelta, Choice, ChatCompletionChunk, NotionTranscriptItemByuser
13
- } from './models.js';
14
- import { proxyPool } from './ProxyPool.js';
15
- import { proxyServer } from './ProxyServer.js';
16
- import { cookieManager } from './CookieManager.js';
17
 
18
- // 获取当前文件的目录路径
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
 
22
- // 加载环境变量
23
- dotenv.config({ path: join(dirname(__dirname), '.env') });
24
 
25
- // 日志配置
26
- const logger = {
27
- info: (message) => console.log(chalk.blue(`[info] ${message}`)),
28
- error: (message) => console.error(chalk.red(`[error] ${message}`)),
29
- warning: (message) => console.warn(chalk.yellow(`[warn] ${message}`)),
30
- success: (message) => console.log(chalk.green(`[success] ${message}`)),
31
- };
32
-
33
- // 配置
34
- const NOTION_API_URL = "https://www.notion.so/api/v3/runInferenceTranscript";
35
- // 这些变量将由cookieManager动态提供
36
- let currentCookieData = null;
37
- const USE_NATIVE_PROXY_POOL = process.env.USE_NATIVE_PROXY_POOL === 'true';
38
- const ENABLE_PROXY_SERVER = process.env.ENABLE_PROXY_SERVER === 'true';
39
- let proxy = null;
40
-
41
- // 代理配置
42
- const PROXY_URL = process.env.PROXY_URL || "";
43
-
44
- // 标记是否成功初始化
45
- let INITIALIZED_SUCCESSFULLY = false;
46
-
47
- // 注册进程退出事件,确保代理服务器在程序退出时关闭
48
- process.on('exit', () => {
49
  try {
50
- if (proxyServer) {
51
- proxyServer.stop();
52
- }
53
  } catch (error) {
54
- logger.error(`程序退出时关闭代理服务器出错: ${error.message}`);
55
- }
56
- });
57
-
58
- // 捕获意外退出信号
59
- ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
60
- process.on(signal, () => {
61
- logger.info(`收到${signal}信号,正在关闭代理服务器...`);
62
- try {
63
- if (proxyServer) {
64
- proxyServer.stop();
65
- }
66
- } catch (error) {
67
- logger.error(`关闭代理服务器出错: ${error.message}`);
68
- }
69
- process.exit(0);
70
- });
71
- });
72
-
73
- // 构建Notion请求
74
- function buildNotionRequest(requestData) {
75
- // 确保我们有当前的cookie数据
76
- if (!currentCookieData) {
77
- currentCookieData = cookieManager.getNext();
78
- if (!currentCookieData) {
79
- throw new Error('没有可用的cookie');
80
- }
81
- }
82
-
83
- // 当前时间
84
- const now = new Date();
85
- // 格式化为ISO字符串,确保包含毫秒和时区
86
- const isoString = now.toISOString();
87
-
88
- // 生成随机名称,类似于Python版本
89
- const randomWords = ["Project", "Workspace", "Team", "Studio", "Lab", "Hub", "Zone", "Space"];
90
- const userName = `User${Math.floor(Math.random() * 900) + 100}`; // 生成100-999之间的随机数
91
- const spaceName = `${randomWords[Math.floor(Math.random() * randomWords.length)]} ${Math.floor(Math.random() * 99) + 1}`;
92
-
93
- // 创建transcript数组
94
- const transcript = [];
95
-
96
- // 添加配置项
97
- if(requestData.model === 'anthropic-sonnet-3.x-stable'){
98
- transcript.push(new NotionTranscriptItem({
99
- type: "config",
100
- value: new NotionTranscriptConfigValue({
101
- })
102
- }));
103
- } else if(requestData.model === 'google-gemini-2.5-pro'){
104
- transcript.push(new NotionTranscriptItem({
105
- type: "config",
106
- value: new NotionTranscriptConfigValue({
107
- model: 'vertex-gemini-2.5-pro'
108
- })
109
- }));
110
- } else if (requestData.model === 'google-gemini-2.5-flash'){
111
- transcript.push(new NotionTranscriptItem({
112
- type: "config",
113
- value: new NotionTranscriptConfigValue({
114
- model: 'vertex-gemini-2.5-flash'
115
- })
116
- }));
117
- }
118
- else{
119
- transcript.push(new NotionTranscriptItem({
120
- type: "config",
121
- value: new NotionTranscriptConfigValue({
122
- model: requestData.model
123
- })
124
- }));
125
- }
126
-
127
-
128
- // 添加上下文项
129
- transcript.push(new NotionTranscriptItem({
130
- type: "context",
131
- value: new NotionTranscriptContextValue({
132
- userId: currentCookieData.userId,
133
- spaceId: currentCookieData.spaceId,
134
- surface: "home_module",
135
- timezone: "America/Los_Angeles",
136
- userName: userName,
137
- spaceName: spaceName,
138
- spaceViewId: randomUUID(),
139
- currentDatetime: isoString
140
- })
141
- }));
142
-
143
- // 添加agent-integration项
144
- transcript.push(new NotionTranscriptItem({
145
- type: "agent-integration"
146
- }));
147
-
148
- // 添加消息
149
- for (const message of requestData.messages) {
150
- // 处理消息内容,确保格式一致
151
- let content = message.content;
152
-
153
- // 处理内容为数组的情况
154
- if (Array.isArray(content)) {
155
- let textContent = "";
156
- for (const part of content) {
157
- if (part && typeof part === 'object' && part.type === 'text') {
158
- if (typeof part.text === 'string') {
159
- textContent += part.text;
160
- }
161
- }
162
- }
163
- content = textContent || ""; // 使用提取的文本或空字符串
164
- } else if (typeof content !== 'string') {
165
- content = ""; // 如果不是字符串或数组,则默认为空字符串
166
- }
167
-
168
- if (message.role === "system") {
169
- // 系统消息作为用户消息添加
170
- transcript.push(new NotionTranscriptItemByuser({
171
- type: "user",
172
- value: [[content]],
173
- userId: currentCookieData.userId,
174
- createdAt: message.createdAt || isoString
175
- }));
176
- } else if (message.role === "user") {
177
- // 用户消息
178
- transcript.push(new NotionTranscriptItemByuser({
179
- type: "user",
180
- value: [[content]],
181
- userId: currentCookieData.userId,
182
- createdAt: message.createdAt || isoString
183
- }));
184
- } else if (message.role === "assistant") {
185
- // 助手消息
186
- transcript.push(new NotionTranscriptItem({
187
- type: "markdown-chat",
188
- value: content,
189
- traceId: message.traceId || randomUUID(),
190
- createdAt: message.createdAt || isoString
191
- }));
192
- }
193
  }
194
-
195
- // 创建请求体
196
- return new NotionRequestBody({
197
- spaceId: currentCookieData.spaceId,
198
- transcript: transcript,
199
- createThread: true,
200
- traceId: randomUUID(),
201
- debugOverrides: new NotionDebugOverrides({
202
- cachedInferences: {},
203
- annotationInferences: {},
204
- emitInferences: false
205
- }),
206
- generateTitle: false,
207
- saveAllThreadOperations: false
208
- });
209
  }
210
 
211
- // 流式处理Notion响应
212
- async function streamNotionResponse(notionRequestBody) {
213
- // 确保我们有当前的cookie数据
214
- if (!currentCookieData) {
215
- currentCookieData = cookieManager.getNext();
216
- if (!currentCookieData) {
217
- throw new Error('没有可用的cookie');
218
- }
219
- }
220
-
221
- // 创建流
222
- const stream = new PassThrough();
223
-
224
- // 标记流状态
225
- let streamClosed = false;
226
-
227
- // 重写stream.end方法,确保安全关闭
228
- const originalEnd = stream.end;
229
- stream.end = function(...args) {
230
- if (streamClosed) return; // 避免重复关闭
231
- streamClosed = true;
232
- return originalEnd.apply(this, args);
233
- };
234
-
235
- // 添加初始数据,确保连接建立
236
- stream.write(':\n\n'); // 发送一个空注释行,保持连接活跃
237
-
238
- // 设置HTTP头模板
239
- const headers = {
240
- 'Content-Type': 'application/json',
241
- 'accept': 'application/x-ndjson',
242
- 'accept-language': 'en-US,en;q=0.9',
243
- 'notion-audit-log-platform': 'web',
244
- 'notion-client-version': '23.13.0.3686',
245
- 'origin': 'https://www.notion.so',
246
- 'referer': 'https://www.notion.so/chat',
247
- 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
248
- 'x-notion-active-user-header': currentCookieData.userId,
249
- 'x-notion-space-id': currentCookieData.spaceId
250
- };
251
-
252
- // 设置超时处理,确保流不会无限等待
253
- const timeoutId = setTimeout(() => {
254
- if (streamClosed) return;
255
-
256
- logger.warning(`请求超时,30秒内未收到响应`);
257
- try {
258
- // 发送结束消息
259
- const endChunk = new ChatCompletionChunk({
260
- choices: [
261
- new Choice({
262
- delta: new ChoiceDelta({ content: "请求超时,未收到Notion响应。" }),
263
- finish_reason: "timeout"
264
- })
265
- ]
266
- });
267
- stream.write(`data: ${JSON.stringify(endChunk)}\n\n`);
268
- stream.write('data: [DONE]\n\n');
269
- stream.end();
270
- } catch (error) {
271
- logger.error(`发送超时消息时出错: ${error}`);
272
- if (!streamClosed) stream.end();
273
- }
274
- }, 30000); // 30秒超时
275
-
276
- // 启动fetch处理
277
- fetchNotionResponse(
278
- stream,
279
- notionRequestBody,
280
- headers,
281
- NOTION_API_URL,
282
- currentCookieData.cookie,
283
- timeoutId
284
- ).catch((error) => {
285
- if (streamClosed) return;
286
-
287
- logger.error(`流处理出错: ${error}`);
288
- clearTimeout(timeoutId); // 清除超时计时器
289
-
290
- try {
291
- // 发送错误消息
292
- const errorChunk = new ChatCompletionChunk({
293
- choices: [
294
- new Choice({
295
- delta: new ChoiceDelta({ content: `处理请求时出错: ${error.message}` }),
296
- finish_reason: "error"
297
- })
298
- ]
299
- });
300
- stream.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
301
- stream.write('data: [DONE]\n\n');
302
- } catch (e) {
303
- logger.error(`发送错误消息时出错: ${e}`);
304
- } finally {
305
- if (!streamClosed) stream.end();
306
- }
307
- });
308
-
309
- return stream;
310
  }
311
 
312
- // 使用fetch调用Notion API并处理流式响应
313
- async function fetchNotionResponse(chunkQueue, notionRequestBody, headers, notionApiUrl, notionCookie, timeoutId) {
314
- let responseReceived = false;
315
- let dom = null;
316
-
317
- // 检查流是否已关闭的辅助函数
318
- const isStreamClosed = () => {
319
- return chunkQueue.destroyed || (typeof chunkQueue.closed === 'boolean' && chunkQueue.closed);
320
- };
321
-
322
- // 安全写入函数,确保只向开启的流写入数据
323
- const safeWrite = (data) => {
324
- if (!isStreamClosed()) {
325
- try {
326
- return chunkQueue.write(data);
327
- } catch (error) {
328
- logger.error(`流写入错误: ${error.message}`);
329
- return false;
330
- }
331
- }
332
- return false;
333
- };
334
-
335
- try {
336
- // 创建JSDOM实例模拟浏览器环境
337
- dom = new JSDOM("", {
338
- url: "https://www.notion.so",
339
- referrer: "https://www.notion.so/chat",
340
- contentType: "text/html",
341
- includeNodeLocations: true,
342
- storageQuota: 10000000,
343
- pretendToBeVisual: true,
344
- userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
345
- });
346
-
347
- // 设置全局对象
348
- const { window } = dom;
349
-
350
- // 使用更安全的方式设置全局对象
351
- try {
352
- if (!global.window) {
353
- global.window = window;
354
- }
355
-
356
- if (!global.document) {
357
- global.document = window.document;
358
- }
359
-
360
- // 安全地设置navigator
361
- if (!global.navigator) {
362
- try {
363
- Object.defineProperty(global, 'navigator', {
364
- value: window.navigator,
365
- writable: true,
366
- configurable: true
367
- });
368
- } catch (navError) {
369
- logger.warning(`无法设置navigator: ${navError.message},继续执行`);
370
- // 继续执行,不会中断流程
371
- }
372
- }
373
- } catch (globalError) {
374
- logger.warning(`设置全局对象时出错: ${globalError.message}`);
375
- }
376
-
377
- // 设置cookie
378
- document.cookie = notionCookie;
379
-
380
- // 创建fetch选项
381
- const fetchOptions = {
382
- method: 'POST',
383
- headers: {
384
- ...headers,
385
- 'user-agent': window.navigator.userAgent,
386
- 'Cookie': notionCookie
387
- },
388
- body: JSON.stringify(notionRequestBody),
389
- };
390
-
391
- // 添加代理配置(如果有)
392
- if (USE_NATIVE_PROXY_POOL && ENABLE_PROXY_SERVER && !PROXY_URL) {
393
- proxy = proxyPool.getProxy();
394
- if (proxy !== null)
395
- {
396
- logger.info(`使用代理: ${proxy.full}`);
397
- }
398
- else{
399
- logger.warning(`没有可用代理`);
400
- }
401
- } else if(USE_NATIVE_PROXY_POOL&&!PROXY_URL&&!ENABLE_PROXY_SERVER) {
402
- const { HttpsProxyAgent } = await import('https-proxy-agent');
403
- proxy = proxyPool.getProxy();
404
- fetchOptions.agent = new HttpsProxyAgent(proxy.full);
405
- logger.info(`使用代理: ${proxy.full}`);
406
- }else if(PROXY_URL){
407
- const { HttpsProxyAgent } = await import('https-proxy-agent');
408
- fetchOptions.agent = new HttpsProxyAgent(PROXY_URL);
409
- logger.info(`使用代理: ${PROXY_URL}`);
410
- }
411
- let response = null;
412
- // 发送请求
413
- if (ENABLE_PROXY_SERVER && USE_NATIVE_PROXY_POOL){
414
- response = await fetch('http://127.0.0.1:10655/proxy', {
415
- method: 'POST',
416
- body: JSON.stringify({
417
- method: 'POST',
418
- url: notionApiUrl,
419
- headers: fetchOptions.headers,
420
- body: fetchOptions.body,
421
- stream:true,
422
- proxy:proxy.full
423
- }),
424
- });
425
- }
426
- else if (ENABLE_PROXY_SERVER && !USE_NATIVE_PROXY_POOL && PROXY_URL){
427
- response = await fetch('http://127.0.0.1:10655/proxy', {
428
- method: 'POST',
429
- body: JSON.stringify({
430
- method: 'POST',
431
- url: notionApiUrl,
432
- headers: fetchOptions.headers,
433
- body: fetchOptions.body,
434
- proxy: PROXY_URL,
435
- stream:true,
436
- }),
437
- });
438
- }
439
- else if(ENABLE_PROXY_SERVER && !USE_NATIVE_PROXY_POOL){
440
- response = await fetch('http://127.0.0.1:10655/proxy', {
441
- method: 'POST',
442
- body: JSON.stringify({
443
- method: 'POST',
444
- url: notionApiUrl,
445
- headers: fetchOptions.headers,
446
- body: fetchOptions.body,
447
- stream:true,
448
- }),
449
- });
450
- }
451
- else{
452
- response = await fetch(notionApiUrl, fetchOptions);
453
- }
454
-
455
- // 检查是否收到401错误(未授权)
456
- if (response.status === 401) {
457
- logger.error(`收到401未授权错误,cookie可能已失效`);
458
- // 标记当前cookie为无效
459
- cookieManager.markAsInvalid(currentCookieData.userId);
460
- // 尝试获取下一个cookie
461
- currentCookieData = cookieManager.getNext();
462
-
463
- if (!currentCookieData) {
464
- throw new Error('所有cookie均已失效,无法继续请求');
465
- }
466
-
467
- // 使用新cookie重新构建请求体
468
- const newRequestBody = buildNotionRequest({
469
- model: notionRequestBody.transcript[0]?.value?.model || '',
470
- messages: [] // 这里应该根据实际情况重构消息
471
- });
472
-
473
- // 使用新cookie重试请求
474
- return fetchNotionResponse(
475
- chunkQueue,
476
- newRequestBody,
477
- {
478
- ...headers,
479
- 'x-notion-active-user-header': currentCookieData.userId,
480
- 'x-notion-space-id': currentCookieData.spaceId
481
- },
482
- notionApiUrl,
483
- currentCookieData.cookie,
484
- timeoutId
485
- );
486
- }
487
-
488
- if (!response.ok) {
489
- throw new Error(`HTTP error! status: ${response.status}`);
490
- }
491
-
492
- // 处理流式响应
493
- if (!response.body) {
494
- throw new Error("Response body is null");
495
- }
496
-
497
- // 创建流读取器
498
- const reader = response.body;
499
- let buffer = '';
500
-
501
- // 处理数据块
502
- reader.on('data', (chunk) => {
503
- // 检查流是否已关闭
504
- if (isStreamClosed()) {
505
- try {
506
- reader.destroy();
507
- } catch (error) {
508
- logger.error(`销毁reader时出错: ${error.message}`);
509
- }
510
- return;
511
- }
512
-
513
- try {
514
- // 标记已收到响应
515
- if (!responseReceived) {
516
- responseReceived = true;
517
- logger.info(`已连接Notion API`);
518
- clearTimeout(timeoutId); // 清除超时计时器
519
- }
520
-
521
- // 解码数据
522
- const text = chunk.toString('utf8');
523
- buffer += text;
524
-
525
- // 按行分割并处理完整的JSON对象
526
- const lines = buffer.split('\n');
527
- buffer = lines.pop() || ''; // 保留最后一行(可能不完整)
528
-
529
- for (const line of lines) {
530
- if (!line.trim()) continue;
531
-
532
- try {
533
- const jsonData = JSON.parse(line);
534
-
535
- // 提取内容
536
- if (jsonData?.type === "markdown-chat" && typeof jsonData?.value === "string") {
537
- const content = jsonData.value;
538
- if (!content) continue;
539
-
540
- // 创建OpenAI格式的块
541
- const chunk = new ChatCompletionChunk({
542
- choices: [
543
- new Choice({
544
- delta: new ChoiceDelta({ content }),
545
- finish_reason: null
546
- })
547
- ]
548
- });
549
-
550
- // 添加到队列
551
- const dataStr = `data: ${JSON.stringify(chunk)}\n\n`;
552
- if (!safeWrite(dataStr)) {
553
- // 如果写入失败,结束处理
554
- try {
555
- reader.destroy();
556
- } catch (error) {
557
- logger.error(`写入失败后销毁reader时出错: ${error.message}`);
558
- }
559
- return;
560
- }
561
- } else if (jsonData?.recordMap) {
562
- // 忽略recordMap响应
563
- } else {
564
- // 忽略其他类型响应
565
- }
566
- } catch (jsonError) {
567
- logger.error(`解析JSON出错: ${jsonError}`);
568
- }
569
- }
570
- } catch (error) {
571
- logger.error(`处理数据块出错: ${error}`);
572
- }
573
- });
574
-
575
- // 处理流结束
576
- reader.on('end', () => {
577
- try {
578
- logger.info(`响应完成`);
579
- if (cookieManager.getValidCount() > 1){
580
- // 尝试切换到下一个cookie
581
- currentCookieData = cookieManager.getNext();
582
- logger.info(`切换到下一个cookie: ${currentCookieData.userId}`);
583
- }
584
-
585
- // 如果没有收到任何响应,发送一个提示消息
586
- if (!responseReceived) {
587
- if (!ENABLE_PROXY_SERVER){
588
- logger.warning(`未从Notion收到内容响应,请尝试启用tls代理服务`)
589
- }else if (USE_NATIVE_PROXY_POOL){
590
- logger.warning(`未从Notion收到内容响应,请重roll,或者切换cookie`)
591
- }else{
592
- logger.warning(`未从Notion收到内容响应,请更换ip重试`);
593
- }
594
- if (USE_NATIVE_PROXY_POOL) {
595
- proxyPool.removeProxy(proxy.ip, proxy.port);
596
- }
597
-
598
- const noContentChunk = new ChatCompletionChunk({
599
- choices: [
600
- new Choice({
601
- delta: new ChoiceDelta({ content: "未从Notion收到内容响应,请更换ip重试。" }),
602
- finish_reason: "no_content"
603
- })
604
- ]
605
- });
606
- safeWrite(`data: ${JSON.stringify(noContentChunk)}\n\n`);
607
- }
608
-
609
- // 创建结束块
610
- const endChunk = new ChatCompletionChunk({
611
- choices: [
612
- new Choice({
613
- delta: new ChoiceDelta({ content: null }),
614
- finish_reason: "stop"
615
- })
616
- ]
617
- });
618
-
619
- // 添加到队列
620
- safeWrite(`data: ${JSON.stringify(endChunk)}\n\n`);
621
- safeWrite('data: [DONE]\n\n');
622
-
623
- // 清除超时计时器(如果尚未清除)
624
- if (timeoutId) clearTimeout(timeoutId);
625
-
626
- // 清理全局对象
627
- try {
628
- if (global.window) delete global.window;
629
- if (global.document) delete global.document;
630
-
631
- // 安全地删除navigator
632
- if (global.navigator) {
633
- try {
634
- delete global.navigator;
635
- } catch (navError) {
636
- // 如果无法删除,尝试将其设置为undefined
637
- try {
638
- Object.defineProperty(global, 'navigator', {
639
- value: undefined,
640
- writable: true,
641
- configurable: true
642
- });
643
- } catch (defineError) {
644
- logger.warning(`无法清理navigator: ${defineError.message}`);
645
- }
646
- }
647
- }
648
- } catch (cleanupError) {
649
- logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
650
- }
651
-
652
- // 结束流
653
- if (!isStreamClosed()) {
654
- chunkQueue.end();
655
- }
656
- } catch (error) {
657
- logger.error(`Error in stream end handler: ${error}`);
658
- if (timeoutId) clearTimeout(timeoutId);
659
-
660
- // 清理全局对象
661
- try {
662
- if (global.window) delete global.window;
663
- if (global.document) delete global.document;
664
-
665
- // 安全地删除navigator
666
- if (global.navigator) {
667
- try {
668
- delete global.navigator;
669
- } catch (navError) {
670
- // 如果无法删除,尝试将其设置为undefined
671
- try {
672
- Object.defineProperty(global, 'navigator', {
673
- value: undefined,
674
- writable: true,
675
- configurable: true
676
- });
677
- } catch (defineError) {
678
- logger.warning(`无法清理navigator: ${defineError.message}`);
679
- }
680
- }
681
- }
682
- } catch (cleanupError) {
683
- logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
684
- }
685
-
686
- if (!isStreamClosed()) {
687
- chunkQueue.end();
688
- }
689
- }
690
- });
691
-
692
- // 处理错误
693
- reader.on('error', (error) => {
694
- logger.error(`Stream error: ${error}`);
695
- if (timeoutId) clearTimeout(timeoutId);
696
-
697
- // 清理全局对象
698
- try {
699
- if (global.window) delete global.window;
700
- if (global.document) delete global.document;
701
-
702
- // 安全地删除navigator
703
- if (global.navigator) {
704
- try {
705
- delete global.navigator;
706
- } catch (navError) {
707
- // 如果无法删除,尝试将其设置为undefined
708
- try {
709
- Object.defineProperty(global, 'navigator', {
710
- value: undefined,
711
- writable: true,
712
- configurable: true
713
- });
714
- } catch (defineError) {
715
- logger.warning(`无法清理navigator: ${defineError.message}`);
716
- }
717
- }
718
- }
719
- } catch (cleanupError) {
720
- logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
721
- }
722
-
723
- try {
724
- const errorChunk = new ChatCompletionChunk({
725
- choices: [
726
- new Choice({
727
- delta: new ChoiceDelta({ content: `流读取错误: ${error.message}` }),
728
- finish_reason: "error"
729
- })
730
- ]
731
- });
732
- safeWrite(`data: ${JSON.stringify(errorChunk)}\n\n`);
733
- safeWrite('data: [DONE]\n\n');
734
- } catch (e) {
735
- logger.error(`Error sending error message: ${e}`);
736
- } finally {
737
- if (!isStreamClosed()) {
738
- chunkQueue.end();
739
- }
740
- }
741
- });
742
- } catch (error) {
743
- logger.error(`Notion API请求失败: ${error}`);
744
- // 清理全局对象
745
- try {
746
- if (global.window) delete global.window;
747
- if (global.document) delete global.document;
748
-
749
- // 安全地删除navigator
750
- if (global.navigator) {
751
- try {
752
- delete global.navigator;
753
- } catch (navError) {
754
- // 如果无法删除,尝试将其设置为undefined
755
- try {
756
- Object.defineProperty(global, 'navigator', {
757
- value: undefined,
758
- writable: true,
759
- configurable: true
760
- });
761
- } catch (defineError) {
762
- logger.warning(`无法清理navigator: ${defineError.message}`);
763
- }
764
- }
765
- }
766
- } catch (cleanupError) {
767
- logger.warning(`清理全局对象时出错: ${cleanupError.message}`);
768
- }
769
-
770
- if (timeoutId) clearTimeout(timeoutId);
771
-
772
- // 确保在错误情况下也触发流结束
773
- try {
774
- if (!responseReceived && !isStreamClosed()) {
775
- const errorChunk = new ChatCompletionChunk({
776
- choices: [
777
- new Choice({
778
- delta: new ChoiceDelta({ content: `Notion API请求失败: ${error.message}` }),
779
- finish_reason: "error"
780
- })
781
- ]
782
- });
783
- safeWrite(`data: ${JSON.stringify(errorChunk)}\n\n`);
784
- safeWrite('data: [DONE]\n\n');
785
- }
786
- } catch (e) {
787
- logger.error(`发送错误消息时出错: ${e}`);
788
- }
789
-
790
- if (!isStreamClosed()) {
791
- chunkQueue.end();
792
- }
793
-
794
- throw error; // 重新抛出错误以便上层捕获
795
- }
796
  }
797
 
798
- // 应用初始化
799
- async function initialize() {
800
- logger.info(`初始化Notion配置...`);
801
-
802
- // 启动代理服务器
803
- try {
804
- await proxyServer.start();
805
- } catch (error) {
806
- logger.error(`启动代理服务器失败: ${error.message}`);
807
- }
808
-
809
- // 初始化cookie管理器
810
- let initResult = false;
811
-
812
- // 检查是否配置了cookie文件
813
- const cookieFilePath = process.env.COOKIE_FILE;
814
- if (cookieFilePath) {
815
- logger.info(`检测到COOKIE_FILE配置: ${cookieFilePath}`);
816
- initResult = await cookieManager.loadFromFile(cookieFilePath);
817
-
818
- if (!initResult) {
819
- logger.error(`从文件加载cookie失败,尝试使用环境变量中的NOTION_COOKIE`);
820
- }
821
- }
822
-
823
- // 如果文件加载失败或未配置文件,尝试从环境变量加载
824
- if (!initResult) {
825
- const cookiesString = process.env.NOTION_COOKIE;
826
- if (!cookiesString) {
827
- logger.error(`错误: 未设置NOTION_COOKIE环境变量或COOKIE_FILE路径,应用无法正常工作`);
828
- logger.error(`请在.env文件中设置有效的NOTION_COOKIE值或COOKIE_FILE路径`);
829
- INITIALIZED_SUCCESSFULLY = false;
830
- return;
831
- }
832
-
833
- logger.info(`正在从环境变量初始化cookie管理器...`);
834
- initResult = await cookieManager.initialize(cookiesString);
835
-
836
- if (!initResult) {
837
- logger.error(`初始化cookie管理器失败,应用无法正常工作`);
838
- INITIALIZED_SUCCESSFULLY = false;
839
- return;
840
- }
841
- }
842
-
843
- // 获取第一个可用的cookie数据
844
- currentCookieData = cookieManager.getNext();
845
- if (!currentCookieData) {
846
- logger.error(`没有可用的cookie,应用无法正常工作`);
847
- INITIALIZED_SUCCESSFULLY = false;
848
- return;
849
- }
850
-
851
- logger.success(`成功初始化cookie管理器,共有 ${cookieManager.getValidCount()} 个有效cookie`);
852
- logger.info(`当前使用的cookie对应的用户ID: ${currentCookieData.userId}`);
853
- logger.info(`当前使用的cookie对应的空间ID: ${currentCookieData.spaceId}`);
854
-
855
- if (process.env.USE_NATIVE_PROXY_POOL === 'true') {
856
- logger.info(`正在初始化本地代理池...`);
857
- // 设置代理池的日志级别为warn,减少详细日志输出
858
- proxyPool.logLevel = 'error';
859
- // 启用进度条显示
860
- proxyPool.showProgressBar = true;
861
-
862
- if (['us', 'uk', 'jp', 'de', 'fr', 'ca'].includes(process.env.PROXY_COUNTRY)) {
863
- proxyPool.setCountry(process.env.PROXY_COUNTRY);
864
- } else {
865
- logger.warning(`未设置正确PROXY_COUNTRY,使用默认代理国家: us`);
866
- proxyPool.setCountry('us');
867
- }
868
- await proxyPool.initialize();
869
- await new Promise(resolve => setTimeout(resolve, 1000));
870
- logger.success(`代理池初始化完成,当前代理国家: ${proxyPool.proxyCountry}`);
871
- }
872
-
873
- INITIALIZED_SUCCESSFULLY = true;
874
- }
875
 
876
- // 导出函数
877
- export {
878
- initialize,
879
- streamNotionResponse,
880
- buildNotionRequest,
881
- INITIALIZED_SUCCESSFULLY
882
- };
 
1
+ /**
2
+ * 轻量级客户端 - 导出接口
3
+ *
4
+ * 这个文件提供了向后兼容的接口,
5
+ * 实际功能已经被重构到各个独立的模块中
6
+ */
 
 
 
 
 
 
 
 
 
 
7
 
8
+ import { notionClient } from './services/NotionClient.js';
9
+ import { createLogger } from './utils/logger.js';
 
10
 
11
+ const logger = createLogger('LightweightClient');
 
12
 
13
+ // 导出初始化函数
14
+ export async function initialize() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  try {
16
+ await notionClient.initialize();
17
+ return true;
 
18
  } catch (error) {
19
+ logger.error(`初始化失败: ${error.message}`, error);
20
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
+ // 导出流式响应函数
25
+ export async function streamNotionResponse(notionRequestBody) {
26
+ return notionClient.createStream(notionRequestBody);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
+ // 导出构建请求函数
30
+ export function buildNotionRequest(requestData) {
31
+ return notionClient.buildRequest(requestData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
+ // 导出初始化状态
35
+ export const INITIALIZED_SUCCESSFULLY = () => notionClient.getStatus().initialized;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ // 向后兼容性导出
38
+ export { notionClient };
 
 
 
 
 
src/middleware/auth.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createLogger } from '../utils/logger.js';
2
+ import { config } from '../config/index.js';
3
+
4
+ const logger = createLogger('AuthMiddleware');
5
+
6
+ // 存储有效的会话令牌(实际生产环境中应使用Redis或其他持久化存储)
7
+ const sessionTokens = new Map();
8
+
9
+ /**
10
+ * 认证中间件
11
+ * 验证请求的Bearer token
12
+ */
13
+ export function authenticate(req, res, next) {
14
+ const authHeader = req.headers.authorization;
15
+
16
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
17
+ logger.warning(`认证失败: 缺少Bearer token - IP: ${req.ip}`);
18
+ return res.status(401).json({
19
+ error: {
20
+ message: "Authentication required. Please provide a valid Bearer token.",
21
+ type: "authentication_error"
22
+ }
23
+ });
24
+ }
25
+
26
+ const token = authHeader.split(' ')[1];
27
+
28
+ // 首先检查是否是管理员会话令牌
29
+ if (sessionTokens.has(token)) {
30
+ const session = sessionTokens.get(token);
31
+ // 检查会话是否过期(24小时)
32
+ if (new Date().getTime() - session.timestamp < 24 * 60 * 60 * 1000) {
33
+ logger.debug(`认证成功(管理员会话) - IP: ${req.ip}`);
34
+ req.user = session.user;
35
+ return next();
36
+ } else {
37
+ // 会话过期,删除令牌
38
+ sessionTokens.delete(token);
39
+ }
40
+ }
41
+
42
+ // 检查是否是API令牌
43
+ if (token !== config.server.authToken) {
44
+ logger.warning(`认证失败: 无效的token - IP: ${req.ip}`);
45
+ return res.status(401).json({
46
+ error: {
47
+ message: "Invalid authentication credentials",
48
+ type: "authentication_error"
49
+ }
50
+ });
51
+ }
52
+
53
+ logger.debug(`认证成功(API令牌) - IP: ${req.ip}`);
54
+ next();
55
+ }
56
+
57
+ /**
58
+ * 添加会话令牌
59
+ */
60
+ export function addSessionToken(token, user) {
61
+ sessionTokens.set(token, {
62
+ user,
63
+ timestamp: new Date().getTime()
64
+ });
65
+ }
66
+
67
+ /**
68
+ * 清理过期的会话令牌
69
+ */
70
+ setInterval(() => {
71
+ const now = new Date().getTime();
72
+ for (const [token, session] of sessionTokens.entries()) {
73
+ if (now - session.timestamp > 24 * 60 * 60 * 1000) {
74
+ sessionTokens.delete(token);
75
+ }
76
+ }
77
+ }, 60 * 60 * 1000); // 每小时清理一次
78
+
79
+ /**
80
+ * 请求日志中间件
81
+ * 记录所有请求的详细信息
82
+ */
83
+ export function requestLogger(req, res, next) {
84
+ const start = Date.now();
85
+
86
+ // 保存原始的 end 方法
87
+ const originalEnd = res.end;
88
+
89
+ // 重写 end 方法以记录请求完成时间
90
+ res.end = function(...args) {
91
+ const duration = Date.now() - start;
92
+ logger.request(req.method, req.path, res.statusCode, duration);
93
+ return originalEnd.apply(this, args);
94
+ };
95
+
96
+ next();
97
+ }
98
+
99
+ /**
100
+ * 错误处理中间件
101
+ * 统一处理所有未捕获的错误
102
+ */
103
+ export function errorHandler(err, req, res, next) {
104
+ logger.error(`未处理的错误: ${err.message}`, err);
105
+
106
+ // 如果响应已经发送,则交给默认错误处理器
107
+ if (res.headersSent) {
108
+ return next(err);
109
+ }
110
+
111
+ // 发送错误响应
112
+ res.status(500).json({
113
+ error: {
114
+ message: process.env.NODE_ENV === 'production'
115
+ ? 'Internal server error'
116
+ : err.message,
117
+ type: 'server_error'
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * 请求体大小限制中间件配置
124
+ */
125
+ export const requestLimits = {
126
+ json: { limit: '50mb' },
127
+ urlencoded: { extended: true, limit: '50mb' }
128
+ };
src/models.js CHANGED
@@ -135,6 +135,7 @@ export class NotionRequestBody {
135
  traceId = randomUUID(),
136
  spaceId,
137
  transcript,
 
138
  createThread = false,
139
  debugOverrides = new NotionDebugOverrides({}),
140
  generateTitle = true,
@@ -143,6 +144,10 @@ export class NotionRequestBody {
143
  this.traceId = traceId;
144
  this.spaceId = spaceId;
145
  this.transcript = transcript;
 
 
 
 
146
  this.createThread = createThread;
147
  this.debugOverrides = debugOverrides;
148
  this.generateTitle = generateTitle;
@@ -210,4 +215,4 @@ export class ModelList {
210
  this.object = object;
211
  this.data = data;
212
  }
213
- }
 
135
  traceId = randomUUID(),
136
  spaceId,
137
  transcript,
138
+ threadId,
139
  createThread = false,
140
  debugOverrides = new NotionDebugOverrides({}),
141
  generateTitle = true,
 
144
  this.traceId = traceId;
145
  this.spaceId = spaceId;
146
  this.transcript = transcript;
147
+ // 只有在 threadId 存在时才添加该字段
148
+ if (threadId !== undefined) {
149
+ this.threadId = threadId;
150
+ }
151
  this.createThread = createThread;
152
  this.debugOverrides = debugOverrides;
153
  this.generateTitle = generateTitle;
 
215
  this.object = object;
216
  this.data = data;
217
  }
218
+ }
src/proxy/chrome_proxy_server_android_arm64 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:e4fce54fee0fe3acc4d255517134dcabb9dc3086e9dbe571bc9f8f216ba64039
3
- size 12863727
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9112e4db1662224d93d2d24a30757333772f7338f059b926b1c3a9259679ab8f
3
+ size 12876212
src/proxy/chrome_proxy_server_linux_amd64 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:2e33e31ff51fb711daff95ca41f84e5f56dbfc9c57b90df3761bc1d69e57bfa9
3
- size 12845606
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1821473bb2f1ef3b6e49b5ada7f53489055febf5510cef14593ae74a0e0ea32c
3
+ size 12856741
src/proxy/chrome_proxy_server_windows_amd64.exe CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:bb09f4ed7261e2f0db9be7b1c37df628549df6243aee4c6e3b25d3e9452028f6
3
- size 12961280
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f4e3b606b6b9bf118b8de79e80778eda24c2b9511e562cc9f5813d803bd69f7
3
+ size 12977664
src/routes/api.js ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { randomUUID } from 'crypto';
3
+ import { createLogger } from '../utils/logger.js';
4
+ import { config } from '../config/index.js';
5
+ import { notionClient } from '../services/NotionClient.js';
6
+ import { streamManager } from '../services/StreamManager.js';
7
+ import { cookieManager } from '../CookieManager.js';
8
+ import { authenticate, addSessionToken } from '../middleware/auth.js';
9
+ import crypto from 'crypto';
10
+
11
+ const logger = createLogger('APIRouter');
12
+ const router = Router();
13
+
14
+ /**
15
+ * POST /admin/login
16
+ * 管理员登录端点
17
+ */
18
+ router.post('/admin/login', async (req, res) => {
19
+ try {
20
+ const { username, password } = req.body;
21
+
22
+ if (!username || !password) {
23
+ return res.status(400).json({
24
+ success: false,
25
+ message: '请提供用户名和密码'
26
+ });
27
+ }
28
+
29
+ // 从环境变量获取管理员凭据
30
+ const adminUsername = process.env.ADMIN_USERNAME || 'admin';
31
+ const adminPassword = process.env.ADMIN_PASSWORD || process.env.AUTH_TOKEN || 'admin123';
32
+
33
+ // 验证用户名和密码
34
+ if (username !== adminUsername || password !== adminPassword) {
35
+ return res.status(401).json({
36
+ success: false,
37
+ message: '用户名或密码错误'
38
+ });
39
+ }
40
+
41
+ // 生成会话令牌
42
+ const sessionToken = crypto.randomBytes(32).toString('hex');
43
+
44
+ // 保存会话令牌
45
+ const user = { username: adminUsername };
46
+ addSessionToken(sessionToken, user);
47
+
48
+ // 返回登录成功信息
49
+ res.json({
50
+ success: true,
51
+ user: user,
52
+ token: sessionToken,
53
+ message: '登录成功'
54
+ });
55
+
56
+ logger.info(`管理员 ${username} 登录成功`);
57
+ } catch (error) {
58
+ logger.error(`登录失败: ${error.message}`, error);
59
+ res.status(500).json({
60
+ success: false,
61
+ message: '登录失败,请稍后重试'
62
+ });
63
+ }
64
+ });
65
+
66
+ /**
67
+ * GET /v1/models
68
+ * 返回可用的模型列表
69
+ */
70
+ router.get('/v1/models', authenticate, (req, res) => {
71
+ const modelList = {
72
+ data: config.availableModels.map(id => ({ id }))
73
+ };
74
+
75
+ res.json(modelList);
76
+ });
77
+
78
+ /**
79
+ * POST /v1/chat/completions
80
+ * 处理聊天完成请求
81
+ */
82
+ router.post('/v1/chat/completions', authenticate, async (req, res) => {
83
+ const clientId = req.headers['x-client-id'] || randomUUID();
84
+
85
+ try {
86
+ // 验证系统状态
87
+ const status = notionClient.getStatus();
88
+
89
+ if (!status.initialized) {
90
+ return res.status(500).json({
91
+ error: {
92
+ message: "系统未成功初始化。请检查您的NOTION_COOKIE是否有效。",
93
+ type: "server_error"
94
+ }
95
+ });
96
+ }
97
+
98
+ if (status.validCookies === 0) {
99
+ return res.status(500).json({
100
+ error: {
101
+ message: "没有可用的有效cookie。请检查您的NOTION_COOKIE配置。",
102
+ type: "server_error"
103
+ }
104
+ });
105
+ }
106
+
107
+ // 验证请求数据
108
+ const requestData = req.body;
109
+ const validation = validateChatRequest(requestData);
110
+
111
+ if (!validation.valid) {
112
+ return res.status(400).json({
113
+ error: {
114
+ message: validation.error,
115
+ type: "invalid_request_error"
116
+ }
117
+ });
118
+ }
119
+
120
+ // 构建Notion请求
121
+ const notionRequestBody = notionClient.buildRequest(requestData);
122
+
123
+ // 处理流式响应
124
+ if (requestData.stream) {
125
+ await handleStreamResponse(req, res, clientId, notionRequestBody);
126
+ } else {
127
+ await handleNonStreamResponse(req, res, clientId, notionRequestBody, requestData);
128
+ }
129
+
130
+ } catch (error) {
131
+ logger.error(`聊天完成端点错误: ${error.message}`, error);
132
+
133
+ if (!res.headersSent) {
134
+ res.status(500).json({
135
+ error: {
136
+ message: `Internal server error: ${error.message}`,
137
+ type: "server_error"
138
+ }
139
+ });
140
+ }
141
+ }
142
+ });
143
+
144
+ /**
145
+ * GET /health
146
+ * 健康检查端点
147
+ */
148
+ router.get('/health', (req, res) => {
149
+ const status = notionClient.getStatus();
150
+
151
+ res.json({
152
+ status: 'ok',
153
+ timestamp: new Date().toISOString(),
154
+ initialized: status.initialized,
155
+ valid_cookies: status.validCookies,
156
+ active_streams: streamManager.getActiveCount()
157
+ });
158
+ });
159
+
160
+ /**
161
+ * GET /cookies/status
162
+ * Cookie状态查询端点
163
+ */
164
+ router.get('/cookies/status', authenticate, (req, res) => {
165
+ res.json({
166
+ total_cookies: cookieManager.getValidCount(),
167
+ cookies: cookieManager.getStatus()
168
+ });
169
+ });
170
+
171
+ /**
172
+ * POST /cookies/add
173
+ * 添加新Cookie
174
+ */
175
+ router.post('/cookies/add', authenticate, async (req, res) => {
176
+ try {
177
+ const { cookies, threadId } = req.body;
178
+
179
+ if (!cookies) {
180
+ return res.status(400).json({
181
+ error: { message: '请提供cookie内容' }
182
+ });
183
+ }
184
+
185
+ // 支持批量添加
186
+ const cookieArray = cookies.includes('|') ? cookies.split('|') : [cookies];
187
+ let added = 0;
188
+ let failed = 0;
189
+ const errors = [];
190
+
191
+ for (const cookie of cookieArray) {
192
+ const trimmedCookie = cookie.trim();
193
+ if (!trimmedCookie) continue;
194
+
195
+ const result = await cookieManager.addCookie(trimmedCookie, threadId);
196
+ if (result.success) {
197
+ added++;
198
+ } else {
199
+ failed++;
200
+ errors.push(result.error);
201
+ }
202
+ }
203
+
204
+ // 如果有成功添加的cookie,保存到cookies.txt文件
205
+ if (added > 0 && config.cookie.filePath) {
206
+ try {
207
+ cookieManager.saveToFile(config.cookie.filePath, true);
208
+ logger.info(`已将更新后的cookie保存到文件: ${config.cookie.filePath}`);
209
+ } catch (saveError) {
210
+ logger.error(`保存cookie到文件失败: ${saveError.message}`);
211
+ }
212
+ }
213
+
214
+ res.json({
215
+ success: true,
216
+ added,
217
+ failed,
218
+ errors: errors.length > 0 ? errors : undefined
219
+ });
220
+ } catch (error) {
221
+ logger.error(`添加Cookie失败: ${error.message}`, error);
222
+ res.status(500).json({
223
+ error: { message: `添加Cookie失败: ${error.message}` }
224
+ });
225
+ }
226
+ });
227
+
228
+ /**
229
+ * PUT /cookies/thread
230
+ * 更新Cookie的Thread ID
231
+ */
232
+ router.put('/cookies/thread', authenticate, (req, res) => {
233
+ try {
234
+ const { userId, threadId } = req.body;
235
+
236
+ if (!userId) {
237
+ return res.status(400).json({
238
+ error: { message: '请提供用户ID' }
239
+ });
240
+ }
241
+
242
+ const success = cookieManager.setThreadId(userId, threadId);
243
+
244
+ if (success) {
245
+ res.json({ success: true });
246
+ } else {
247
+ res.status(404).json({
248
+ error: { message: '未找到指定用户的Cookie' }
249
+ });
250
+ }
251
+ } catch (error) {
252
+ logger.error(`更新Thread ID失败: ${error.message}`, error);
253
+ res.status(500).json({
254
+ error: { message: `更新Thread ID失败: ${error.message}` }
255
+ });
256
+ }
257
+ });
258
+
259
+ /**
260
+ * DELETE /cookies/:userId
261
+ * 删除指定用户的Cookie
262
+ */
263
+ router.delete('/cookies/:userId', authenticate, (req, res) => {
264
+ try {
265
+ const { userId } = req.params;
266
+
267
+ const success = cookieManager.deleteCookie(userId);
268
+
269
+ if (success) {
270
+ // 删除成功后,保存到cookies.txt文件
271
+ if (config.cookie.filePath) {
272
+ try {
273
+ cookieManager.saveToFile(config.cookie.filePath, true);
274
+ logger.info(`已将更新后的cookie保存到文件: ${config.cookie.filePath}`);
275
+ } catch (saveError) {
276
+ logger.error(`保存cookie到文件失败: ${saveError.message}`);
277
+ }
278
+ }
279
+
280
+ res.json({ success: true });
281
+ } else {
282
+ res.status(404).json({
283
+ error: { message: '未找到指定用户的Cookie' }
284
+ });
285
+ }
286
+ } catch (error) {
287
+ logger.error(`删除Cookie失败: ${error.message}`, error);
288
+ res.status(500).json({
289
+ error: { message: `删除Cookie失败: ${error.message}` }
290
+ });
291
+ }
292
+ });
293
+
294
+ /**
295
+ * POST /cookies/refresh
296
+ * 刷新所有Cookie状态
297
+ */
298
+ router.post('/cookies/refresh', authenticate, async (req, res) => {
299
+ try {
300
+ // 重新验证所有cookie
301
+ const cookies = cookieManager.getStatus();
302
+ let refreshed = 0;
303
+
304
+ for (const cookie of cookies) {
305
+ // 这里可以添加重新验证逻辑
306
+ // 暂时只返回成功
307
+ refreshed++;
308
+ }
309
+
310
+ res.json({
311
+ success: true,
312
+ refreshed,
313
+ total: cookies.length
314
+ });
315
+ } catch (error) {
316
+ logger.error(`刷新Cookie状态失败: ${error.message}`, error);
317
+ res.status(500).json({
318
+ error: { message: `刷新失败: ${error.message}` }
319
+ });
320
+ }
321
+ });
322
+
323
+ /**
324
+ * PUT /cookies/:userId/toggle
325
+ * 切换Cookie的启用状态
326
+ */
327
+ router.put('/cookies/:userId/toggle', authenticate, (req, res) => {
328
+ try {
329
+ const { userId } = req.params;
330
+ const { enabled } = req.body;
331
+
332
+ if (typeof enabled !== 'boolean') {
333
+ return res.status(400).json({
334
+ error: { message: '请提供有效的enabled状态(true/false)' }
335
+ });
336
+ }
337
+
338
+ const success = cookieManager.toggleCookie(userId, enabled);
339
+
340
+ if (success) {
341
+ res.json({ success: true, enabled });
342
+ } else {
343
+ res.status(404).json({
344
+ error: { message: '未找到指定用户的Cookie' }
345
+ });
346
+ }
347
+ } catch (error) {
348
+ logger.error(`切换Cookie状态失败: ${error.message}`, error);
349
+ res.status(500).json({
350
+ error: { message: `切换状态失败: ${error.message}` }
351
+ });
352
+ }
353
+ });
354
+
355
+ /**
356
+ * 验证聊天请求数据
357
+ */
358
+ function validateChatRequest(requestData) {
359
+ if (!requestData.messages) {
360
+ return { valid: false, error: "Invalid request: 'messages' field is required." };
361
+ }
362
+
363
+ if (!Array.isArray(requestData.messages)) {
364
+ return { valid: false, error: "Invalid request: 'messages' field must be an array." };
365
+ }
366
+
367
+ if (requestData.messages.length === 0) {
368
+ return { valid: false, error: "Invalid request: 'messages' field must be a non-empty array." };
369
+ }
370
+
371
+ // 验证每个消息的格式
372
+ for (const message of requestData.messages) {
373
+ if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) {
374
+ return { valid: false, error: "Invalid message format: each message must have a valid 'role' field." };
375
+ }
376
+
377
+ if (message.content === undefined || message.content === null) {
378
+ return { valid: false, error: "Invalid message format: each message must have a 'content' field." };
379
+ }
380
+ }
381
+
382
+ return { valid: true };
383
+ }
384
+
385
+ /**
386
+ * 处理流式响应
387
+ */
388
+ async function handleStreamResponse(req, res, clientId, notionRequestBody) {
389
+ res.setHeader('Content-Type', 'text/event-stream');
390
+ res.setHeader('Cache-Control', 'no-cache');
391
+ res.setHeader('Connection', 'keep-alive');
392
+
393
+ logger.info(`开始流式响应 - 客户端: ${clientId}`);
394
+
395
+ const stream = await notionClient.createStream(notionRequestBody);
396
+
397
+ // 注册流
398
+ streamManager.register(clientId, stream);
399
+
400
+ // 将流连接到响应
401
+ stream.pipe(res);
402
+
403
+ // 处理客户端断开连接
404
+ req.on('close', () => {
405
+ logger.info(`客户端 ${clientId} 断开连接`);
406
+ streamManager.close(clientId);
407
+ });
408
+
409
+ // 处理流错误
410
+ stream.on('error', (error) => {
411
+ logger.error(`流错误 - 客户端 ${clientId}: ${error.message}`);
412
+ if (!res.headersSent) {
413
+ res.status(500).json({
414
+ error: {
415
+ message: `Stream error: ${error.message}`,
416
+ type: "server_error"
417
+ }
418
+ });
419
+ }
420
+ });
421
+ }
422
+
423
+ /**
424
+ * 处理非流式响应
425
+ */
426
+ async function handleNonStreamResponse(req, res, clientId, notionRequestBody, requestData) {
427
+ logger.info(`开始非流式响应 - 客户端: ${clientId}`);
428
+
429
+ const chunks = [];
430
+ const stream = await notionClient.createStream(notionRequestBody);
431
+
432
+ // 注册流
433
+ streamManager.register(clientId, stream);
434
+
435
+ return new Promise((resolve, reject) => {
436
+ stream.on('data', (chunk) => {
437
+ const chunkStr = chunk.toString();
438
+ if (chunkStr.startsWith('data: ') && !chunkStr.includes('[DONE]')) {
439
+ try {
440
+ const dataJson = chunkStr.substring(6).trim();
441
+ if (dataJson) {
442
+ const chunkData = JSON.parse(dataJson);
443
+ if (chunkData.choices && chunkData.choices[0].delta && chunkData.choices[0].delta.content) {
444
+ chunks.push(chunkData.choices[0].delta.content);
445
+ }
446
+ }
447
+ } catch (error) {
448
+ logger.error(`解析非流式响应块时出错: ${error.message}`);
449
+ }
450
+ }
451
+ });
452
+
453
+ stream.on('end', () => {
454
+ const fullResponse = {
455
+ id: `chatcmpl-${randomUUID()}`,
456
+ object: "chat.completion",
457
+ created: Math.floor(Date.now() / 1000),
458
+ model: requestData.model,
459
+ choices: [
460
+ {
461
+ index: 0,
462
+ message: {
463
+ role: "assistant",
464
+ content: chunks.join('')
465
+ },
466
+ finish_reason: "stop"
467
+ }
468
+ ],
469
+ usage: {
470
+ prompt_tokens: null,
471
+ completion_tokens: null,
472
+ total_tokens: null
473
+ }
474
+ };
475
+
476
+ res.json(fullResponse);
477
+ resolve();
478
+ });
479
+
480
+ stream.on('error', (error) => {
481
+ logger.error(`非流式响应出错: ${error.message}`);
482
+ reject(error);
483
+ });
484
+
485
+ // 处理客户端断开连接
486
+ req.on('close', () => {
487
+ logger.info(`客户端 ${clientId} 断开连接(非流式)`);
488
+ streamManager.close(clientId);
489
+ });
490
+ });
491
+ }
492
+
493
+ export default router;
src/services/NotionClient.js ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fetch from 'node-fetch';
2
+ import { JSDOM } from 'jsdom';
3
+ import { randomUUID } from 'crypto';
4
+ import { createLogger } from '../utils/logger.js';
5
+ import { config } from '../config/index.js';
6
+ import {
7
+ NotionTranscriptConfigValue,
8
+ NotionTranscriptContextValue,
9
+ NotionTranscriptItem,
10
+ NotionDebugOverrides,
11
+ NotionRequestBody,
12
+ NotionTranscriptItemByuser,
13
+ ChoiceDelta,
14
+ Choice,
15
+ ChatCompletionChunk
16
+ } from '../models.js';
17
+ import { proxyPool } from '../ProxyPool.js';
18
+ import { cookieManager } from '../CookieManager.js';
19
+ import { streamManager } from './StreamManager.js';
20
+
21
+ const logger = createLogger('NotionClient');
22
+
23
+ /**
24
+ * Notion API 客户端
25
+ * 封装与Notion API的所有交互逻辑
26
+ */
27
+ export class NotionClient {
28
+ constructor() {
29
+ this.currentCookieData = null;
30
+ this.initialized = false;
31
+ }
32
+
33
+ /**
34
+ * 初始化客户端
35
+ */
36
+ async initialize() {
37
+ logger.info('初始化Notion客户端...');
38
+
39
+ // 初始化cookie管理器
40
+ let initResult = false;
41
+
42
+ if (config.cookie.filePath) {
43
+ logger.info(`检测到COOKIE_FILE配置: ${config.cookie.filePath}`);
44
+ initResult = await cookieManager.loadFromFile(config.cookie.filePath);
45
+
46
+ if (!initResult) {
47
+ logger.error('从文件加载cookie失败,尝试使用环境变量中的NOTION_COOKIE');
48
+ }
49
+ }
50
+
51
+ if (!initResult) {
52
+ if (!config.cookie.envCookies) {
53
+ throw new Error('未设置NOTION_COOKIE环境变量或COOKIE_FILE路径');
54
+ }
55
+
56
+ logger.info('正在从环境变量初始化cookie管理器...');
57
+ initResult = await cookieManager.initialize(config.cookie.envCookies);
58
+
59
+ if (!initResult) {
60
+ throw new Error('初始化cookie管理器失败');
61
+ }
62
+ }
63
+
64
+ // 获取第一个可用的cookie数据
65
+ this.currentCookieData = cookieManager.getNext();
66
+ if (!this.currentCookieData) {
67
+ throw new Error('没有可用的cookie');
68
+ }
69
+
70
+ logger.success(`成功初始化cookie管理器,共有 ${cookieManager.getValidCount()} 个有效cookie`);
71
+ logger.info(`当前使用的cookie对应的用户ID: ${this.currentCookieData.userId}`);
72
+ logger.info(`当前使用的cookie对应的空间ID: ${this.currentCookieData.spaceId}`);
73
+
74
+ this.initialized = true;
75
+ }
76
+
77
+ /**
78
+ * 构建Notion请求
79
+ * @param {Object} requestData - OpenAI格式的请求数据
80
+ * @returns {NotionRequestBody} Notion格式的请求体
81
+ */
82
+ buildRequest(requestData) {
83
+ // 确保有当前的cookie数据
84
+ if (!this.currentCookieData) {
85
+ this.currentCookieData = cookieManager.getNext();
86
+ if (!this.currentCookieData) {
87
+ throw new Error('没有可用的cookie');
88
+ }
89
+ }
90
+
91
+ const now = new Date();
92
+ const isoString = now.toISOString();
93
+
94
+ // 生成随机名称
95
+ const randomWords = ["Project", "Workspace", "Team", "Studio", "Lab", "Hub", "Zone", "Space"];
96
+ const userName = `User${Math.floor(Math.random() * 900) + 100}`;
97
+ const spaceName = `${randomWords[Math.floor(Math.random() * randomWords.length)]} ${Math.floor(Math.random() * 99) + 1}`;
98
+
99
+ const transcript = [];
100
+
101
+ // 添加配置项
102
+ const modelName = config.modelMapping[requestData.model] || requestData.model;
103
+
104
+ if (requestData.model === 'anthropic-sonnet-3.x-stable') {
105
+ transcript.push(new NotionTranscriptItem({
106
+ type: "config",
107
+ value: new NotionTranscriptConfigValue({})
108
+ }));
109
+ } else {
110
+ transcript.push(new NotionTranscriptItem({
111
+ type: "config",
112
+ value: new NotionTranscriptConfigValue({ model: modelName })
113
+ }));
114
+ }
115
+
116
+ // 添加上下文项
117
+ transcript.push(new NotionTranscriptItem({
118
+ type: "context",
119
+ value: new NotionTranscriptContextValue({
120
+ userId: this.currentCookieData.userId,
121
+ spaceId: this.currentCookieData.spaceId,
122
+ surface: "home_module",
123
+ timezone: "America/Los_Angeles",
124
+ userName: userName,
125
+ spaceName: spaceName,
126
+ spaceViewId: randomUUID(),
127
+ currentDatetime: isoString
128
+ })
129
+ }));
130
+
131
+ // 添加agent-integration项
132
+ transcript.push(new NotionTranscriptItem({
133
+ type: "agent-integration"
134
+ }));
135
+
136
+ // 添加消息
137
+ for (const message of requestData.messages) {
138
+ let content = this.normalizeMessageContent(message.content);
139
+
140
+ if (message.role === "system" || message.role === "user") {
141
+ transcript.push(new NotionTranscriptItemByuser({
142
+ type: "user",
143
+ value: [[content]],
144
+ userId: this.currentCookieData.userId,
145
+ createdAt: message.createdAt || isoString
146
+ }));
147
+ } else if (message.role === "assistant") {
148
+ transcript.push(new NotionTranscriptItem({
149
+ type: "markdown-chat",
150
+ value: content,
151
+ traceId: message.traceId || randomUUID(),
152
+ createdAt: message.createdAt || isoString
153
+ }));
154
+ }
155
+ }
156
+
157
+ // 构建基本请求体
158
+ const requestBodyData = {
159
+ spaceId: this.currentCookieData.spaceId,
160
+ transcript: transcript,
161
+ createThread: false,
162
+ traceId: randomUUID(),
163
+ debugOverrides: new NotionDebugOverrides({
164
+ cachedInferences: {},
165
+ annotationInferences: {},
166
+ emitInferences: false
167
+ }),
168
+ generateTitle: false,
169
+ saveAllThreadOperations: false
170
+ };
171
+
172
+ // 只有在有threadId时才添加相关字段
173
+ if (this.currentCookieData.threadId) {
174
+ requestBodyData.threadId = this.currentCookieData.threadId;
175
+ }
176
+ // 如果没有threadId,threadId字段不会被包含在请求体中
177
+
178
+ return new NotionRequestBody(requestBodyData);
179
+ }
180
+
181
+ /**
182
+ * 标准化消息内容
183
+ * @param {string|Array} content - 消息内容
184
+ * @returns {string} 标准化后的字符串内容
185
+ */
186
+ normalizeMessageContent(content) {
187
+ if (Array.isArray(content)) {
188
+ let textContent = "";
189
+ for (const part of content) {
190
+ if (part && typeof part === 'object' && part.type === 'text') {
191
+ if (typeof part.text === 'string') {
192
+ textContent += part.text;
193
+ }
194
+ }
195
+ }
196
+ return textContent || "";
197
+ } else if (typeof content !== 'string') {
198
+ return "";
199
+ }
200
+ return content;
201
+ }
202
+
203
+ /**
204
+ * 创建流式响应
205
+ * @param {NotionRequestBody} notionRequestBody - Notion请求体
206
+ * @returns {Promise<Stream>} 响应流
207
+ */
208
+ async createStream(notionRequestBody) {
209
+ // 确保有当前的cookie数据
210
+ if (!this.currentCookieData) {
211
+ this.currentCookieData = cookieManager.getNext();
212
+ if (!this.currentCookieData) {
213
+ throw new Error('没有可用的cookie');
214
+ }
215
+ }
216
+
217
+ // 创建流
218
+ const stream = streamManager.createStream();
219
+
220
+ // 添加初始数据,确保连接建立
221
+ stream.write(':\n\n');
222
+
223
+ // 设置HTTP头
224
+ const headers = this.buildHeaders();
225
+
226
+ // 设置超时处理
227
+ const timeoutId = setTimeout(() => {
228
+ if (stream.isClosed()) return;
229
+
230
+ logger.warning('请求超时,30秒内未收到响应');
231
+ this.sendErrorToStream(stream, '请求超时,未收到Notion响应。', 'timeout');
232
+ }, config.timeout.request);
233
+
234
+ // 启动fetch处理
235
+ this.fetchAndStream(
236
+ stream,
237
+ notionRequestBody,
238
+ headers,
239
+ this.currentCookieData.cookie,
240
+ timeoutId
241
+ ).catch((error) => {
242
+ if (stream.isClosed()) return;
243
+
244
+ logger.error(`流处理出错: ${error.message}`, error);
245
+ clearTimeout(timeoutId);
246
+ this.sendErrorToStream(stream, `处理请求时出错: ${error.message}`, 'error');
247
+ });
248
+
249
+ return stream;
250
+ }
251
+
252
+ /**
253
+ * 构建请求头
254
+ * @returns {Object} HTTP请求头
255
+ */
256
+ buildHeaders() {
257
+ return {
258
+ 'Content-Type': 'application/json',
259
+ 'accept': 'application/x-ndjson',
260
+ 'accept-language': 'en-US,en;q=0.9',
261
+ 'notion-audit-log-platform': 'web',
262
+ 'notion-client-version': config.notion.clientVersion,
263
+ 'origin': config.notion.origin,
264
+ 'referer': config.notion.referer,
265
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
266
+ 'x-notion-active-user-header': this.currentCookieData.userId,
267
+ 'x-notion-space-id': this.currentCookieData.spaceId
268
+ };
269
+ }
270
+
271
+ /**
272
+ * 发送错误消息到流
273
+ * @param {Stream} stream - 目标流
274
+ * @param {string} message - 错误消息
275
+ * @param {string} finishReason - 结束原因
276
+ */
277
+ sendErrorToStream(stream, message, finishReason) {
278
+ try {
279
+ const errorChunk = new ChatCompletionChunk({
280
+ choices: [
281
+ new Choice({
282
+ delta: new ChoiceDelta({ content: message }),
283
+ finish_reason: finishReason
284
+ })
285
+ ]
286
+ });
287
+ streamManager.safeWrite(stream, `data: ${JSON.stringify(errorChunk)}\n\n`);
288
+ streamManager.safeWrite(stream, 'data: [DONE]\n\n');
289
+ } catch (e) {
290
+ logger.error(`发送错误消息时出错: ${e.message}`);
291
+ } finally {
292
+ if (!stream.isClosed()) stream.end();
293
+ }
294
+ }
295
+
296
+ /**
297
+ * 执行fetch请求并处理流式响应
298
+ */
299
+ async fetchAndStream(stream, notionRequestBody, headers, notionCookie, timeoutId) {
300
+ let responseReceived = false;
301
+ let dom = null;
302
+
303
+ try {
304
+ // 创建JSDOM实例
305
+ dom = this.createDOMEnvironment();
306
+
307
+ // 设置cookie
308
+ dom.window.document.cookie = notionCookie;
309
+
310
+ // 创建fetch选项
311
+ const fetchOptions = await this.buildFetchOptions(headers, notionCookie, notionRequestBody);
312
+
313
+ // 发送请求
314
+ const response = await this.executeRequest(fetchOptions);
315
+
316
+ // 处理401错误
317
+ if (response.status === 401) {
318
+ await this.handle401Error(stream, notionRequestBody, headers, timeoutId);
319
+ return;
320
+ }
321
+
322
+ if (!response.ok) {
323
+ throw new Error(`HTTP error! status: ${response.status}`);
324
+ }
325
+
326
+ // 处理流式响应
327
+ await this.processStreamResponse(response, stream, responseReceived, timeoutId);
328
+
329
+ } catch (error) {
330
+ logger.error(`Notion API请求失败: ${error.message}`, error);
331
+ if (timeoutId) clearTimeout(timeoutId);
332
+
333
+ if (!responseReceived && !stream.isClosed()) {
334
+ this.sendErrorToStream(stream, `Notion API请求失败: ${error.message}`, 'error');
335
+ }
336
+
337
+ throw error;
338
+ } finally {
339
+ // 清理DOM环境
340
+ this.cleanupDOMEnvironment();
341
+ if (dom) dom.window.close();
342
+ }
343
+ }
344
+
345
+ /**
346
+ * 创建DOM环境
347
+ */
348
+ createDOMEnvironment() {
349
+ const dom = new JSDOM("", {
350
+ url: "https://www.notion.so",
351
+ referrer: "https://www.notion.so/chat",
352
+ contentType: "text/html",
353
+ includeNodeLocations: true,
354
+ storageQuota: 10000000,
355
+ pretendToBeVisual: true,
356
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
357
+ });
358
+
359
+ const { window } = dom;
360
+
361
+ // 安全设置全局对象
362
+ try {
363
+ if (!global.window) global.window = window;
364
+ if (!global.document) global.document = window.document;
365
+ if (!global.navigator) {
366
+ Object.defineProperty(global, 'navigator', {
367
+ value: window.navigator,
368
+ writable: true,
369
+ configurable: true
370
+ });
371
+ }
372
+ } catch (error) {
373
+ logger.warning(`设置全局对象时出错: ${error.message}`);
374
+ }
375
+
376
+ return dom;
377
+ }
378
+
379
+ /**
380
+ * 清理DOM环境
381
+ */
382
+ cleanupDOMEnvironment() {
383
+ try {
384
+ if (global.window) delete global.window;
385
+ if (global.document) delete global.document;
386
+ if (global.navigator) {
387
+ try {
388
+ delete global.navigator;
389
+ } catch (error) {
390
+ Object.defineProperty(global, 'navigator', {
391
+ value: undefined,
392
+ writable: true,
393
+ configurable: true
394
+ });
395
+ }
396
+ }
397
+ } catch (error) {
398
+ logger.warning(`清理全局对象时出错: ${error.message}`);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * 构建fetch选项
404
+ */
405
+ async buildFetchOptions(headers, notionCookie, notionRequestBody) {
406
+ const fetchOptions = {
407
+ method: 'POST',
408
+ headers: {
409
+ ...headers,
410
+ 'user-agent': global.window.navigator.userAgent,
411
+ 'Cookie': notionCookie
412
+ },
413
+ body: JSON.stringify(notionRequestBody),
414
+ };
415
+
416
+ // 添加代理配置
417
+ if (config.proxy.useNativePool && !config.proxy.url) {
418
+ const proxy = proxyPool.getProxy();
419
+ if (proxy) {
420
+ logger.info(`使用代理: ${proxy.full}`);
421
+ if (!config.proxy.enableServer) {
422
+ const { HttpsProxyAgent } = await import('https-proxy-agent');
423
+ fetchOptions.agent = new HttpsProxyAgent(proxy.full);
424
+ }
425
+ fetchOptions.proxy = proxy;
426
+ }
427
+ } else if (config.proxy.url) {
428
+ logger.info(`使用代理: ${config.proxy.url}`);
429
+ if (!config.proxy.enableServer) {
430
+ const { HttpsProxyAgent } = await import('https-proxy-agent');
431
+ fetchOptions.agent = new HttpsProxyAgent(config.proxy.url);
432
+ }
433
+ fetchOptions.proxyUrl = config.proxy.url;
434
+ }
435
+
436
+ return fetchOptions;
437
+ }
438
+
439
+ /**
440
+ * 执行请求
441
+ */
442
+ async executeRequest(fetchOptions) {
443
+ if (config.proxy.enableServer) {
444
+ const proxyRequest = {
445
+ method: 'POST',
446
+ url: config.notion.apiUrl,
447
+ headers: fetchOptions.headers,
448
+ body: fetchOptions.body,
449
+ stream: true
450
+ };
451
+
452
+ if (fetchOptions.proxy) {
453
+ proxyRequest.proxy = fetchOptions.proxy.full;
454
+ } else if (fetchOptions.proxyUrl) {
455
+ proxyRequest.proxy = fetchOptions.proxyUrl;
456
+ }
457
+
458
+ return await fetch(`http://127.0.0.1:${config.proxy.serverPort}/proxy`, {
459
+ method: 'POST',
460
+ body: JSON.stringify(proxyRequest)
461
+ });
462
+ }
463
+
464
+ return await fetch(config.notion.apiUrl, fetchOptions);
465
+ }
466
+
467
+ /**
468
+ * 处理401错误
469
+ */
470
+ async handle401Error(stream, notionRequestBody, headers, timeoutId) {
471
+ logger.error('收到401未授权错误,cookie可能已失效');
472
+ cookieManager.markAsInvalid(this.currentCookieData.userId);
473
+
474
+ this.currentCookieData = cookieManager.getNext();
475
+ if (!this.currentCookieData) {
476
+ throw new Error('所有cookie均已失效,无法继续请求');
477
+ }
478
+
479
+ // 重新构建请求并���试
480
+ const newHeaders = {
481
+ ...headers,
482
+ 'x-notion-active-user-header': this.currentCookieData.userId,
483
+ 'x-notion-space-id': this.currentCookieData.spaceId
484
+ };
485
+
486
+ return this.fetchAndStream(
487
+ stream,
488
+ notionRequestBody,
489
+ newHeaders,
490
+ this.currentCookieData.cookie,
491
+ timeoutId
492
+ );
493
+ }
494
+
495
+ /**
496
+ * 处理流式响应
497
+ */
498
+ async processStreamResponse(response, stream, responseReceived, timeoutId) {
499
+ if (!response.body) {
500
+ throw new Error("Response body is null");
501
+ }
502
+
503
+ const reader = response.body;
504
+ let buffer = '';
505
+
506
+ reader.on('data', (chunk) => {
507
+ if (stream.isClosed()) {
508
+ try {
509
+ reader.destroy();
510
+ } catch (error) {
511
+ logger.error(`销毁reader时出错: ${error.message}`);
512
+ }
513
+ return;
514
+ }
515
+
516
+ try {
517
+ if (!responseReceived) {
518
+ responseReceived = true;
519
+ logger.info('已连接Notion API');
520
+ clearTimeout(timeoutId);
521
+ }
522
+
523
+ const text = chunk.toString('utf8');
524
+ buffer += text;
525
+
526
+ const lines = buffer.split('\n');
527
+ buffer = lines.pop() || '';
528
+
529
+ for (const line of lines) {
530
+ if (!line.trim()) continue;
531
+
532
+ try {
533
+ const jsonData = JSON.parse(line);
534
+
535
+ if (jsonData?.type === "markdown-chat" && typeof jsonData?.value === "string") {
536
+ const content = jsonData.value;
537
+ if (!content) continue;
538
+
539
+ const chunk = new ChatCompletionChunk({
540
+ choices: [
541
+ new Choice({
542
+ delta: new ChoiceDelta({ content }),
543
+ finish_reason: null
544
+ })
545
+ ]
546
+ });
547
+
548
+ const dataStr = `data: ${JSON.stringify(chunk)}\n\n`;
549
+ if (!streamManager.safeWrite(stream, dataStr)) {
550
+ try {
551
+ reader.destroy();
552
+ } catch (error) {
553
+ logger.error(`写入失败后销毁reader时出错: ${error.message}`);
554
+ }
555
+ return;
556
+ }
557
+ }
558
+ } catch (jsonError) {
559
+ logger.error(`解析JSON出错: ${jsonError.message}`);
560
+ }
561
+ }
562
+ } catch (error) {
563
+ logger.error(`处理数据块出错: ${error.message}`);
564
+ }
565
+ });
566
+
567
+ reader.on('end', () => {
568
+ try {
569
+ logger.info('响应完成');
570
+
571
+ if (cookieManager.getValidCount() > 1) {
572
+ this.currentCookieData = cookieManager.getNext();
573
+ logger.info(`切换到下一个cookie: ${this.currentCookieData.userId}`);
574
+ }
575
+
576
+ if (!responseReceived) {
577
+ this.handleNoContentResponse(stream);
578
+ }
579
+
580
+ this.sendEndChunk(stream);
581
+
582
+ if (timeoutId) clearTimeout(timeoutId);
583
+ if (!stream.isClosed()) stream.end();
584
+
585
+ } catch (error) {
586
+ logger.error(`处理流结束时出错: ${error.message}`);
587
+ if (timeoutId) clearTimeout(timeoutId);
588
+ if (!stream.isClosed()) stream.end();
589
+ }
590
+ });
591
+
592
+ reader.on('error', (error) => {
593
+ logger.error(`流错误: ${error.message}`);
594
+ if (timeoutId) clearTimeout(timeoutId);
595
+ this.sendErrorToStream(stream, `流读取错误: ${error.message}`, 'error');
596
+ });
597
+ }
598
+
599
+ /**
600
+ * 处理无内容响应
601
+ */
602
+ handleNoContentResponse(stream) {
603
+ if (!config.proxy.enableServer) {
604
+ logger.warning('未从Notion收到内容响应,请尝试启用tls代理服务');
605
+ } else if (config.proxy.useNativePool) {
606
+ logger.warning('未从Notion收到内容响应,请重roll,或者切换cookie');
607
+ } else {
608
+ logger.warning('未从Notion收到内容响应,请更换ip重试');
609
+ }
610
+
611
+ const noContentChunk = new ChatCompletionChunk({
612
+ choices: [
613
+ new Choice({
614
+ delta: new ChoiceDelta({ content: "未从Notion收到内容响应,请更换ip重试。" }),
615
+ finish_reason: "no_content"
616
+ })
617
+ ]
618
+ });
619
+ streamManager.safeWrite(stream, `data: ${JSON.stringify(noContentChunk)}\n\n`);
620
+ }
621
+
622
+ /**
623
+ * 发送结束块
624
+ */
625
+ sendEndChunk(stream) {
626
+ const endChunk = new ChatCompletionChunk({
627
+ choices: [
628
+ new Choice({
629
+ delta: new ChoiceDelta({ content: null }),
630
+ finish_reason: "stop"
631
+ })
632
+ ]
633
+ });
634
+
635
+ streamManager.safeWrite(stream, `data: ${JSON.stringify(endChunk)}\n\n`);
636
+ streamManager.safeWrite(stream, 'data: [DONE]\n\n');
637
+ }
638
+
639
+ /**
640
+ * 获取状态信息
641
+ */
642
+ getStatus() {
643
+ return {
644
+ initialized: this.initialized,
645
+ validCookies: cookieManager.getValidCount(),
646
+ currentUserId: this.currentCookieData?.userId || null,
647
+ currentSpaceId: this.currentCookieData?.spaceId || null
648
+ };
649
+ }
650
+ }
651
+
652
+ // 创建全局NotionClient实例
653
+ export const notionClient = new NotionClient();
src/services/StreamManager.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PassThrough } from 'stream';
2
+ import { createLogger } from '../utils/logger.js';
3
+
4
+ const logger = createLogger('StreamManager');
5
+
6
+ /**
7
+ * 流管理器 - 负责管理和跟踪活跃的流
8
+ */
9
+ export class StreamManager {
10
+ constructor() {
11
+ this.activeStreams = new Map();
12
+ }
13
+
14
+ /**
15
+ * 创建新的流
16
+ * @returns {PassThrough} 新创建的流
17
+ */
18
+ createStream() {
19
+ const stream = new PassThrough();
20
+ let streamClosed = false;
21
+
22
+ // 重写stream.end方法,确保安全关闭
23
+ const originalEnd = stream.end;
24
+ stream.end = function(...args) {
25
+ if (streamClosed) return;
26
+ streamClosed = true;
27
+ return originalEnd.apply(this, args);
28
+ };
29
+
30
+ // 添加状态检查方法
31
+ stream.isClosed = () => streamClosed;
32
+
33
+ return stream;
34
+ }
35
+
36
+ /**
37
+ * 注册并管理流
38
+ * @param {string} clientId - 客户端ID
39
+ * @param {Stream} stream - 要管理的流
40
+ * @returns {Stream} 返回被管理的流
41
+ */
42
+ register(clientId, stream) {
43
+ // 如果该客户端已有活跃流,先关闭它
44
+ if (this.activeStreams.has(clientId)) {
45
+ this.close(clientId);
46
+ }
47
+
48
+ // 注册新流
49
+ this.activeStreams.set(clientId, stream);
50
+ logger.debug(`注册客户端 ${clientId} 的新流`);
51
+
52
+ // 设置流事件监听器
53
+ stream.on('end', () => {
54
+ if (this.activeStreams.get(clientId) === stream) {
55
+ this.activeStreams.delete(clientId);
56
+ logger.debug(`客户端 ${clientId} 的流已结束并移除`);
57
+ }
58
+ });
59
+
60
+ stream.on('error', (error) => {
61
+ logger.error(`客户端 ${clientId} 的流错误: ${error.message}`);
62
+ if (this.activeStreams.get(clientId) === stream) {
63
+ this.activeStreams.delete(clientId);
64
+ }
65
+ });
66
+
67
+ return stream;
68
+ }
69
+
70
+ /**
71
+ * 关闭指定客户端的流
72
+ * @param {string} clientId - 客户端ID
73
+ */
74
+ close(clientId) {
75
+ const stream = this.activeStreams.get(clientId);
76
+ if (stream) {
77
+ try {
78
+ logger.debug(`关闭客户端 ${clientId} 的流`);
79
+ stream.end();
80
+ this.activeStreams.delete(clientId);
81
+ } catch (error) {
82
+ logger.error(`关闭流时出错: ${error.message}`);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 获取指定客户端的流
89
+ * @param {string} clientId - 客户端ID
90
+ * @returns {Stream|null} 流对象或null
91
+ */
92
+ get(clientId) {
93
+ return this.activeStreams.get(clientId) || null;
94
+ }
95
+
96
+ /**
97
+ * 检查客户端是否有活跃流
98
+ * @param {string} clientId - 客户端ID
99
+ * @returns {boolean}
100
+ */
101
+ has(clientId) {
102
+ return this.activeStreams.has(clientId);
103
+ }
104
+
105
+ /**
106
+ * 获取活跃流的数量
107
+ * @returns {number}
108
+ */
109
+ getActiveCount() {
110
+ return this.activeStreams.size;
111
+ }
112
+
113
+ /**
114
+ * 关闭所有流
115
+ */
116
+ closeAll() {
117
+ logger.info(`关闭所有活跃流 (共 ${this.activeStreams.size} 个)`);
118
+ for (const [clientId, stream] of this.activeStreams) {
119
+ this.close(clientId);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 安全写入数据到流
125
+ * @param {Stream} stream - 目标流
126
+ * @param {string|Buffer} data - 要写入的数据
127
+ * @returns {boolean} 写入是否成功
128
+ */
129
+ safeWrite(stream, data) {
130
+ if (!stream || stream.destroyed || (stream.isClosed && stream.isClosed())) {
131
+ return false;
132
+ }
133
+
134
+ try {
135
+ return stream.write(data);
136
+ } catch (error) {
137
+ logger.error(`流写入错误: ${error.message}`);
138
+ return false;
139
+ }
140
+ }
141
+ }
142
+
143
+ // 创建全局流管理器实例
144
+ export const streamManager = new StreamManager();
src/utils/logger.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * 日志级别枚举
5
+ */
6
+ export const LogLevel = {
7
+ DEBUG: 0,
8
+ INFO: 1,
9
+ WARNING: 2,
10
+ ERROR: 3,
11
+ SUCCESS: 4,
12
+ };
13
+
14
+ /**
15
+ * 统一的日志管理器
16
+ */
17
+ class Logger {
18
+ constructor(name = 'app') {
19
+ this.name = name;
20
+ this.level = LogLevel.INFO;
21
+ }
22
+
23
+ setLevel(level) {
24
+ this.level = level;
25
+ }
26
+
27
+ debug(message) {
28
+ if (this.level <= LogLevel.DEBUG) {
29
+ console.log(chalk.gray(`[debug][${this.name}] ${message}`));
30
+ }
31
+ }
32
+
33
+ info(message) {
34
+ if (this.level <= LogLevel.INFO) {
35
+ console.log(chalk.blue(`[info][${this.name}] ${message}`));
36
+ }
37
+ }
38
+
39
+ warning(message) {
40
+ if (this.level <= LogLevel.WARNING) {
41
+ console.warn(chalk.yellow(`[warn][${this.name}] ${message}`));
42
+ }
43
+ }
44
+
45
+ error(message, error = null) {
46
+ if (this.level <= LogLevel.ERROR) {
47
+ console.error(chalk.red(`[error][${this.name}] ${message}`));
48
+ if (error && error.stack) {
49
+ console.error(chalk.red(error.stack));
50
+ }
51
+ }
52
+ }
53
+
54
+ success(message) {
55
+ if (this.level <= LogLevel.SUCCESS) {
56
+ console.log(chalk.green(`[success][${this.name}] ${message}`));
57
+ }
58
+ }
59
+
60
+ request(method, path, status, time) {
61
+ if (this.level <= LogLevel.INFO) {
62
+ const statusColor = status >= 500 ? chalk.red :
63
+ status >= 400 ? chalk.yellow :
64
+ status >= 300 ? chalk.cyan :
65
+ status >= 200 ? chalk.green : chalk.white;
66
+ console.log(`${chalk.magenta(`[${method}]`)} - ${path} ${statusColor(status)} ${chalk.gray(`${time}ms`)}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ // 创建默认日志实例
72
+ export const defaultLogger = new Logger();
73
+
74
+ // 创建具名日志实例的工厂函数
75
+ export function createLogger(name) {
76
+ return new Logger(name);
77
+ }
78
+
79
+ // 导出默认方法
80
+ export default {
81
+ debug: (message) => defaultLogger.debug(message),
82
+ info: (message) => defaultLogger.info(message),
83
+ warning: (message) => defaultLogger.warning(message),
84
+ error: (message, error) => defaultLogger.error(message, error),
85
+ success: (message) => defaultLogger.success(message),
86
+ request: (method, path, status, time) => defaultLogger.request(method, path, status, time),
87
+ setLevel: (level) => defaultLogger.setLevel(level),
88
+ };
src/utils/storage.js ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+ import { createLogger } from './logger.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ const logger = createLogger('Storage');
11
+
12
+ /**
13
+ * 持久化存储管理器
14
+ * 用于保存和加载Cookie数据(包括Thread ID)
15
+ */
16
+ class StorageManager {
17
+ constructor() {
18
+ // 数据文件路径
19
+ this.dataFilePath = path.join(dirname(dirname(__dirname)), 'data', 'cookies-data.json');
20
+ this.backupFilePath = path.join(dirname(dirname(__dirname)), 'data', 'cookies-data.backup.json');
21
+
22
+ // 确保数据目录存在
23
+ this.ensureDataDirectory();
24
+ }
25
+
26
+ /**
27
+ * 确保数据目录存在
28
+ */
29
+ ensureDataDirectory() {
30
+ const dataDir = path.dirname(this.dataFilePath);
31
+ if (!fs.existsSync(dataDir)) {
32
+ fs.mkdirSync(dataDir, { recursive: true });
33
+ logger.info(`创建数据目录: ${dataDir}`);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 保存Cookie数据到文件
39
+ * @param {Array} cookieEntries - Cookie条目数组
40
+ * @returns {boolean} - 是否保存成功
41
+ */
42
+ saveCookieData(cookieEntries) {
43
+ try {
44
+ // 准备要保存的数据
45
+ const dataToSave = {
46
+ version: '1.0',
47
+ lastUpdated: new Date().toISOString(),
48
+ cookies: cookieEntries.map(entry => ({
49
+ userId: entry.userId,
50
+ spaceId: entry.spaceId,
51
+ threadId: entry.threadId,
52
+ enabled: entry.enabled,
53
+ valid: entry.valid,
54
+ lastUsed: entry.lastUsed,
55
+ // 不保存实际的cookie值,只保存其哈希或标识
56
+ cookieHash: this.hashCookie(entry.cookie)
57
+ }))
58
+ };
59
+
60
+ // 先备份现有文件
61
+ if (fs.existsSync(this.dataFilePath)) {
62
+ fs.copyFileSync(this.dataFilePath, this.backupFilePath);
63
+ }
64
+
65
+ // 保存新数据
66
+ fs.writeFileSync(this.dataFilePath, JSON.stringify(dataToSave, null, 2), 'utf8');
67
+ logger.info(`成功保存 ${cookieEntries.length} 个Cookie的数据`);
68
+ return true;
69
+ } catch (error) {
70
+ logger.error(`保存Cookie数据失败: ${error.message}`);
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 加载Cookie数据
77
+ * @returns {Object|null} - 加载的数据或null
78
+ */
79
+ loadCookieData() {
80
+ try {
81
+ if (!fs.existsSync(this.dataFilePath)) {
82
+ logger.info('Cookie数据文件不存在');
83
+ return null;
84
+ }
85
+
86
+ const fileContent = fs.readFileSync(this.dataFilePath, 'utf8');
87
+ const data = JSON.parse(fileContent);
88
+
89
+ logger.info(`成功加载 ${data.cookies?.length || 0} 个Cookie的数据`);
90
+ return data;
91
+ } catch (error) {
92
+ logger.error(`加载Cookie数据失败: ${error.message}`);
93
+
94
+ // 尝试从备份恢复
95
+ if (fs.existsSync(this.backupFilePath)) {
96
+ try {
97
+ logger.info('尝试从备份文件恢复...');
98
+ const backupContent = fs.readFileSync(this.backupFilePath, 'utf8');
99
+ const backupData = JSON.parse(backupContent);
100
+
101
+ // 将备份恢复为主文件
102
+ fs.copyFileSync(this.backupFilePath, this.dataFilePath);
103
+ logger.success('成功从备份恢复数据');
104
+ return backupData;
105
+ } catch (backupError) {
106
+ logger.error(`从备份恢复失败: ${backupError.message}`);
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 合并保存的数据和内存中的Cookie条目
116
+ * @param {Array} cookieEntries - 内存中的Cookie条目
117
+ * @param {Object} savedData - 保存的数据
118
+ * @returns {Array} - 合并后的Cookie条目
119
+ */
120
+ mergeCookieData(cookieEntries, savedData) {
121
+ if (!savedData || !savedData.cookies) {
122
+ return cookieEntries;
123
+ }
124
+
125
+ const mergedEntries = [];
126
+
127
+ // 为每个内存中的cookie条目恢复保存的数据
128
+ for (const entry of cookieEntries) {
129
+ const savedEntry = savedData.cookies.find(saved =>
130
+ saved.userId === entry.userId ||
131
+ saved.cookieHash === this.hashCookie(entry.cookie)
132
+ );
133
+
134
+ if (savedEntry) {
135
+ // 恢复保存的数据
136
+ entry.threadId = savedEntry.threadId || entry.threadId;
137
+ entry.enabled = savedEntry.enabled !== undefined ? savedEntry.enabled : entry.enabled;
138
+ entry.lastUsed = savedEntry.lastUsed || entry.lastUsed;
139
+
140
+ logger.info(`恢复用户 ${entry.userId} 的数据: threadId=${entry.threadId}`);
141
+ }
142
+
143
+ mergedEntries.push(entry);
144
+ }
145
+
146
+ return mergedEntries;
147
+ }
148
+
149
+ /**
150
+ * 生成Cookie的哈希值(用于匹配,不存储实际cookie)
151
+ * @param {string} cookie - Cookie字符串
152
+ * @returns {string} - 哈希值
153
+ */
154
+ hashCookie(cookie) {
155
+ if (!cookie) return '';
156
+
157
+ // 简单的哈希实现,���cookie的前20个字符和后20个字符
158
+ const prefix = cookie.substring(0, 20);
159
+ const suffix = cookie.substring(Math.max(0, cookie.length - 20));
160
+ return `${prefix}...${suffix}`;
161
+ }
162
+
163
+ /**
164
+ * 清理过期数据
165
+ * @param {number} daysToKeep - 保留多少天的数据
166
+ */
167
+ cleanupOldData(daysToKeep = 30) {
168
+ try {
169
+ const dataDir = path.dirname(this.dataFilePath);
170
+ const files = fs.readdirSync(dataDir);
171
+ const now = Date.now();
172
+ const maxAge = daysToKeep * 24 * 60 * 60 * 1000;
173
+
174
+ files.forEach(file => {
175
+ if (file.startsWith('cookies-data') && file.endsWith('.backup.json')) {
176
+ const filePath = path.join(dataDir, file);
177
+ const stats = fs.statSync(filePath);
178
+
179
+ if (now - stats.mtime.getTime() > maxAge) {
180
+ fs.unlinkSync(filePath);
181
+ logger.info(`删除过期备份文件: ${file}`);
182
+ }
183
+ }
184
+ });
185
+ } catch (error) {
186
+ logger.error(`清理过期数据失败: ${error.message}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ export const storageManager = new StorageManager();