Spaces:
Paused
Paused
Upload 27 files
Browse files- .env.example +5 -1
- .gitignore +14 -1
- docs/admin-guide.md +134 -0
- package.json +9 -6
- public/admin.html +754 -0
- public/admin.js +758 -0
- src/CookieManager.js +170 -10
- src/ProxyPool.js +52 -34
- src/ProxyServer.js +8 -9
- src/app.js +183 -0
- src/config/index.js +80 -0
- src/lightweight-client-express.js +1 -1
- src/lightweight-client.js +25 -869
- src/middleware/auth.js +128 -0
- src/models.js +6 -1
- src/proxy/chrome_proxy_server_android_arm64 +2 -2
- src/proxy/chrome_proxy_server_linux_amd64 +2 -2
- src/proxy/chrome_proxy_server_windows_amd64.exe +2 -2
- src/routes/api.js +493 -0
- src/services/NotionClient.js +653 -0
- src/services/StreamManager.js +144 -0
- src/utils/logger.js +88 -0
- src/utils/storage.js +191 -0
.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/
|
| 6 |
"type": "module",
|
| 7 |
"bin": {
|
| 8 |
"notion-cookie": "src/cookie-cli.js"
|
| 9 |
},
|
| 10 |
"scripts": {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
//
|
| 358 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
// 更新索引,实现轮询
|
| 361 |
-
this.currentIndex = (this.currentIndex + 1) %
|
| 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
|
| 409 |
-
spaceId: entry.spaceId
|
| 410 |
valid: entry.valid,
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
| 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 - 测试代理超时时间(毫秒),默认
|
| 13 |
-
* @param {number} options.requestTimeout - 请求目标网站超时时间(毫秒),默认
|
| 14 |
* @param {string} options.targetUrl - 目标网站URL,默认'https://www.notion.so'
|
| 15 |
-
* @param {number} options.concurrentRequests - 并发请求数量,默认
|
| 16 |
* @param {number} options.minThreshold - 可用代理数量低于此阈值时自动补充,默认5
|
| 17 |
* @param {number} options.checkInterval - 检查代理池状态的时间间隔(毫秒),默认30000
|
| 18 |
* @param {string} options.proxyProtocol - 代理协议,默认'http'
|
| 19 |
-
* @param {number} options.maxRefillAttempts - 最大补充尝试次数,默认
|
| 20 |
-
* @param {number} options.retryDelay - 重试延迟(毫秒),默认
|
| 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 ||
|
| 31 |
-
this.requestTimeout = options.requestTimeout ||
|
| 32 |
this.targetUrl = options.targetUrl || 'https://www.notion.so';
|
| 33 |
-
this.concurrentRequests = options.concurrentRequests ||
|
| 34 |
this.minThreshold = options.minThreshold || 5;
|
| 35 |
this.checkInterval = options.checkInterval || 30000; // 默认30秒检查一次
|
| 36 |
this.proxyProtocol = options.proxyProtocol || 'http';
|
| 37 |
-
this.maxRefillAttempts = options.maxRefillAttempts ||
|
| 38 |
-
this.retryDelay = options.retryDelay ||
|
| 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,
|
| 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 |
-
//
|
| 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 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 *
|
| 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:
|
| 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:
|
| 726 |
useCache: true, // 启用缓存
|
| 727 |
-
maxRefillAttempts:
|
| 728 |
-
retryDelay:
|
|
|
|
|
|
|
| 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 || '
|
| 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 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 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 = '
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 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 |
-
|
| 20 |
-
const __dirname = dirname(__filename);
|
| 21 |
|
| 22 |
-
|
| 23 |
-
dotenv.config({ path: join(dirname(__dirname), '.env') });
|
| 24 |
|
| 25 |
-
//
|
| 26 |
-
|
| 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 |
-
|
| 51 |
-
|
| 52 |
-
}
|
| 53 |
} catch (error) {
|
| 54 |
-
logger.error(
|
| 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 |
-
//
|
| 212 |
-
async function streamNotionResponse(notionRequestBody) {
|
| 213 |
-
|
| 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 |
-
//
|
| 313 |
-
|
| 314 |
-
|
| 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 |
-
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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();
|