Spaces:
Paused
Paused
Initial upload from Google Colab
Browse files- .dockerignore +10 -0
- .gitignore +41 -0
- Dockerfile +22 -0
- docker-compose.yml +17 -0
- docs/README.md +1 -0
- docs/README_CN.md +586 -0
- examples/README.md +120 -0
- examples/direct-api/axios-example.js +53 -0
- examples/direct-api/chat-management.js +80 -0
- examples/direct-api/fetch-example.js +40 -0
- examples/file-upload/test-file.txt +1 -0
- examples/file-upload/test-image.jpg +0 -0
- examples/file-upload/upload-example.js +213 -0
- examples/openai-sdk/conversation.js +66 -0
- examples/openai-sdk/image-analysis.js +48 -0
- examples/openai-sdk/openai-compatibility.js +78 -0
- examples/openai-sdk/simple.js +32 -0
- examples/openai-sdk/streaming.js +39 -0
- examples/openai-sdk/system-message.js +39 -0
- index.js +234 -0
- package.json +39 -0
- patch-cline.bat +57 -0
- scripts/addAccount.js +8 -0
- scripts/auth.js +143 -0
- src/AvaibleModels.txt +18 -0
- src/api/chat.js +753 -0
- src/api/chatHistory.js +346 -0
- src/api/fileUpload.js +284 -0
- src/api/modelMapping.js +181 -0
- src/api/routes.js +470 -0
- src/api/tokenManager.js +102 -0
- src/browser/auth.js +234 -0
- src/browser/browser.js +365 -0
- src/browser/session.js +166 -0
- src/logger/index.js +142 -0
- src/utils/accountSetup.js +164 -0
- start.bat +39 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
npm-debug.log
|
| 3 |
+
logs
|
| 4 |
+
session
|
| 5 |
+
uploads
|
| 6 |
+
.git
|
| 7 |
+
.gitignore
|
| 8 |
+
*.log
|
| 9 |
+
patch-cline.bat
|
| 10 |
+
start.bat
|
.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Зависимости Node.js
|
| 2 |
+
node_modules/
|
| 3 |
+
package-lock.json
|
| 4 |
+
yarn.lock
|
| 5 |
+
|
| 6 |
+
# Данные сессии и кэш
|
| 7 |
+
session/
|
| 8 |
+
.cache/
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Служебные файлы
|
| 12 |
+
.DS_Store
|
| 13 |
+
.env
|
| 14 |
+
.env.local
|
| 15 |
+
.env.development.local
|
| 16 |
+
.env.test.local
|
| 17 |
+
.env.production.local
|
| 18 |
+
|
| 19 |
+
# Логи
|
| 20 |
+
logs
|
| 21 |
+
*.log
|
| 22 |
+
npm-debug.log*
|
| 23 |
+
yarn-debug.log*
|
| 24 |
+
yarn-error.log*
|
| 25 |
+
|
| 26 |
+
# Файлы Python
|
| 27 |
+
__pycache__/
|
| 28 |
+
*.py[cod]
|
| 29 |
+
*$py.class
|
| 30 |
+
.pytest_cache/
|
| 31 |
+
venv/
|
| 32 |
+
.env/
|
| 33 |
+
.venv/
|
| 34 |
+
|
| 35 |
+
# Файлы редакторов
|
| 36 |
+
.idea/
|
| 37 |
+
.vscode/
|
| 38 |
+
*.swp
|
| 39 |
+
*.swo
|
| 40 |
+
|
| 41 |
+
src/Authorization.txt
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1.6
|
| 2 |
+
FROM mcr.microsoft.com/playwright:v1.55.1-jammy AS base
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
RUN npm ci --omit=dev
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
ENV NODE_ENV=production \
|
| 12 |
+
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
| 13 |
+
|
| 14 |
+
RUN npx playwright install --with-deps chromium \
|
| 15 |
+
&& mkdir -p /app/session /app/logs /app/uploads \
|
| 16 |
+
&& chown -R pwuser:pwuser /app
|
| 17 |
+
|
| 18 |
+
USER pwuser
|
| 19 |
+
|
| 20 |
+
EXPOSE 3264
|
| 21 |
+
|
| 22 |
+
CMD ["node", "index.js"]
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
qwen-proxy:
|
| 3 |
+
build: .
|
| 4 |
+
image: qwen-api-proxy:latest
|
| 5 |
+
container_name: qwen-proxy
|
| 6 |
+
environment:
|
| 7 |
+
- NODE_ENV=production
|
| 8 |
+
- PORT=${PORT:-3264}
|
| 9 |
+
- HOST=0.0.0.0
|
| 10 |
+
- SKIP_ACCOUNT_MENU=true
|
| 11 |
+
ports:
|
| 12 |
+
- "${PORT:-3264}:3264"
|
| 13 |
+
volumes:
|
| 14 |
+
- ./session:/app/session
|
| 15 |
+
- ./logs:/app/logs
|
| 16 |
+
- ./uploads:/app/uploads
|
| 17 |
+
restart: unless-stopped
|
docs/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ref
|
docs/README_CN.md
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen AI API 代理
|
| 2 |
+
|
| 3 |
+
本地 API 代理服务器,通过浏览器模拟与 Qwen AI 交互。允许用户无需官方 API 密钥即可使用 Qwen 模型。
|
| 4 |
+
|
| 5 |
+
- **免费访问**:无需支付 API 密钥费用即可使用 Qwen 模型
|
| 6 |
+
- **完全兼容**:支持 OpenAI 兼容接口,便于集成
|
| 7 |
+
|
| 8 |
+
## 📋 目录
|
| 9 |
+
|
| 10 |
+
- [🚀 快速开始](#-快速开始)
|
| 11 |
+
- [安装](#安装)
|
| 12 |
+
- [启动](#启动)
|
| 13 |
+
- [💡 功能](#-功能)
|
| 14 |
+
- [📘 API 参考](#-api-参考)
|
| 15 |
+
- [主要端点](#主要端点)
|
| 16 |
+
- [请求格式](#请求格式)
|
| 17 |
+
- [对话历史管理](#对话历史管理)
|
| 18 |
+
- [图像处理](#图像处理)
|
| 19 |
+
- [文件上传](#文件上传)
|
| 20 |
+
- [对话管理](#对话管理)
|
| 21 |
+
- [📝 使用示例](#-使用示例)
|
| 22 |
+
- [文本请求](#文本请求)
|
| 23 |
+
- [图像请求](#图像请求)
|
| 24 |
+
- [Postman 示例](#postman-示例)
|
| 25 |
+
- [🔄 上下文管理](#-上下文管理)
|
| 26 |
+
- [🔌 OpenAI API 兼容性](#-openai-api-兼容性)
|
| 27 |
+
- [主要特性](#主要特性)
|
| 28 |
+
- [流式输出支持](#流式输出支持)
|
| 29 |
+
- [OpenAI SDK 使用示例](#openai-sdk-使用示例)
|
| 30 |
+
- [🔧 实现细节](#-实现细节)
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 🚀 快速开始
|
| 35 |
+
|
| 36 |
+
### 安装
|
| 37 |
+
|
| 38 |
+
1. 克隆仓库
|
| 39 |
+
2. 安装依赖:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
npm install
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### 启动
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
npm start
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
也可以使用快速启动文件:
|
| 52 |
+
|
| 53 |
+
```
|
| 54 |
+
start.bat
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
> **注意:** 首次启动时会打开浏览器窗口,您需要在 Qwen AI 网站上进行登录授权。成功登录后,按回车键继续。
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 💡 功能
|
| 62 |
+
|
| 63 |
+
本项目允许您:
|
| 64 |
+
|
| 65 |
+
- 通过本地 API 使用 Qwen AI 模型
|
| 66 |
+
- 在请求之间保存对话上下文
|
| 67 |
+
- 通过 API 管理对话
|
| 68 |
+
- 选择不同的 Qwen 模型生成回答
|
| 69 |
+
- 发送图像进行分析
|
| 70 |
+
- 使用支持流式输出的 OpenAI 兼容 API
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## 📘 API 参考
|
| 75 |
+
|
| 76 |
+
### 主要端点
|
| 77 |
+
|
| 78 |
+
| 端点 | 方法 | 描述 |
|
| 79 |
+
|----------|-------|----------|
|
| 80 |
+
| `/api/chat` | POST | 发送消息并获取回复 |
|
| 81 |
+
| `/api/chat/completions` | POST | 支持流式输出的 OpenAI 兼容端点 |
|
| 82 |
+
| `/api/models` | GET | 获取可用模型列表 |
|
| 83 |
+
| `/api/status` | GET | 检查授权状态 |
|
| 84 |
+
| `/api/files/upload` | POST | 上传图像用于请求 |
|
| 85 |
+
| `/api/chats` | POST/GET | 创建新对话 / 获取所有对话列表 |
|
| 86 |
+
| `/api/chats/:chatId` | GET/DELETE | 获取对话历史 / 删除对话 |
|
| 87 |
+
| `/api/chats/:chatId/rename` | PUT | 重命名对话 |
|
| 88 |
+
| `/api/chats/cleanup` | POST | 根据条件自动删除对话 |
|
| 89 |
+
|
| 90 |
+
### 请求格式
|
| 91 |
+
|
| 92 |
+
代理支持两种向 `/api/chat` 发送请求的格式:
|
| 93 |
+
|
| 94 |
+
#### 1. 使用 `message` 参数的简化格式
|
| 95 |
+
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"message": "消息文本",
|
| 99 |
+
"model": "qwen-max-latest",
|
| 100 |
+
"chatId": "对话ID"
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
#### 2. 与官方 Qwen API 兼容的 `messages` 参数格式
|
| 105 |
+
|
| 106 |
+
```json
|
| 107 |
+
{
|
| 108 |
+
"messages": [
|
| 109 |
+
{"role": "user", "content": "你好,最近怎么样?"}
|
| 110 |
+
],
|
| 111 |
+
"model": "qwen-max-latest",
|
| 112 |
+
"chatId": "对话ID"
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### 对话历史管理
|
| 117 |
+
|
| 118 |
+
> **重要提示:** 代理在服务器上使用内部系统存储对话历史。
|
| 119 |
+
|
| 120 |
+
1. 使用 `message` 格式时 - 消息直接添加到对话历史中。
|
| 121 |
+
2. 使用 `messages` 格式时 - 只从数组中提取最后一条用户消息并添加到历史中。
|
| 122 |
+
|
| 123 |
+
发送请求到官方 Qwen API 时,**始终**使用与指定 `chatId` 关联的完整对话历史。这意味着使用 `messages` 参数时,您只需包含带有 "user" 角色的新用户消息,而不是整个对话历史。
|
| 124 |
+
|
| 125 |
+
### 图像处理
|
| 126 |
+
|
| 127 |
+
代理支持在两种格式中发送带图像的消息:
|
| 128 |
+
|
| 129 |
+
#### 带图像的 `message` 格式
|
| 130 |
+
|
| 131 |
+
```json
|
| 132 |
+
{
|
| 133 |
+
"message": [
|
| 134 |
+
{
|
| 135 |
+
"type": "text",
|
| 136 |
+
"text": "描述这张图片中的物体"
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"type": "image",
|
| 140 |
+
"image": "图像URL"
|
| 141 |
+
}
|
| 142 |
+
],
|
| 143 |
+
"model": "qwen3-235b-a22b",
|
| 144 |
+
"chatId": "对话ID"
|
| 145 |
+
}
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
#### 带图像的 `messages` 格式
|
| 149 |
+
|
| 150 |
+
```json
|
| 151 |
+
{
|
| 152 |
+
"messages": [
|
| 153 |
+
{
|
| 154 |
+
"role": "user",
|
| 155 |
+
"content": [
|
| 156 |
+
{
|
| 157 |
+
"type": "text",
|
| 158 |
+
"text": "描述这张图片中的物体"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"type": "image",
|
| 162 |
+
"image": "图像URL"
|
| 163 |
+
}
|
| 164 |
+
]
|
| 165 |
+
}
|
| 166 |
+
],
|
| 167 |
+
"model": "qwen3-235b-a22b",
|
| 168 |
+
"chatId": "对话ID"
|
| 169 |
+
}
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### 文件上传
|
| 173 |
+
|
| 174 |
+
#### 上传图像
|
| 175 |
+
|
| 176 |
+
```
|
| 177 |
+
POST http://localhost:3264/api/files/upload
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
**请求格式:** `multipart/form-data`
|
| 181 |
+
|
| 182 |
+
**参数:**
|
| 183 |
+
|
| 184 |
+
- `file` - 图像文件(支持格式:jpg, jpeg, png, gif, webp)
|
| 185 |
+
|
| 186 |
+
**使用 curl 的示例:**
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
curl -X POST http://localhost:3264/api/files/upload \
|
| 190 |
+
-F "file=@/path/to/image.jpg"
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
**响应示例:**
|
| 194 |
+
|
| 195 |
+
```json
|
| 196 |
+
{
|
| 197 |
+
"imageUrl": "https://cdn.qwenlm.ai/user-id/file-id_filename.jpg?key=..."
|
| 198 |
+
}
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
#### 获取图像 URL
|
| 202 |
+
|
| 203 |
+
要通过 API 代理发送图像,您首先需要获取图像 URL。可以通过两种方式实现:
|
| 204 |
+
|
| 205 |
+
##### 方法 1:通过 API 代理上传
|
| 206 |
+
|
| 207 |
+
如上所述,向 `/api/files/upload` 端点发送 POST 请求上传图像。
|
| 208 |
+
|
| 209 |
+
##### 方法 2:通过 Qwen 网页界面获取 URL
|
| 210 |
+
|
| 211 |
+
1. 在官方 Qwen 网页界面上传图像 (<https://chat.qwen.ai/>)
|
| 212 |
+
2. 打开浏览器开发者工��(F12 或 Ctrl+Shift+I)
|
| 213 |
+
3. 切换到 "Network"(网络)选项卡
|
| 214 |
+
4. 找到包含您图像的 API Qwen 请求(通常是 GetsToken 请求)
|
| 215 |
+
5. 在请求主体中找到图像 URL,格式类似:`https://cdn.qwenlm.ai/user-id/file-id_filename.jpg?key=...`
|
| 216 |
+
6. 复制此 URL 以在 API 请求中使用
|
| 217 |
+
|
| 218 |
+
### 对话管理
|
| 219 |
+
|
| 220 |
+
#### 创建新对话
|
| 221 |
+
|
| 222 |
+
```
|
| 223 |
+
POST http://localhost:3264/api/chats
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
**请求体:**
|
| 227 |
+
|
| 228 |
+
```json
|
| 229 |
+
{
|
| 230 |
+
"name": "对话名称"
|
| 231 |
+
}
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
**响应:**
|
| 235 |
+
|
| 236 |
+
```json
|
| 237 |
+
{
|
| 238 |
+
"chatId": "唯一标识符"
|
| 239 |
+
}
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
#### 获取所有对话列表
|
| 243 |
+
|
| 244 |
+
```
|
| 245 |
+
GET http://localhost:3264/api/chats
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
#### 获取对话历史
|
| 249 |
+
|
| 250 |
+
```
|
| 251 |
+
GET http://localhost:3264/api/chats/:chatId
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
#### 删除对话
|
| 255 |
+
|
| 256 |
+
```
|
| 257 |
+
DELETE http://localhost:3264/api/chats/:chatId
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
#### 重命名对话
|
| 261 |
+
|
| 262 |
+
```
|
| 263 |
+
PUT http://localhost:3264/api/chats/:chatId/rename
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
**请求体:**
|
| 267 |
+
|
| 268 |
+
```json
|
| 269 |
+
{
|
| 270 |
+
"name": "新对话名称"
|
| 271 |
+
}
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
#### 自动删除对话
|
| 275 |
+
|
| 276 |
+
```
|
| 277 |
+
POST http://localhost:3264/api/chats/cleanup
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
**请求体**(所有参数都是可选的):
|
| 281 |
+
|
| 282 |
+
```json
|
| 283 |
+
{
|
| 284 |
+
"olderThan": 604800000, // 删除超过指定时间的对话(毫秒),例如 7 天
|
| 285 |
+
"userMessageCountLessThan": 3, // 删除用户消息少于 3 条的对话
|
| 286 |
+
"messageCountLessThan": 5, // 删除总消息少于 5 条的对话
|
| 287 |
+
"maxChats": 50 // 只保留最新的 50 个对话
|
| 288 |
+
}
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
---
|
| 292 |
+
|
| 293 |
+
## 📝 使用示例
|
| 294 |
+
|
| 295 |
+
### 文本请求
|
| 296 |
+
|
| 297 |
+
#### 简单文本请求示例
|
| 298 |
+
|
| 299 |
+
```bash
|
| 300 |
+
curl -X POST http://localhost:3264/api/chat \
|
| 301 |
+
-H "Content-Type: application/json" \
|
| 302 |
+
-d '{
|
| 303 |
+
"message": "什么是人工智能?",
|
| 304 |
+
"model": "qwen-max-latest"
|
| 305 |
+
}'
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
#### 官方 API 格式请求示例
|
| 309 |
+
|
| 310 |
+
```bash
|
| 311 |
+
curl -X POST http://localhost:3264/api/chat \
|
| 312 |
+
-H "Content-Type: application/json" \
|
| 313 |
+
-d '{
|
| 314 |
+
"messages": [
|
| 315 |
+
{"role": "user", "content": "什么是人工智能?"}
|
| 316 |
+
],
|
| 317 |
+
"model": "qwen-max-latest"
|
| 318 |
+
}'
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
### 图像请求
|
| 322 |
+
|
| 323 |
+
#### 上传图像并发送请求示例
|
| 324 |
+
|
| 325 |
+
```bash
|
| 326 |
+
# 步骤 1:上传图像
|
| 327 |
+
UPLOAD_RESPONSE=$(curl -s -X POST http://localhost:3264/api/files/upload \
|
| 328 |
+
-F "file=@/path/to/image.jpg")
|
| 329 |
+
|
| 330 |
+
# 步骤 2:提取图像 URL
|
| 331 |
+
IMAGE_URL=$(echo $UPLOAD_RESPONSE | grep -o '"imageUrl":"[^"]*"' | sed 's/"imageUrl":"//;s/"//')
|
| 332 |
+
|
| 333 |
+
# 步骤 3:发送带图像的请求
|
| 334 |
+
curl -X POST http://localhost:3264/api/chat \
|
| 335 |
+
-H "Content-Type: application/json" \
|
| 336 |
+
-d '{
|
| 337 |
+
"message": [
|
| 338 |
+
{
|
| 339 |
+
"type": "text",
|
| 340 |
+
"text": "描述这张图片中的物体"
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"type": "image",
|
| 344 |
+
"image": "'$IMAGE_URL'"
|
| 345 |
+
}
|
| 346 |
+
],
|
| 347 |
+
"model": "qwen3-235b-a22b"
|
| 348 |
+
}'
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
### Postman 示例
|
| 352 |
+
|
| 353 |
+
#### 上传并使用图像
|
| 354 |
+
|
| 355 |
+
1. **上传图像**:
|
| 356 |
+
- 创建一个新的 POST 请求到 `http://localhost:3264/api/files/upload`
|
| 357 |
+
- 选择 "Body" 选项卡
|
| 358 |
+
- 选择类型 "form-data"
|
| 359 |
+
- 添加键 "file" 并选择类型 "File"
|
| 360 |
+
- 点击 "Select Files" 按钮上传图像
|
| 361 |
+
- 点击 "Send"
|
| 362 |
+
|
| 363 |
+
响应将包含图像 URL:
|
| 364 |
+
|
| 365 |
+
```json
|
| 366 |
+
{
|
| 367 |
+
"imageUrl": "https://cdn.qwenlm.ai/user-id/file-id_filename.jpg?key=..."
|
| 368 |
+
}
|
| 369 |
+
```
|
| 370 |
+
|
| 371 |
+
2. **在请求中使用图像**:
|
| 372 |
+
- 创建一个新的 POST 请求到 `http://localhost:3264/api/chat`
|
| 373 |
+
- 选择 "Body" 选项卡
|
| 374 |
+
- 选择类型 "raw" 和格式 "JSON"
|
| 375 |
+
- 粘贴以下 JSON,将 `图像URL` 替换为获取的 URL:
|
| 376 |
+
|
| 377 |
+
```json
|
| 378 |
+
{
|
| 379 |
+
"message": [
|
| 380 |
+
{
|
| 381 |
+
"type": "text",
|
| 382 |
+
"text": "描述这张图片中的物体"
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
"type": "image",
|
| 386 |
+
"image": "图像URL"
|
| 387 |
+
}
|
| 388 |
+
],
|
| 389 |
+
"model": "qwen3-235b-a22b"
|
| 390 |
+
}
|
| 391 |
+
```
|
| 392 |
+
|
| 393 |
+
- 点击 "Send"
|
| 394 |
+
|
| 395 |
+
#### 使用 OpenAI 兼容端点
|
| 396 |
+
|
| 397 |
+
1. **通过 OpenAI 兼容端点发送请求**:
|
| 398 |
+
- 创建一个新的 POST 请求到 `http://localhost:3264/api/chat/completions`
|
| 399 |
+
- 选择 "Body" 选项卡
|
| 400 |
+
- 选择类型 "raw" 和格式 "JSON"
|
| 401 |
+
- 粘贴以下 JSON,将 `图像URL` 替换为获取的 URL:
|
| 402 |
+
|
| 403 |
+
```json
|
| 404 |
+
{
|
| 405 |
+
"messages": [
|
| 406 |
+
{
|
| 407 |
+
"role": "user",
|
| 408 |
+
"content": [
|
| 409 |
+
{
|
| 410 |
+
"type": "text",
|
| 411 |
+
"text": "描述这张图片中的内容是什么?"
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"type": "image",
|
| 415 |
+
"image": "图像URL"
|
| 416 |
+
}
|
| 417 |
+
]
|
| 418 |
+
}
|
| 419 |
+
],
|
| 420 |
+
"model": "qwen3-235b-a22b"
|
| 421 |
+
}
|
| 422 |
+
```
|
| 423 |
+
|
| 424 |
+
- 点击 "Send"
|
| 425 |
+
|
| 426 |
+
2. **流式模式请求**:
|
| 427 |
+
- 使用相同的 URL 和请求体,但添加参数 `"stream": true`
|
| 428 |
+
- 注意:要在 Postman 中正确显示流,请在控制台中勾选 "Preserve log" 选项
|
| 429 |
+
|
| 430 |
+
---
|
| 431 |
+
|
| 432 |
+
## 🔄 上下文管理
|
| 433 |
+
|
| 434 |
+
系统会自动保存对话历史并在每个请求中发送到 Qwen API。这使模型能够在生成回答时考虑之前的消息。
|
| 435 |
+
|
| 436 |
+
### 上下文工作流程
|
| 437 |
+
|
| 438 |
+
1. **首次请求**(不指定 `chatId`):
|
| 439 |
+
|
| 440 |
+
```json
|
| 441 |
+
{
|
| 442 |
+
"message": "你好,你叫什么名字?"
|
| 443 |
+
}
|
| 444 |
+
```
|
| 445 |
+
|
| 446 |
+
2. **响应**(包含 `chatId`):
|
| 447 |
+
|
| 448 |
+
```json
|
| 449 |
+
{
|
| 450 |
+
"chatId": "abcd-1234-5678",
|
| 451 |
+
"choices": [...]
|
| 452 |
+
}
|
| 453 |
+
```
|
| 454 |
+
|
| 455 |
+
3. **后续请求**(使用获得的 `chatId`):
|
| 456 |
+
|
| 457 |
+
```json
|
| 458 |
+
{
|
| 459 |
+
"message": "2+2等于多少?",
|
| 460 |
+
"chatId": "abcd-1234-5678"
|
| 461 |
+
}
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
---
|
| 465 |
+
|
| 466 |
+
## 🔌 OpenAI API 兼容性
|
| 467 |
+
|
| 468 |
+
代理支持 OpenAI API 兼容端点,用于连接使用 OpenAI API 的客户端:
|
| 469 |
+
|
| 470 |
+
```
|
| 471 |
+
POST /api/chat/completions
|
| 472 |
+
```
|
| 473 |
+
|
| 474 |
+
### 主要特性
|
| 475 |
+
|
| 476 |
+
1. **为每个请求创建新对话:** 每个向 `/chat/completions` 的请求都会在系统中创建一个名为 "OpenAI API Chat" 的新对话。
|
| 477 |
+
|
| 478 |
+
2. **保存完整消息历史:** 请求中的所有消息(包括系统消息、用户消息和助手消息)都会保存在对话历史中。
|
| 479 |
+
|
| 480 |
+
3. **支持系统消息:** 代理正确处理并保存系统消息(`role: "system"`),这些消息通常用于配置模型行为。
|
| 481 |
+
|
| 482 |
+
**带系统消息的请求示例:**
|
| 483 |
+
|
| 484 |
+
```json
|
| 485 |
+
{
|
| 486 |
+
"messages": [
|
| 487 |
+
{"role": "system", "content": "你是 JavaScript 专家。只回答关于 JavaScript 的问题。"},
|
| 488 |
+
{"role": "user", "content": "如何在 JavaScript 中创建类?"}
|
| 489 |
+
],
|
| 490 |
+
"model": "qwen-max-latest"
|
| 491 |
+
}
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
### 流式输出支持
|
| 495 |
+
|
| 496 |
+
代理支持响应流式传输模式,允许您实时分批接收响应:
|
| 497 |
+
|
| 498 |
+
```json
|
| 499 |
+
{
|
| 500 |
+
"messages": [
|
| 501 |
+
{"role": "user", "content": "写一个关于太空的长故事"}
|
| 502 |
+
],
|
| 503 |
+
"model": "qwen-max-latest",
|
| 504 |
+
"stream": true
|
| 505 |
+
}
|
| 506 |
+
```
|
| 507 |
+
|
| 508 |
+
使用流式模式时,响应将以与 OpenAI API 兼容的 Server-Sent Events (SSE) 格式逐步返回。
|
| 509 |
+
|
| 510 |
+
### OpenAI SDK 使用示例
|
| 511 |
+
|
| 512 |
+
```javascript
|
| 513 |
+
// 使用 OpenAI Node.js SDK 的示例
|
| 514 |
+
import OpenAI from 'openai';
|
| 515 |
+
import fs from 'fs';
|
| 516 |
+
import axios from 'axios';
|
| 517 |
+
|
| 518 |
+
const openai = new OpenAI({
|
| 519 |
+
baseURL: 'http://localhost:3264/api', // 代理的基本 URL
|
| 520 |
+
apiKey: 'dummy-key', // 不需要真实密钥,但库要求此字段
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
// 不使用流式输出的请求
|
| 524 |
+
const completion = await openai.chat.completions.create({
|
| 525 |
+
messages: [{ role: 'user', content: '你好,最近怎么样?' }],
|
| 526 |
+
model: 'qwen-max-latest', // 使用的 Qwen 模型
|
| 527 |
+
});
|
| 528 |
+
|
| 529 |
+
console.log(completion.choices[0].message);
|
| 530 |
+
|
| 531 |
+
// 使用流式输出的请求
|
| 532 |
+
const stream = await openai.chat.completions.create({
|
| 533 |
+
messages: [{ role: 'user', content: '讲一个关于太空的长故事' }],
|
| 534 |
+
model: 'qwen-max-latest',
|
| 535 |
+
stream: true,
|
| 536 |
+
});
|
| 537 |
+
|
| 538 |
+
for await (const chunk of stream) {
|
| 539 |
+
process.stdout.write(chunk.choices[0]?.delta?.content || '');
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
// 上传并使用图像
|
| 543 |
+
async function uploadAndAnalyzeImage(imagePath) {
|
| 544 |
+
// 通过 API 代理上传图像
|
| 545 |
+
const formData = new FormData();
|
| 546 |
+
formData.append('file', fs.createReadStream(imagePath));
|
| 547 |
+
|
| 548 |
+
const uploadResponse = await axios.post('http://localhost:3264/api/files/upload', formData, {
|
| 549 |
+
headers: { 'Content-Type': 'multipart/form-data' }
|
| 550 |
+
});
|
| 551 |
+
|
| 552 |
+
const imageUrl = uploadResponse.data.imageUrl;
|
| 553 |
+
|
| 554 |
+
// 创建带图像的请求
|
| 555 |
+
const completion = await openai.chat.completions.create({
|
| 556 |
+
messages: [
|
| 557 |
+
{
|
| 558 |
+
role: 'user',
|
| 559 |
+
content: [
|
| 560 |
+
{ type: 'text', text: '描述这张图片中的内容是什么?' },
|
| 561 |
+
{ type: 'image', image: imageUrl }
|
| 562 |
+
]
|
| 563 |
+
}
|
| 564 |
+
],
|
| 565 |
+
model: 'qwen3-235b-a22b',
|
| 566 |
+
});
|
| 567 |
+
|
| 568 |
+
console.log(completion.choices[0].message.content);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
// 使用方法:uploadAndAnalyzeImage('./image.jpg');
|
| 572 |
+
```
|
| 573 |
+
|
| 574 |
+
> **兼容性限制:**
|
| 575 |
+
>
|
| 576 |
+
> 1. 一些 OpenAI 特有的参数(如 `logprobs`、`functions` 等)不受支持。
|
| 577 |
+
> 2. 流式传输速度可能与原始 OpenAI API 不同。
|
| 578 |
+
|
| 579 |
+
---
|
| 580 |
+
|
| 581 |
+
## 🔧 实现细节
|
| 582 |
+
|
| 583 |
+
- 代理通过无头浏览器模拟与 Qwen 网页界面的交互
|
| 584 |
+
- 自动管理会话和授权
|
| 585 |
+
- 通过浏览器页面池优化性能
|
| 586 |
+
- 支持自动保存和恢复授权状态
|
examples/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Примеры использования FreeQwenApi
|
| 2 |
+
|
| 3 |
+
В этой директории собраны примеры использования API-прокси для Qwen AI.
|
| 4 |
+
|
| 5 |
+
## Установка и запуск
|
| 6 |
+
|
| 7 |
+
Установка зависимостей производится в корневой директории проекта:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# В корневой директории проекта
|
| 11 |
+
npm install
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
Перед запуском примеров убедитесь, что сервер FreeQwenApi запущен и доступен по адресу `http://localhost:3264`.
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
# Запуск сервера
|
| 18 |
+
npm start
|
| 19 |
+
|
| 20 |
+
# В отдельном терминале запустите примеры
|
| 21 |
+
npm run example:simple
|
| 22 |
+
npm run example:stream
|
| 23 |
+
# и т.д.
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Примеры с использованием OpenAI SDK
|
| 27 |
+
|
| 28 |
+
### 1. Простой запрос (не потоковый)
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
npm run example:simple
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Демонстрирует отправку простого запроса к Qwen AI с использованием OpenAI SDK.
|
| 35 |
+
|
| 36 |
+
### 2. Потоковый запрос
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
npm run example:stream
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Показывает, как получать ответ в потоковом режиме, где токены приходят по мере их генерации.
|
| 43 |
+
|
| 44 |
+
### 3. Запрос с системным сообщением
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
npm run example:system
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Пример использования системного сообщения для задания роли и инструкций модели.
|
| 51 |
+
|
| 52 |
+
### 4. Анализ изображения
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
npm run example:image
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Демонстрация отправки изображения для анализа моделью (требуется заменить URL изображения в примере).
|
| 59 |
+
|
| 60 |
+
### 5. Диалог с несколькими сообщениями
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
npm run example:conversation
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Пример поддержания диалога из нескольких сообщений с сохранением контекста.
|
| 67 |
+
|
| 68 |
+
### 6. Совместимость с OpenAI API
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
npm run example:compatibility
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Демонстрация полной совместимости с форматом API OpenAI.
|
| 75 |
+
|
| 76 |
+
## Примеры прямого использования API
|
| 77 |
+
|
| 78 |
+
### 1. Запрос с использованием fetch
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
npm run example:direct
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
Пример отправки прямого запроса к API без использования SDK, с использованием нативного fetch.
|
| 85 |
+
|
| 86 |
+
### 2. Запрос с использованием axios
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
npm run example:axios
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Пример использования библиотеки axios для отправки запросов к API.
|
| 93 |
+
|
| 94 |
+
### 3. Управление диалогами
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
npm run example:chat-management
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Демонстрация API для управления диалогами: создание, получение списка, получение истории, переименование и удаление.
|
| 101 |
+
|
| 102 |
+
## Модификация примеров
|
| 103 |
+
|
| 104 |
+
Вы можете модифицировать примеры для своих нужд:
|
| 105 |
+
|
| 106 |
+
1. Изменяйте запросы и параметры в файлах примеров
|
| 107 |
+
2. Попробуйте различные модели (список доступен через `/api/models`)
|
| 108 |
+
3. Экспериментируйте с разными форматами запросов
|
| 109 |
+
|
| 110 |
+
## Работа с изображениями
|
| 111 |
+
|
| 112 |
+
Для примеров с изображениями необходимо:
|
| 113 |
+
|
| 114 |
+
1. Загрузить изображение в официальном веб-интерфейсе Qwen
|
| 115 |
+
2. Получить URL изображения из сетевых запросов (см. инструкцию в README.md основного проекта)
|
| 116 |
+
3. Заменить `IMAGE_URL` в примере на полученный URL
|
| 117 |
+
|
| 118 |
+
## Дополнительная информация
|
| 119 |
+
|
| 120 |
+
Подробная документация API доступна в README.md основного проекта.
|
examples/direct-api/axios-example.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример прямого запроса к API прокси Qwen с использованием axios
|
| 2 |
+
// Установка: npm install axios
|
| 3 |
+
// Для запуска примера: node axios-example.js
|
| 4 |
+
|
| 5 |
+
import axios from 'axios';
|
| 6 |
+
|
| 7 |
+
async function axiosExample() {
|
| 8 |
+
try {
|
| 9 |
+
console.log('Отправка запроса через axios к API Qwen...\n');
|
| 10 |
+
|
| 11 |
+
// Пример с форматом messages, совместимым с OpenAI
|
| 12 |
+
const response = await axios.post('http://localhost:3264/api/chat', {
|
| 13 |
+
messages: [
|
| 14 |
+
{ role: 'system', content: 'Ты эксперт по программированию на JavaScript.' },
|
| 15 |
+
{ role: 'user', content: 'Объясни, как работают асинхронные функции в JavaScript' }
|
| 16 |
+
],
|
| 17 |
+
model: 'qwen-max-latest'
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
console.log('Ответ от API:\n');
|
| 21 |
+
console.log(response.data.choices[0].message.content);
|
| 22 |
+
console.log('\nЗапрос успешно выполнен.');
|
| 23 |
+
|
| 24 |
+
// Вывод дополнительной информации
|
| 25 |
+
console.log('\nИнформация о запросе:');
|
| 26 |
+
console.log(`ID чата: ${response.data.chatId}`);
|
| 27 |
+
console.log(`Модель: ${response.data.model}`);
|
| 28 |
+
|
| 29 |
+
// Сохраняем ID чата для следующего примера
|
| 30 |
+
const chatId = response.data.chatId;
|
| 31 |
+
|
| 32 |
+
// Продолжаем диалог в том же чате
|
| 33 |
+
console.log('\n\nОтправка второго сообщения в тот же чат...\n');
|
| 34 |
+
|
| 35 |
+
const followUpResponse = await axios.post('http://localhost:3264/api/chat', {
|
| 36 |
+
message: 'Приведи пример использования async/await',
|
| 37 |
+
model: 'qwen-max-latest',
|
| 38 |
+
chatId: chatId
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
console.log('Ответ на второе сообщение:\n');
|
| 42 |
+
console.log(followUpResponse.data.choices[0].message.content);
|
| 43 |
+
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.error('Ошибка при выполнении запроса:', error);
|
| 46 |
+
if (error.response) {
|
| 47 |
+
console.error('Детали ошибки:', error.response.data);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Запуск
|
| 53 |
+
axiosExample();
|
examples/direct-api/chat-management.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример управления диалогами через API прокси Qwen
|
| 2 |
+
// Установка: npm install axios
|
| 3 |
+
// Для запуска примера: node chat-management.js
|
| 4 |
+
|
| 5 |
+
import axios from 'axios';
|
| 6 |
+
|
| 7 |
+
const API_BASE_URL = 'http://localhost:3264/api';
|
| 8 |
+
|
| 9 |
+
async function chatManagementExample() {
|
| 10 |
+
try {
|
| 11 |
+
console.log('Демонстрация API управления диалогами\n');
|
| 12 |
+
|
| 13 |
+
// 1. Создание нового диалога
|
| 14 |
+
console.log('1. Создание нового диалога...');
|
| 15 |
+
const createResponse = await axios.post(`${API_BASE_URL}/chats`, {
|
| 16 |
+
name: 'Тестовый диалог о программировании'
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
const chatId = createResponse.data.chatId;
|
| 20 |
+
console.log(`Создан диалог с ID: ${chatId}`);
|
| 21 |
+
|
| 22 |
+
// 2. Отправка сообщения в этот диалог
|
| 23 |
+
console.log('\n2. Отправка сообщения в диалог...');
|
| 24 |
+
await axios.post(`${API_BASE_URL}/chat`, {
|
| 25 |
+
message: 'Расскажи о Python и его преимуществах',
|
| 26 |
+
chatId: chatId,
|
| 27 |
+
model: 'qwen-max-latest'
|
| 28 |
+
});
|
| 29 |
+
console.log('Сообщение отправлено');
|
| 30 |
+
|
| 31 |
+
// 3. Получение списка всех диалогов
|
| 32 |
+
console.log('\n3. Получение списка всех диалогов...');
|
| 33 |
+
const chatsResponse = await axios.get(`${API_BASE_URL}/chats`);
|
| 34 |
+
console.log(`Найдено ${chatsResponse.data.chats.length} диалогов:`);
|
| 35 |
+
chatsResponse.data.chats.forEach(chat => {
|
| 36 |
+
console.log(`- ${chat.id}: ${chat.name} (${new Date(chat.createdAt).toLocaleString()})`);
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// 4. Получение истории конкретного диалога
|
| 40 |
+
console.log(`\n4. Получение истории диалога ${chatId}...`);
|
| 41 |
+
const historyResponse = await axios.get(`${API_BASE_URL}/chats/${chatId}`);
|
| 42 |
+
console.log(`Получена история диалога, ${historyResponse.data.history.messages.length} сообщений:`);
|
| 43 |
+
historyResponse.data.history.messages.forEach(msg => {
|
| 44 |
+
const timestamp = new Date(msg.timestamp * 1000).toLocaleTimeString();
|
| 45 |
+
console.log(`[${timestamp}] ${msg.role}: ${typeof msg.content === 'string' ? msg.content.substring(0, 50) + '...' : '[Составное сообщение]'}`);
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// 5. Переименование диалога
|
| 49 |
+
console.log(`\n5. Переименование диалога ${chatId}...`);
|
| 50 |
+
await axios.put(`${API_BASE_URL}/chats/${chatId}/rename`, {
|
| 51 |
+
name: 'Обновленное название диалога'
|
| 52 |
+
});
|
| 53 |
+
console.log('Диалог переименован');
|
| 54 |
+
|
| 55 |
+
// 6. Автоудаление старых диалогов (демонстрация API)
|
| 56 |
+
console.log('\n6. Демонстрация API автоудаления диалогов...');
|
| 57 |
+
const cleanupResponse = await axios.post(`${API_BASE_URL}/chats/cleanup`, {
|
| 58 |
+
olderThan: 30 * 24 * 60 * 60 * 1000, // Диалоги старше 30 дней
|
| 59 |
+
userMessageCountLessThan: 2, // С менее чем 2 сообщениями пользователя
|
| 60 |
+
maxChats: 100 // Оставить максимум 100 диалогов
|
| 61 |
+
});
|
| 62 |
+
console.log(`Автоудаление: ${cleanupResponse.data.deletedCount} диалогов удалено`);
|
| 63 |
+
|
| 64 |
+
// 7. Удаление тестового диалога
|
| 65 |
+
console.log(`\n7. Удаление тестового диалога ${chatId}...`);
|
| 66 |
+
await axios.delete(`${API_BASE_URL}/chats/${chatId}`);
|
| 67 |
+
console.log('Тестовый диалог удален');
|
| 68 |
+
|
| 69 |
+
console.log('\nПример управления диалогами успешно завершен!');
|
| 70 |
+
|
| 71 |
+
} catch (error) {
|
| 72 |
+
console.error('Ошибка в примере управления диалогами:', error);
|
| 73 |
+
if (error.response) {
|
| 74 |
+
console.error('Детали ошибки:', error.response.data);
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Запуск
|
| 80 |
+
chatManagementExample();
|
examples/direct-api/fetch-example.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример прямого запроса к API прокси Qwen с использованием fetch
|
| 2 |
+
// Для запуска примера: node fetch-example.js
|
| 3 |
+
|
| 4 |
+
async function directApiRequest() {
|
| 5 |
+
try {
|
| 6 |
+
console.log('Отправка прямого запроса к API Qwen...\n');
|
| 7 |
+
|
| 8 |
+
const response = await fetch('http://localhost:3264/api/chat', {
|
| 9 |
+
method: 'POST',
|
| 10 |
+
headers: {
|
| 11 |
+
'Content-Type': 'application/json',
|
| 12 |
+
},
|
| 13 |
+
body: JSON.stringify({
|
| 14 |
+
message: 'Объясни простыми словами, что такое искусственный интеллект',
|
| 15 |
+
model: 'qwen-max-latest'
|
| 16 |
+
})
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
if (!response.ok) {
|
| 20 |
+
throw new Error(`HTTP ошибка! Статус: ${response.status}`);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const result = await response.json();
|
| 24 |
+
|
| 25 |
+
console.log('Ответ от API:\n');
|
| 26 |
+
console.log(result.choices[0].message.content);
|
| 27 |
+
console.log('\nЗапрос успешно выполнен.');
|
| 28 |
+
|
| 29 |
+
// Вывод дополнительной информации
|
| 30 |
+
console.log('\nИнформация о запросе:');
|
| 31 |
+
console.log(`ID чата: ${result.chatId}`);
|
| 32 |
+
console.log(`Модель: ${result.model}`);
|
| 33 |
+
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error('Ошибка при выполнении запроса:', error);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Запуск
|
| 40 |
+
directApiRequest();
|
examples/file-upload/test-file.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Это тестовый файл для загрузки.
|
examples/file-upload/test-image.jpg
ADDED
|
examples/file-upload/upload-example.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример для тестирования загрузки файлов
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
import axios from 'axios';
|
| 6 |
+
import FormData from 'form-data';
|
| 7 |
+
|
| 8 |
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 9 |
+
|
| 10 |
+
// URL API
|
| 11 |
+
const API_URL = 'http://localhost:3264/api';
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Загружает тестовый файл на сервер
|
| 15 |
+
* @param {string} filePath - Путь к файлу для загрузки
|
| 16 |
+
* @returns {Promise<Object>} - Результат загрузки файла
|
| 17 |
+
*/
|
| 18 |
+
async function uploadTestFile(filePath) {
|
| 19 |
+
try {
|
| 20 |
+
console.log(`Загрузка файла: ${filePath}`);
|
| 21 |
+
|
| 22 |
+
if (!fs.existsSync(filePath)) {
|
| 23 |
+
throw new Error(`Файл не найден: ${filePath}`);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Создаем FormData для загрузки файла
|
| 27 |
+
const formData = new FormData();
|
| 28 |
+
formData.append('file', fs.createReadStream(filePath));
|
| 29 |
+
|
| 30 |
+
// Отправляем запрос на загрузку
|
| 31 |
+
const response = await axios.post(`${API_URL}/files/upload`, formData, {
|
| 32 |
+
headers: {
|
| 33 |
+
...formData.getHeaders()
|
| 34 |
+
},
|
| 35 |
+
maxContentLength: Infinity,
|
| 36 |
+
maxBodyLength: Infinity
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
console.log('Файл успешно загружен:');
|
| 40 |
+
console.log(JSON.stringify(response.data, null, 2));
|
| 41 |
+
|
| 42 |
+
return response.data;
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error('Ошибка при загрузке файла:');
|
| 45 |
+
if (error.response) {
|
| 46 |
+
console.error(error.response.data);
|
| 47 |
+
} else {
|
| 48 |
+
console.error(error.message);
|
| 49 |
+
}
|
| 50 |
+
throw error;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Получает STS токен напрямую (для тестирования)
|
| 56 |
+
* @param {Object} fileInfo - Информация о файле
|
| 57 |
+
* @returns {Promise<Object>} - Данные STS токена
|
| 58 |
+
*/
|
| 59 |
+
async function getTestStsToken(fileInfo) {
|
| 60 |
+
try {
|
| 61 |
+
console.log(`Запрос STS токена для файла: ${fileInfo.filename}`);
|
| 62 |
+
|
| 63 |
+
const response = await axios.post(`${API_URL}/files/getstsToken`, fileInfo);
|
| 64 |
+
|
| 65 |
+
console.log('Получен STS токен:');
|
| 66 |
+
console.log(JSON.stringify(response.data, null, 2));
|
| 67 |
+
|
| 68 |
+
return response.data;
|
| 69 |
+
} catch (error) {
|
| 70 |
+
console.error('Ошибка при получении STS токена:');
|
| 71 |
+
if (error.response) {
|
| 72 |
+
console.error(error.response.data);
|
| 73 |
+
} else {
|
| 74 |
+
console.error(error.message);
|
| 75 |
+
}
|
| 76 |
+
throw error;
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Напрямую загружает файл через OSS (для тестирования)
|
| 82 |
+
* @param {string} filePath - Путь к файлу
|
| 83 |
+
* @param {Object} stsData - Данные STS токена
|
| 84 |
+
* @returns {Promise<Object>} - Результат загрузки
|
| 85 |
+
*/
|
| 86 |
+
async function directUploadFile(filePath, stsData) {
|
| 87 |
+
try {
|
| 88 |
+
console.log(`Прямая загрузка файла: ${filePath}`);
|
| 89 |
+
|
| 90 |
+
if (!stsData || !stsData.file_url || !stsData.file_path) {
|
| 91 |
+
throw new Error('Некорректные данные STS токена');
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Загружаем ali-oss библиотеку динамически
|
| 95 |
+
const OSS = (await import('ali-oss')).default;
|
| 96 |
+
|
| 97 |
+
// Проверяем наличие необходимых данных для OSS
|
| 98 |
+
if (!stsData.access_key_id || !stsData.access_key_secret || !stsData.security_token ||
|
| 99 |
+
!stsData.region || !stsData.bucketname) {
|
| 100 |
+
throw new Error('Неполные данные STS токена для OSS');
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
console.log(`Создание OSS клиента: регион ${stsData.region}, бакет ${stsData.bucketname}`);
|
| 104 |
+
|
| 105 |
+
// Создаем клиент OSS с STS токеном
|
| 106 |
+
const client = new OSS({
|
| 107 |
+
region: stsData.region,
|
| 108 |
+
accessKeyId: stsData.access_key_id,
|
| 109 |
+
accessKeySecret: stsData.access_key_secret,
|
| 110 |
+
stsToken: stsData.security_token,
|
| 111 |
+
bucket: stsData.bucketname,
|
| 112 |
+
secure: true, // Используем HTTPS
|
| 113 |
+
timeout: 60000 // 60 секунд таймаут
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// Получаем имя объекта из file_path
|
| 117 |
+
const objectName = stsData.file_path;
|
| 118 |
+
|
| 119 |
+
console.log(`Загрузка файла в OSS: ${objectName}`);
|
| 120 |
+
|
| 121 |
+
// Загружаем файл
|
| 122 |
+
const result = await client.put(objectName, filePath);
|
| 123 |
+
|
| 124 |
+
console.log('Файл успешно загружен в OSS:');
|
| 125 |
+
console.log(`URL: ${stsData.file_url}`);
|
| 126 |
+
console.log(`Ответ OSS: ${JSON.stringify(result)}`);
|
| 127 |
+
|
| 128 |
+
// Проверяем, что файл действительно загружен
|
| 129 |
+
try {
|
| 130 |
+
const verifyResponse = await axios.get(stsData.file_url);
|
| 131 |
+
console.log(`Файл успешно проверен, статус: ${verifyResponse.status}`);
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.log(`Не удалось проверить файл: ${error.message}`);
|
| 134 |
+
// Это не критическая ошибка, так как файл может быть недоступен сразу
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
success: true,
|
| 139 |
+
fileName: path.basename(filePath),
|
| 140 |
+
url: stsData.file_url,
|
| 141 |
+
fileId: stsData.file_id,
|
| 142 |
+
ossResponse: result
|
| 143 |
+
};
|
| 144 |
+
} catch (error) {
|
| 145 |
+
console.error('Ошибка при загрузке файла в OSS:');
|
| 146 |
+
if (error.response) {
|
| 147 |
+
console.error(`Статус: ${error.response.status}`);
|
| 148 |
+
console.error(error.response.data);
|
| 149 |
+
} else {
|
| 150 |
+
console.error(error.message);
|
| 151 |
+
}
|
| 152 |
+
throw error;
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// Основная функция для запуска тестов
|
| 157 |
+
async function runTest() {
|
| 158 |
+
try {
|
| 159 |
+
// Путь к тестовому файлу (например, изображение)
|
| 160 |
+
const testFilePath = path.join(__dirname, 'test-image.jpg');
|
| 161 |
+
|
| 162 |
+
// Если файл не существует, создадим простой текстовый файл для теста
|
| 163 |
+
if (!fs.existsSync(testFilePath)) {
|
| 164 |
+
console.log('Тестовый файл не найден, создаем текстовый файл для теста...');
|
| 165 |
+
|
| 166 |
+
const textFilePath = path.join(__dirname, 'test-file.txt');
|
| 167 |
+
fs.writeFileSync(textFilePath, 'Это тестовый файл для загрузки.');
|
| 168 |
+
|
| 169 |
+
console.log(`Создан тестовый файл: ${textFilePath}`);
|
| 170 |
+
|
| 171 |
+
// Тестируем получение STS токена
|
| 172 |
+
const fileInfo = {
|
| 173 |
+
filename: 'test-file.txt',
|
| 174 |
+
filesize: fs.statSync(textFilePath).size,
|
| 175 |
+
filetype: 'file'
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
const stsData = await getTestStsToken(fileInfo);
|
| 179 |
+
|
| 180 |
+
// Тестируем прямую загрузку файла
|
| 181 |
+
console.log('\n--- Тестирование прямой загрузки файла ---');
|
| 182 |
+
await directUploadFile(textFilePath, stsData);
|
| 183 |
+
|
| 184 |
+
// Тестируем загрузку через API
|
| 185 |
+
console.log('\n--- Тестирование загрузки через API ---');
|
| 186 |
+
await uploadTestFile(textFilePath);
|
| 187 |
+
} else {
|
| 188 |
+
// Тестируем получение STS токена
|
| 189 |
+
const fileInfo = {
|
| 190 |
+
filename: 'test-image.jpg',
|
| 191 |
+
filesize: fs.statSync(testFilePath).size,
|
| 192 |
+
filetype: 'image'
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
const stsData = await getTestStsToken(fileInfo);
|
| 196 |
+
|
| 197 |
+
// Тестируем прямую загрузку файла
|
| 198 |
+
console.log('\n--- Тестирование прямой загрузки файла ---');
|
| 199 |
+
await directUploadFile(testFilePath, stsData);
|
| 200 |
+
|
| 201 |
+
// Тестируем загрузку через API
|
| 202 |
+
console.log('\n--- Тестирование загрузки через API ---');
|
| 203 |
+
await uploadTestFile(testFilePath);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
console.log('\nТестирование завершено успешно!');
|
| 207 |
+
} catch (error) {
|
| 208 |
+
console.error('Ошибка при выполнении теста:', error.message);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// Запускаем тест
|
| 213 |
+
runTest();
|
examples/openai-sdk/conversation.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример использования OpenAI SDK для диалога с несколькими сообщениями
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
const openai = new OpenAI({
|
| 7 |
+
baseURL: 'http://localhost:3264/api',
|
| 8 |
+
apiKey: 'dummy-key', // Ключ не используется, но требуется для SDK
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
async function conversationExample() {
|
| 12 |
+
try {
|
| 13 |
+
console.log('Начинаем диалог с Qwen AI...\n');
|
| 14 |
+
|
| 15 |
+
// Первое сообщение пользователя
|
| 16 |
+
console.log('Пользователь: Привет! Расскажи о квантовой физике простыми словами.');
|
| 17 |
+
|
| 18 |
+
let completion = await openai.chat.completions.create({
|
| 19 |
+
messages: [
|
| 20 |
+
{ role: 'user', content: 'Привет! Расскажи о квантовой физике простыми словами.' }
|
| 21 |
+
],
|
| 22 |
+
model: 'qwen-max-latest',
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
const assistantResponse1 = completion.choices[0].message.content;
|
| 26 |
+
console.log('\nQwen:', assistantResponse1);
|
| 27 |
+
|
| 28 |
+
// Второе сообщение пользователя, включающее историю беседы
|
| 29 |
+
console.log('\nПользователь: А как это связано с теорией относительности?');
|
| 30 |
+
|
| 31 |
+
completion = await openai.chat.completions.create({
|
| 32 |
+
messages: [
|
| 33 |
+
{ role: 'user', content: 'Привет! Расскажи о квантовой физике простыми словами.' },
|
| 34 |
+
{ role: 'assistant', content: assistantResponse1 },
|
| 35 |
+
{ role: 'user', content: 'А как это связано с теорией относительности?' }
|
| 36 |
+
],
|
| 37 |
+
model: 'qwen-max-latest',
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const assistantResponse2 = completion.choices[0].message.content;
|
| 41 |
+
console.log('\nQwen:', assistantResponse2);
|
| 42 |
+
|
| 43 |
+
// Третье сообщение пользователя
|
| 44 |
+
console.log('\nПользователь: Спасибо! Кто из ученых внес наибольший вклад в развитие этих теорий?');
|
| 45 |
+
|
| 46 |
+
completion = await openai.chat.completions.create({
|
| 47 |
+
messages: [
|
| 48 |
+
{ role: 'user', content: 'Привет! Расскажи о квантовой физике простыми словами.' },
|
| 49 |
+
{ role: 'assistant', content: assistantResponse1 },
|
| 50 |
+
{ role: 'user', content: 'А как это связано с теорией относительности?' },
|
| 51 |
+
{ role: 'assistant', content: assistantResponse2 },
|
| 52 |
+
{ role: 'user', content: 'Спасибо! Кто из ученых внес наибольший вклад в развитие этих теорий?' }
|
| 53 |
+
],
|
| 54 |
+
model: 'qwen-max-latest',
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
console.log('\nQwen:', completion.choices[0].message.content);
|
| 58 |
+
console.log('\nДиалог успешно завершен.');
|
| 59 |
+
|
| 60 |
+
} catch (error) {
|
| 61 |
+
console.error('Ошибка при выполнении диалога:', error);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Запуск
|
| 66 |
+
conversationExample();
|
examples/openai-sdk/image-analysis.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример использования OpenAI SDK для анализа изображения
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
const openai = new OpenAI({
|
| 7 |
+
baseURL: 'http://localhost:3264/api',
|
| 8 |
+
apiKey: 'dummy-key', // Ключ не используется, но требуется для SDK
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
// ВАЖНО: Замените URL_ИЗОБРАЖЕНИЯ на реальный URL изображения, полученный из интерфейса Qwen
|
| 12 |
+
// Инструкция по получению URL в README.md, раздел "Получение URL изображения из интерфейса Qwen"
|
| 13 |
+
const IMAGE_URL = "https://cdn.qwenlm.ai/bf6238a3-4578-49d6-b4a9-516e8a5eb27b/c88bc915-6ae7-4057-9bf9-1185c9141a0a_image.png?key=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZXNvdXJjZV91c2VyX2lkIjoiYmY2MjM4YTMtNDU3OC00OWQ2LWI0YTktNTE2ZThhNWViMjdiIiwicmVzb3VyY2VfaWQiOiJjODhiYzkxNS02YWU3LTQwNTctOWJmOS0xMTg1YzkxNDFhMGEiLCJyZXNvdXJjZV9jaGF0X2lkIjpudWxsfQ.qPvHr4fq23IgzxmxOyFJuFcVL0AJlpGgPlWB8BHkrlo";
|
| 14 |
+
|
| 15 |
+
async function analyzeImage() {
|
| 16 |
+
try {
|
| 17 |
+
console.log('Отправка запроса с изображением к Qwen AI...\n');
|
| 18 |
+
|
| 19 |
+
const completion = await openai.chat.completions.create({
|
| 20 |
+
messages: [
|
| 21 |
+
{
|
| 22 |
+
role: 'user',
|
| 23 |
+
content: [
|
| 24 |
+
{
|
| 25 |
+
type: 'text',
|
| 26 |
+
text: 'Опиши подробно, что изображено на этой картинке'
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
type: 'image',
|
| 30 |
+
image: IMAGE_URL
|
| 31 |
+
}
|
| 32 |
+
]
|
| 33 |
+
}
|
| 34 |
+
],
|
| 35 |
+
model: 'qwen3-235b-a22b', // Используем модель с поддержкой изображений
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
console.log('Ответ от Qwen:\n');
|
| 39 |
+
console.log(completion.choices[0].message.content);
|
| 40 |
+
console.log('\nАнализ изображения успешно выполнен.');
|
| 41 |
+
|
| 42 |
+
} catch (error) {
|
| 43 |
+
console.error('Ошибка при выполнении запроса с изображением (Убедитесь, что размер изображения не превышает 10MB):', error);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Запуск
|
| 48 |
+
analyzeImage();
|
examples/openai-sdk/openai-compatibility.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример, демонстрирующий совместимость с OpenAI API
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
// Настройка клиента OpenAI с использованием нашего прокси как точки доступа
|
| 7 |
+
const openai = new OpenAI({
|
| 8 |
+
baseURL: 'http://localhost:3264/api',
|
| 9 |
+
apiKey: 'dummy-key', // Ключ не используется, но требуется для SDK
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
async function openaiCompatibilityExample() {
|
| 13 |
+
try {
|
| 14 |
+
console.log('Демонстрация совместимости с OpenAI API\n');
|
| 15 |
+
|
| 16 |
+
// 1. Стандартный запрос в формате OpenAI
|
| 17 |
+
console.log('1. Стандартный запрос в формате OpenAI...');
|
| 18 |
+
|
| 19 |
+
const completion = await openai.chat.completions.create({
|
| 20 |
+
model: 'qwen-max-latest',
|
| 21 |
+
messages: [
|
| 22 |
+
{ role: 'system', content: 'Ты полезный ассистент, который дает краткие и четкие ответы.' },
|
| 23 |
+
{ role: 'user', content: 'Что такое искусственный интеллект?' }
|
| 24 |
+
],
|
| 25 |
+
temperature: 0.7,
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
console.log('Ответ:');
|
| 29 |
+
console.log(completion.choices[0].message.content);
|
| 30 |
+
|
| 31 |
+
// 2. Потоковый запрос в формате OpenAI
|
| 32 |
+
console.log('\n2. Потоковый запрос в формате OpenAI...');
|
| 33 |
+
|
| 34 |
+
console.log('Ответ (потоковый режим):');
|
| 35 |
+
const stream = await openai.chat.completions.create({
|
| 36 |
+
model: 'qwen-max-latest',
|
| 37 |
+
messages: [
|
| 38 |
+
{ role: 'system', content: 'Ты полезный ассистент, который отвечает кратко.' },
|
| 39 |
+
{ role: 'user', content: 'Перечисли 5 самых популярных языков программирования' }
|
| 40 |
+
],
|
| 41 |
+
stream: true,
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
let streamedContent = '';
|
| 45 |
+
for await (const chunk of stream) {
|
| 46 |
+
const content = chunk.choices[0]?.delta?.content || '';
|
| 47 |
+
streamedContent += content;
|
| 48 |
+
process.stdout.write(content);
|
| 49 |
+
}
|
| 50 |
+
console.log('\n');
|
| 51 |
+
|
| 52 |
+
// 3. Демонстрация структуры ответа в формате OpenAI
|
| 53 |
+
console.log('\n3. Структура ответа в формате OpenAI:');
|
| 54 |
+
|
| 55 |
+
const responseDemo = await openai.chat.completions.create({
|
| 56 |
+
model: 'qwen-max-latest',
|
| 57 |
+
messages: [{ role: 'user', content: 'Привет!' }],
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
// Выводим структуру ответа (без содержимого сообщения)
|
| 61 |
+
const { choices, ...responseWithoutChoices } = responseDemo;
|
| 62 |
+
console.log(JSON.stringify({
|
| 63 |
+
...responseWithoutChoices,
|
| 64 |
+
choices: [{
|
| 65 |
+
...choices[0],
|
| 66 |
+
message: { role: choices[0].message.role, content: '[содержимое сообщения скрыто для краткости]' }
|
| 67 |
+
}]
|
| 68 |
+
}, null, 2));
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
} catch (error) {
|
| 73 |
+
console.error('Ошибка при выполнении примера:', error);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Запуск
|
| 78 |
+
openaiCompatibilityExample();
|
examples/openai-sdk/simple.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример использования OpenAI SDK с прокси для Qwen AI - обычный запрос
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
const openai = new OpenAI({
|
| 7 |
+
baseURL: 'http://localhost:3264/api',
|
| 8 |
+
apiKey: 'dummy-key', // Ключ не используется, но требуется для SDK
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
async function simpleRequest() {
|
| 12 |
+
try {
|
| 13 |
+
console.log('Отправка запроса к Qwen AI...\n');
|
| 14 |
+
|
| 15 |
+
const completion = await openai.chat.completions.create({
|
| 16 |
+
messages: [
|
| 17 |
+
{ role: 'user', content: 'Напиши 5 интересных фактов о космосе' }
|
| 18 |
+
],
|
| 19 |
+
model: 'qwen-max-latest',
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
console.log('Ответ от Qwen:\n');
|
| 23 |
+
console.log(completion.choices[0].message.content);
|
| 24 |
+
console.log('\nЗапрос успешно выполнен.');
|
| 25 |
+
|
| 26 |
+
} catch (error) {
|
| 27 |
+
console.error('Ошибка при выполнении запроса:', error);
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Запуск
|
| 32 |
+
simpleRequest();
|
examples/openai-sdk/streaming.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример использования OpenAI SDK с прокси для Qwen AI в потоковом режиме
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
const openai = new OpenAI({
|
| 7 |
+
baseURL: 'http://localhost:3264/api',
|
| 8 |
+
apiKey: 'dummy-key',
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
async function streamFromQwen() {
|
| 12 |
+
try {
|
| 13 |
+
console.log('Отправка потокового запроса к Qwen AI...\n');
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
const stream = await openai.chat.completions.create({
|
| 17 |
+
messages: [
|
| 18 |
+
{ role: 'user', content: 'Напиши небольшую историю о космических путешествиях' }
|
| 19 |
+
],
|
| 20 |
+
model: 'qwen-max-latest',
|
| 21 |
+
stream: true,
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
console.log('Ответ от Qwen (потоковый режим):\n');
|
| 25 |
+
|
| 26 |
+
for await (const chunk of stream) {
|
| 27 |
+
const content = chunk.choices[0]?.delta?.content || '';
|
| 28 |
+
process.stdout.write(content);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
console.log('\n\nПотоковый ответ завершен.');
|
| 32 |
+
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Ошибка при выполнении потокового запроса:', error);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Запуск
|
| 39 |
+
streamFromQwen();
|
examples/openai-sdk/system-message.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Пример использования OpenAI SDK с системным сообщением
|
| 2 |
+
// Установка: npm install openai
|
| 3 |
+
|
| 4 |
+
import OpenAI from 'openai';
|
| 5 |
+
|
| 6 |
+
const openai = new OpenAI({
|
| 7 |
+
baseURL: 'http://localhost:3264/api',
|
| 8 |
+
apiKey: 'dummy-key', // Ключ не используется, но требуется для SDK
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
async function systemMessageExample() {
|
| 12 |
+
try {
|
| 13 |
+
console.log('Отправка запроса с системным сообщением к Qwen AI...\n');
|
| 14 |
+
|
| 15 |
+
const completion = await openai.chat.completions.create({
|
| 16 |
+
messages: [
|
| 17 |
+
{
|
| 18 |
+
role: 'system',
|
| 19 |
+
content: 'Ты опытный астроном, который специализируется на планетах Солнечной системы. Отвечай научно точно, но понятным языком.'
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
role: 'user',
|
| 23 |
+
content: 'Расскажи мне о Марсе и его особенностях'
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
model: 'qwen-max-latest',
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
console.log('Ответ от Qwen:\n');
|
| 30 |
+
console.log(completion.choices[0].message.content);
|
| 31 |
+
console.log('\nЗапрос с системным сообщением успешно выполнен.');
|
| 32 |
+
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Ошибка при выполнении запроса:', error);
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Запуск
|
| 39 |
+
systemMessageExample();
|
index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import bodyParser from 'body-parser';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
+
import path from 'path';
|
| 5 |
+
import readline from 'readline';
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
import { initBrowser, shutdownBrowser } from './src/browser/browser.js';
|
| 9 |
+
import apiRoutes from './src/api/routes.js';
|
| 10 |
+
import { getAvailableModelsFromFile, getApiKeys } from './src/api/chat.js';
|
| 11 |
+
import { loadTokens } from './src/api/tokenManager.js';
|
| 12 |
+
import { addAccountInteractive } from './src/utils/accountSetup.js';
|
| 13 |
+
import { logHttpRequest, logInfo, logError, logWarn } from './src/logger/index.js';
|
| 14 |
+
|
| 15 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 16 |
+
const __dirname = path.dirname(__filename);
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
const app = express();
|
| 20 |
+
|
| 21 |
+
const DEFAULT_PORT = 3264;
|
| 22 |
+
const port = Number.parseInt(process.env.PORT ?? DEFAULT_PORT, 10);
|
| 23 |
+
const host = process.env.HOST || '0.0.0.0';
|
| 24 |
+
|
| 25 |
+
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
| 26 |
+
throw new Error(`Некорректное значение переменной PORT: ${process.env.PORT}`);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const skipAccountMenu = toBoolean(process.env.SKIP_ACCOUNT_MENU) || toBoolean(process.env.NON_INTERACTIVE);
|
| 30 |
+
|
| 31 |
+
function prompt(question) {
|
| 32 |
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
| 33 |
+
return new Promise(res => rl.question(question, ans => { rl.close(); res(ans.trim()); }));
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
function toBoolean(value) {
|
| 37 |
+
if (typeof value !== 'string') return false;
|
| 38 |
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function ensureNonInteractiveTokens() {
|
| 42 |
+
const tokens = loadTokens();
|
| 43 |
+
if (!tokens.length) {
|
| 44 |
+
logError('Не найдено ни одного аккаунта. Запустите скрипт авторизации перед запуском сервера.');
|
| 45 |
+
process.exit(1);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const now = Date.now();
|
| 49 |
+
const validTokens = tokens.filter(t => (!t.resetAt || new Date(t.resetAt).getTime() <= now) && !t.invalid);
|
| 50 |
+
|
| 51 |
+
if (!validTokens.length) {
|
| 52 |
+
logError('Все аккаунты недоступны. Перезапустите авторизацию перед запуском сервера.');
|
| 53 |
+
process.exit(1);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
logInfo(`Автоматический запуск: обнаружено ${tokens.length} аккаунтов, из них ${validTokens.length} активны.`);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Middleware для логирования HTTP-запросов
|
| 60 |
+
app.use(logHttpRequest);
|
| 61 |
+
|
| 62 |
+
app.use(bodyParser.json({ limit: '150mb' }));
|
| 63 |
+
app.use(bodyParser.urlencoded({ limit: '150mb', extended: true }));
|
| 64 |
+
|
| 65 |
+
app.use((req, res, next) => {
|
| 66 |
+
res.header('Access-Control-Allow-Origin', '*');
|
| 67 |
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
| 68 |
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
| 69 |
+
|
| 70 |
+
if (req.method === 'OPTIONS') {
|
| 71 |
+
return res.sendStatus(200);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
next();
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
app.use('/api', apiRoutes);
|
| 78 |
+
|
| 79 |
+
// Обработчик 404
|
| 80 |
+
app.use((req, res) => {
|
| 81 |
+
logWarn(`404 Not Found: ${req.method} ${req.originalUrl}`);
|
| 82 |
+
res.status(404).json({ error: 'Эндпоинт не найден' });
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
// Обработчик ошибок
|
| 86 |
+
app.use((err, req, res, next) => {
|
| 87 |
+
logError('Внутренняя ошибка сервера', err);
|
| 88 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
process.on('SIGINT', handleShutdown);
|
| 92 |
+
process.on('SIGTERM', handleShutdown);
|
| 93 |
+
process.on('SIGHUP', handleShutdown);
|
| 94 |
+
|
| 95 |
+
process.on('uncaughtException', async (error) => {
|
| 96 |
+
logError('Необработанное исключение', error);
|
| 97 |
+
await handleShutdown();
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
async function handleShutdown() {
|
| 101 |
+
logInfo('\nПолучен сигнал завершения. Закрываем браузер...');
|
| 102 |
+
await shutdownBrowser();
|
| 103 |
+
logInfo('Завершение работы.');
|
| 104 |
+
|
| 105 |
+
process.exit(0);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
async function startServer() {
|
| 109 |
+
console.log(`
|
| 110 |
+
███████ ██████ ███████ ███████ ██████ ██ ██ ███████ ███ ██ █████ ██████ ██
|
| 111 |
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██
|
| 112 |
+
█████ ██████ █████ █████ ██ ██ ██ █ ██ █████ ██ ██ ██ ███████ ██████ ██
|
| 113 |
+
██ ██ ██ ██ ██ ██ ▄▄ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
| 114 |
+
██ ██ ██ ███████ ███████ ██████ ███ ███ ███████ ██ ████ ██ ██ ██ ██
|
| 115 |
+
▀▀
|
| 116 |
+
API-прокси для Qwen
|
| 117 |
+
`);
|
| 118 |
+
|
| 119 |
+
logInfo('Запуск сервера...');
|
| 120 |
+
|
| 121 |
+
if (!skipAccountMenu) {
|
| 122 |
+
// Меню управления аккаунтами перед запуском прокси
|
| 123 |
+
while (true) {
|
| 124 |
+
const tokens = loadTokens();
|
| 125 |
+
console.log('\nСписок аккаунтов:');
|
| 126 |
+
if (!tokens.length) {
|
| 127 |
+
console.log(' (пусто)');
|
| 128 |
+
} else {
|
| 129 |
+
tokens.forEach((token, i) => {
|
| 130 |
+
const now = Date.now();
|
| 131 |
+
const isInvalid = token.invalid === true;
|
| 132 |
+
const isWaiting = Boolean(token.resetAt && new Date(token.resetAt).getTime() > now);
|
| 133 |
+
const statusCode = isInvalid ? 0 : isWaiting ? 1 : 2;
|
| 134 |
+
const statusLabel = isInvalid ? '❌ Недействителен' : isWaiting ? '⏳ Ожидание сброса' : '✅ OK';
|
| 135 |
+
console.log(`${String(i + 1).padStart(2, ' ')} | ${token.id} | ${statusLabel} (${statusCode})`);
|
| 136 |
+
});
|
| 137 |
+
}
|
| 138 |
+
console.log('\n=== Меню ===');
|
| 139 |
+
console.log('1 - Добавить новый аккаунт');
|
| 140 |
+
console.log('2 - Перелогинить аккаунт с истекшим токеном');
|
| 141 |
+
console.log('3 - Запустить прокси (по умолчанию)');
|
| 142 |
+
console.log('4 - Удалить аккаунт');
|
| 143 |
+
let choice = await prompt('Ваш выбор (Enter = 3): ');
|
| 144 |
+
if (!choice) choice = '3';
|
| 145 |
+
if (choice === '1') {
|
| 146 |
+
await addAccountInteractive();
|
| 147 |
+
} else if (choice === '2') {
|
| 148 |
+
const { reloginAccountInteractive } = await import('./src/utils/accountSetup.js');
|
| 149 |
+
await reloginAccountInteractive();
|
| 150 |
+
} else if (choice === '3') {
|
| 151 |
+
const hasValidToken = tokens.some(token => {
|
| 152 |
+
if (token.invalid) return false;
|
| 153 |
+
if (!token.resetAt) return true;
|
| 154 |
+
return new Date(token.resetAt).getTime() <= Date.now();
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
if (!tokens.length || !hasValidToken) {
|
| 158 |
+
console.log('Нужен хотя бы один валидный аккаунт для запуска.');
|
| 159 |
+
continue;
|
| 160 |
+
}
|
| 161 |
+
break;
|
| 162 |
+
} else if (choice === '4') {
|
| 163 |
+
const { removeAccountInteractive } = await import('./src/utils/accountSetup.js');
|
| 164 |
+
await removeAccountInteractive();
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
} else {
|
| 168 |
+
ensureNonInteractiveTokens();
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
//=====================================================================================================
|
| 172 |
+
//const sim = await prompt('Смоделировать ошибку RateLimited для первого запроса? (y/N): ');
|
| 173 |
+
//if (sim.toLowerCase() === 'y') {
|
| 174 |
+
// global.simulateRateLimit = true;
|
| 175 |
+
// }
|
| 176 |
+
//=====================================================================================================
|
| 177 |
+
|
| 178 |
+
const browserInitialized = await initBrowser(false);
|
| 179 |
+
if (!browserInitialized) {
|
| 180 |
+
logError('Не удалось инициализировать браузер. Завершение работы.');
|
| 181 |
+
process.exit(1);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
try {
|
| 185 |
+
app.listen(port, host, () => {
|
| 186 |
+
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
| 187 |
+
logInfo(`Сервер запущен на ${host}:${port}`);
|
| 188 |
+
logInfo(`API доступен по адресу: http://${displayHost}:${port}/api`);
|
| 189 |
+
logInfo('Для проверки статуса авторизации: GET /api/status');
|
| 190 |
+
logInfo('Для отправки сообщения: POST /api/chat');
|
| 191 |
+
logInfo('Для получения списка моделей: GET /api/models');
|
| 192 |
+
logInfo('======================================================');
|
| 193 |
+
logInfo('API v2 - История чатов хранится на серверах Qwen');
|
| 194 |
+
logInfo('Создать новый чат: POST /api/chats');
|
| 195 |
+
logInfo('Отправить сообщение: POST /api/chat (с chatId и parentId)');
|
| 196 |
+
logInfo('======================================================');
|
| 197 |
+
logInfo('Доступно 19 моделей Qwen (через систему маппинга):');
|
| 198 |
+
logInfo('- Стандартные: qwen-max, qwen-plus, qwen-turbo и их latest-версии');
|
| 199 |
+
logInfo('- Coder: qwen3-coder-plus, qwen2.5-coder-*b-instruct (0.5b - 32b)');
|
| 200 |
+
logInfo('- Визуальные: qwen-vl-max, qwen-vl-plus и их latest-версии');
|
| 201 |
+
logInfo('- Qwen 3: qwen3, qwen3-max, qwen3-plus, qwen3-omni-flash');
|
| 202 |
+
logInfo('======================================================');
|
| 203 |
+
logInfo('Формат JSON запроса на чат:');
|
| 204 |
+
logInfo('{ "message": "т��кст сообщения", "model": "название модели (опционально)", "chatId": "ID чата (опционально)", "parentId": "ID родительского сообщения (опционально)" }');
|
| 205 |
+
logInfo('Пример первого запроса: { "message": "Привет, как дела?" }');
|
| 206 |
+
logInfo('Пример второго запроса: { "message": "А что ты умеешь?", "chatId": "полученный_id_чата", "parentId": "полученный_parentId" }');
|
| 207 |
+
logInfo('======================================================');
|
| 208 |
+
logInfo('Поддержка OpenAI совместимого API: POST /api/chat/completions');
|
| 209 |
+
logInfo('В ответе возвращаются chatId и parentId для продолжения диалога');
|
| 210 |
+
logInfo('======================================================');
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
getApiKeys();
|
| 214 |
+
|
| 215 |
+
getAvailableModelsFromFile();
|
| 216 |
+
});
|
| 217 |
+
} catch (err) {
|
| 218 |
+
if (err.code === 'EADDRINUSE') {
|
| 219 |
+
logError(`Порт ${port} уже используется. Возможно, сервер уже запущен.`);
|
| 220 |
+
logError('Завершите работу существующего сервера или используйте другой порт.');
|
| 221 |
+
await shutdownBrowser();
|
| 222 |
+
process.exit(1);
|
| 223 |
+
} else {
|
| 224 |
+
throw err;
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
startServer().catch(async error => {
|
| 230 |
+
logError('Ошибка при запуске сервера:', error);
|
| 231 |
+
await shutdownBrowser();
|
| 232 |
+
|
| 233 |
+
process.exit(1);
|
| 234 |
+
});
|
package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "qwen-api-proxy",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Прокси-сервер для доступа к Qwen API через эмуляцию браузера",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"type": "module",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"start": "node index.js",
|
| 9 |
+
"test": "echo \"Error: no test specified\" && exit 1",
|
| 10 |
+
"example:stream": "node examples/openai-sdk/streaming.js",
|
| 11 |
+
"example:simple": "node examples/openai-sdk/simple.js",
|
| 12 |
+
"example:system": "node examples/openai-sdk/system-message.js",
|
| 13 |
+
"example:image": "node examples/openai-sdk/image-analysis.js",
|
| 14 |
+
"example:conversation": "node examples/openai-sdk/conversation.js",
|
| 15 |
+
"example:compatibility": "node examples/openai-sdk/openai-compatibility.js",
|
| 16 |
+
"example:direct": "node examples/direct-api/fetch-example.js",
|
| 17 |
+
"example:axios": "node examples/direct-api/axios-example.js",
|
| 18 |
+
"example:chat-management": "node examples/direct-api/chat-management.js",
|
| 19 |
+
"example:file-upload": "node examples/file-upload/upload-example.js",
|
| 20 |
+
"auth": "node scripts/auth.js"
|
| 21 |
+
},
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"ali-oss": "^6.23.0",
|
| 24 |
+
"axios": "^1.9.0",
|
| 25 |
+
"body-parser": "^1.20.2",
|
| 26 |
+
"express": "^4.18.2",
|
| 27 |
+
"form-data": "^4.0.2",
|
| 28 |
+
"morgan": "^1.10.0",
|
| 29 |
+
"multer": "^2.0.0",
|
| 30 |
+
"node-fetch": "^3.3.2",
|
| 31 |
+
"openai": "^4.104.0",
|
| 32 |
+
"playwright": "^1.35.0",
|
| 33 |
+
"playwright-extra": "^4.3.6",
|
| 34 |
+
"puppeteer": "^24.31.0",
|
| 35 |
+
"puppeteer-extra": "^3.3.6",
|
| 36 |
+
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
| 37 |
+
"winston": "^3.17.0"
|
| 38 |
+
}
|
| 39 |
+
}
|
patch-cline.bat
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
setlocal EnableDelayedExpansion
|
| 3 |
+
:: Остановим все процессы VSCode
|
| 4 |
+
echo Останавливаю Visual Studio Code...
|
| 5 |
+
taskkill /im "Code.exe" /f >nul 2>&1
|
| 6 |
+
|
| 7 |
+
:: Путь к файлу extension.js — автоматически определяем из профиля пользователя
|
| 8 |
+
set USERPROFILE=%USERPROFILE%
|
| 9 |
+
set EXT_PATH=%USERPROFILE%\.vscode\extensions\saoudrizwan.claude-dev-3.17.8\dist\extension.js
|
| 10 |
+
|
| 11 |
+
:: Проверяем, существует ли файл
|
| 12 |
+
if not exist "%EXT_PATH%" (
|
| 13 |
+
echo Файл не найден: "%EXT_PATH%"
|
| 14 |
+
echo Введите полный путь к файлу extension.js:
|
| 15 |
+
set /p EXT_PATH=
|
| 16 |
+
if not exist "!EXT_PATH!" (
|
| 17 |
+
echo Файл не найден: "!EXT_PATH!"
|
| 18 |
+
pause
|
| 19 |
+
exit /b 1
|
| 20 |
+
)
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
:: Делаем бэкап, если его ещё нет
|
| 24 |
+
if not exist "%EXT_PATH%-" (
|
| 25 |
+
echo Делаю резервную копию...
|
| 26 |
+
copy "%EXT_PATH%" "%EXT_PATH%-" >nul
|
| 27 |
+
) else (
|
| 28 |
+
echo Восстанавливаю из резервной копии...
|
| 29 |
+
copy /Y "%EXT_PATH%-" "%EXT_PATH%" >nul
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
:: Замена строки в файле
|
| 33 |
+
echo Меняю URL API на http://localhost:3264/api
|
| 34 |
+
powershell -Command "(Get-Content '%EXT_PATH%') -replace 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'http://localhost:3264/api' | Set-Content '%EXT_PATH%'"
|
| 35 |
+
powershell -Command "(Get-Content '%EXT_PATH%') -replace 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', 'http://localhost:3264/api' | Set-Content '%EXT_PATH%'"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
:: Определение пути к VS Code
|
| 39 |
+
set VSCODE_PATH=C:\Users\%USERNAME%\AppData\Local\Programs\Microsoft VS Code\Code.exe
|
| 40 |
+
if not exist "%VSCODE_PATH%" (
|
| 41 |
+
set VSCODE_PATH=C:\Program Files\Microsoft VS Code\Code.exe
|
| 42 |
+
if not exist "%VSCODE_PATH%" (
|
| 43 |
+
set VSCODE_PATH=C:\Program Files (x86)\Microsoft VS Code\Code.exe
|
| 44 |
+
if not exist "%VSCODE_PATH%" (
|
| 45 |
+
echo VS Code не найден по стандартным путям.
|
| 46 |
+
echo Введите полный путь к Code.exe:
|
| 47 |
+
set /p VSCODE_PATH=
|
| 48 |
+
)
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
:: Перезапускаем VSCode
|
| 53 |
+
echo Перезапускаю Visual Studio Code...
|
| 54 |
+
start "" "%VSCODE_PATH%"
|
| 55 |
+
|
| 56 |
+
echo Готово!
|
| 57 |
+
pause
|
scripts/addAccount.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Скрипт interactively добавляет новые аккаунты.
|
| 2 |
+
// Запуск: node scripts/addAccount.js
|
| 3 |
+
|
| 4 |
+
import { interactiveAccountMenu } from '../src/utils/accountSetup.js';
|
| 5 |
+
|
| 6 |
+
(async () => {
|
| 7 |
+
await interactiveAccountMenu();
|
| 8 |
+
})();
|
scripts/auth.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
import readline from 'readline';
|
| 4 |
+
|
| 5 |
+
import { loadTokens } from '../src/api/tokenManager.js';
|
| 6 |
+
import { addAccountInteractive, reloginAccountInteractive, removeAccountInteractive } from '../src/utils/accountSetup.js';
|
| 7 |
+
|
| 8 |
+
function prompt(question) {
|
| 9 |
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
| 10 |
+
return new Promise(resolve => rl.question(question, answer => { rl.close(); resolve(answer.trim()); }));
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function printDivider() {
|
| 14 |
+
console.log('======================================================');
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const STATUS_CODES = {
|
| 18 |
+
INVALID: 0,
|
| 19 |
+
WAIT: 1,
|
| 20 |
+
OK: 2
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
function formatStatus(token) {
|
| 24 |
+
const now = Date.now();
|
| 25 |
+
if (token.invalid) {
|
| 26 |
+
return { code: STATUS_CODES.INVALID, label: '❌ Недействителен' };
|
| 27 |
+
}
|
| 28 |
+
if (token.resetAt && new Date(token.resetAt).getTime() > now) {
|
| 29 |
+
return { code: STATUS_CODES.WAIT, label: '⏳ Ожидание сброса' };
|
| 30 |
+
}
|
| 31 |
+
return { code: STATUS_CODES.OK, label: '✅ OK' };
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function printAccounts(tokens) {
|
| 35 |
+
console.log('\nСписок аккаунтов:');
|
| 36 |
+
if (!tokens.length) {
|
| 37 |
+
console.log(' (пусто)');
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
tokens.forEach((token, index) => {
|
| 42 |
+
const status = formatStatus(token);
|
| 43 |
+
console.log(`${String(index + 1).padStart(2, ' ')} | ${token.id} | ${status.label} (${status.code})`);
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function handleList(tokens) {
|
| 48 |
+
printAccounts(tokens);
|
| 49 |
+
const active = tokens.filter(t => formatStatus(t).code === STATUS_CODES.OK);
|
| 50 |
+
console.log(`\nАктивных аккаунтов: ${active.length} из ${tokens.length}`);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
function parseArgs(argv) {
|
| 54 |
+
const args = new Set(argv.slice(2));
|
| 55 |
+
if (args.has('--help') || args.has('-h')) return 'help';
|
| 56 |
+
if (args.has('--list')) return 'list';
|
| 57 |
+
if (args.has('--add')) return 'add';
|
| 58 |
+
if (args.has('--relogin')) return 'relogin';
|
| 59 |
+
if (args.has('--remove')) return 'remove';
|
| 60 |
+
return null;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function printHelp() {
|
| 64 |
+
printDivider();
|
| 65 |
+
console.log('Скрипт управления аккаунтами Qwen');
|
| 66 |
+
printDivider();
|
| 67 |
+
console.log('Опции:');
|
| 68 |
+
console.log(' --list Показать список аккаунтов и статусы');
|
| 69 |
+
console.log(' --add Добавить новый аккаунт');
|
| 70 |
+
console.log(' --relogin Перелогинить аккаунт с истекшим токеном');
|
| 71 |
+
console.log(' --remove Удалить аккаунт');
|
| 72 |
+
console.log('Без опций запускается интерактивное меню.');
|
| 73 |
+
printDivider();
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async function runCliAction(action) {
|
| 77 |
+
if (action === 'help') {
|
| 78 |
+
printHelp();
|
| 79 |
+
return;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (action === 'list') {
|
| 83 |
+
const tokens = loadTokens();
|
| 84 |
+
handleList(tokens);
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (action === 'add') {
|
| 89 |
+
await addAccountInteractive();
|
| 90 |
+
return;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if (action === 'relogin') {
|
| 94 |
+
await reloginAccountInteractive();
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if (action === 'remove') {
|
| 99 |
+
await removeAccountInteractive();
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async function runInteractiveMenu() {
|
| 105 |
+
while (true) {
|
| 106 |
+
const tokens = loadTokens();
|
| 107 |
+
printDivider();
|
| 108 |
+
printAccounts(tokens);
|
| 109 |
+
printDivider();
|
| 110 |
+
console.log('Меню:');
|
| 111 |
+
console.log('1 - Добавить новый аккаунт');
|
| 112 |
+
console.log('2 - Перелогинить аккаунт с истекшим токеном');
|
| 113 |
+
console.log('3 - Удалить аккаунт');
|
| 114 |
+
console.log('4 - Показать список и статусы');
|
| 115 |
+
console.log('5 - Выход');
|
| 116 |
+
const choice = await prompt('Ваш выбор (Enter = 5): ');
|
| 117 |
+
const normalized = choice || '5';
|
| 118 |
+
|
| 119 |
+
if (normalized === '1') {
|
| 120 |
+
await addAccountInteractive();
|
| 121 |
+
} else if (normalized === '2') {
|
| 122 |
+
await reloginAccountInteractive();
|
| 123 |
+
} else if (normalized === '3') {
|
| 124 |
+
await removeAccountInteractive();
|
| 125 |
+
} else if (normalized === '4') {
|
| 126 |
+
handleList(tokens);
|
| 127 |
+
await prompt('\nНажмите Enter, чтобы вернуться в меню...');
|
| 128 |
+
} else if (normalized === '5') {
|
| 129 |
+
console.log('Выход из скрипта.');
|
| 130 |
+
break;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
(async () => {
|
| 136 |
+
const action = parseArgs(process.argv);
|
| 137 |
+
if (action) {
|
| 138 |
+
await runCliAction(action);
|
| 139 |
+
return;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
await runInteractiveMenu();
|
| 143 |
+
})();
|
src/AvaibleModels.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
qwen3-max
|
| 2 |
+
qwen3-vl-plus
|
| 3 |
+
qwen3-coder-plus
|
| 4 |
+
qwen3-omni-flash
|
| 5 |
+
qwen-plus-2025-09-11
|
| 6 |
+
qwen3-235b-a22b
|
| 7 |
+
qwen3-30b-a3b
|
| 8 |
+
qwen3-coder-30b-a3b-instruct
|
| 9 |
+
qwen-max-latest
|
| 10 |
+
qwen-plus-2025-01-25
|
| 11 |
+
qwq-32b
|
| 12 |
+
qwen-turbo-2025-02-11
|
| 13 |
+
qwen2.5-omni-7b
|
| 14 |
+
qvq-72b-preview-0310
|
| 15 |
+
qwen2.5-vl-32b-instruct
|
| 16 |
+
qwen2.5-14b-instruct-1m
|
| 17 |
+
qwen2.5-coder-32b-instruct
|
| 18 |
+
qwen2.5-72b-instruct
|
src/api/chat.js
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getBrowserContext, getAuthenticationStatus, setAuthenticationStatus } from '../browser/browser.js';
|
| 2 |
+
import { checkAuthentication } from '../browser/auth.js';
|
| 3 |
+
import { checkVerification } from '../browser/auth.js';
|
| 4 |
+
import { shutdownBrowser, initBrowser } from '../browser/browser.js';
|
| 5 |
+
import { saveAuthToken } from '../browser/session.js';
|
| 6 |
+
import { getAvailableToken, markRateLimited, removeInvalidToken } from './tokenManager.js';
|
| 7 |
+
import fs from 'fs';
|
| 8 |
+
import path from 'path';
|
| 9 |
+
import { fileURLToPath } from 'url';
|
| 10 |
+
import { logRaw } from '../logger/index.js';
|
| 11 |
+
import crypto from 'crypto';
|
| 12 |
+
|
| 13 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 14 |
+
const __dirname = path.dirname(__filename);
|
| 15 |
+
|
| 16 |
+
const CHAT_API_URL_V2 = 'https://chat.qwen.ai/api/v2/chat/completions';
|
| 17 |
+
const CREATE_CHAT_URL = 'https://chat.qwen.ai/api/v2/chats/new';
|
| 18 |
+
const CHAT_PAGE_URL = 'https://chat.qwen.ai/';
|
| 19 |
+
|
| 20 |
+
const MODELS_FILE = path.join(__dirname, '..', 'AvaibleModels.txt');
|
| 21 |
+
const AUTH_KEYS_FILE = path.join(__dirname, '..', 'Authorization.txt');
|
| 22 |
+
|
| 23 |
+
let authToken = null;
|
| 24 |
+
let availableModels = null;
|
| 25 |
+
let authKeys = null;
|
| 26 |
+
|
| 27 |
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
| 28 |
+
|
| 29 |
+
async function getPage(context) {
|
| 30 |
+
if (context && typeof context.goto === 'function') {
|
| 31 |
+
return context;
|
| 32 |
+
} else if (context && typeof context.newPage === 'function') {
|
| 33 |
+
const page = await context.newPage();
|
| 34 |
+
return page;
|
| 35 |
+
} else {
|
| 36 |
+
throw new Error('Неверный контекст: не страница Puppeteer, не контекст Playwright');
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export const pagePool = {
|
| 41 |
+
pages: [],
|
| 42 |
+
maxSize: 3,
|
| 43 |
+
|
| 44 |
+
async getPage(context) {
|
| 45 |
+
if (this.pages.length > 0) {
|
| 46 |
+
return this.pages.pop();
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const newPage = await getPage(context);
|
| 50 |
+
await newPage.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 51 |
+
|
| 52 |
+
if (!authToken) {
|
| 53 |
+
try {
|
| 54 |
+
authToken = await newPage.evaluate(() => localStorage.getItem('token'));
|
| 55 |
+
console.log('Токен авторизации получен из браузера');
|
| 56 |
+
|
| 57 |
+
if (authToken) {
|
| 58 |
+
saveAuthToken(authToken);
|
| 59 |
+
}
|
| 60 |
+
} catch (e) {
|
| 61 |
+
console.error('Ошибка при получении токена авторизации:', e);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return newPage;
|
| 66 |
+
},
|
| 67 |
+
|
| 68 |
+
releasePage(page) {
|
| 69 |
+
if (this.pages.length < this.maxSize) {
|
| 70 |
+
this.pages.push(page);
|
| 71 |
+
} else {
|
| 72 |
+
page.close().catch(e => console.error('Ошибка при закрытии страницы:', e));
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
|
| 76 |
+
async clear() {
|
| 77 |
+
for (const page of this.pages) {
|
| 78 |
+
try {
|
| 79 |
+
await page.close();
|
| 80 |
+
} catch (e) {
|
| 81 |
+
console.error('Ошибка при закрытии страницы в пуле:', e);
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
this.pages = [];
|
| 85 |
+
}
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
export async function extractAuthToken(context, forceRefresh = false) {
|
| 89 |
+
if (authToken && !forceRefresh) {
|
| 90 |
+
return authToken;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
try {
|
| 94 |
+
const page = await getPage(context);
|
| 95 |
+
|
| 96 |
+
try {
|
| 97 |
+
await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 98 |
+
await delay(2000);
|
| 99 |
+
|
| 100 |
+
const newToken = await page.evaluate(() => localStorage.getItem('token'));
|
| 101 |
+
|
| 102 |
+
if (typeof context.newPage === 'function') {
|
| 103 |
+
await page.close();
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
if (newToken) {
|
| 107 |
+
authToken = newToken;
|
| 108 |
+
console.log('Токен авторизации успешно извлечен');
|
| 109 |
+
saveAuthToken(authToken);
|
| 110 |
+
return authToken;
|
| 111 |
+
} else {
|
| 112 |
+
console.error('Токен авторизации не найден в браузере');
|
| 113 |
+
return null;
|
| 114 |
+
}
|
| 115 |
+
} catch (error) {
|
| 116 |
+
if (typeof context.newPage === 'function') {
|
| 117 |
+
await page.close().catch(() => {});
|
| 118 |
+
}
|
| 119 |
+
throw error;
|
| 120 |
+
}
|
| 121 |
+
} catch (error) {
|
| 122 |
+
console.error('Ошибка при извлечении токена авторизации:', error);
|
| 123 |
+
return null;
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export function getAvailableModelsFromFile() {
|
| 128 |
+
try {
|
| 129 |
+
if (!fs.existsSync(MODELS_FILE)) {
|
| 130 |
+
console.error(`Файл с моделями не найден: ${MODELS_FILE}`);
|
| 131 |
+
return ['qwen-max-latest'];
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
const fileContent = fs.readFileSync(MODELS_FILE, 'utf8');
|
| 135 |
+
const models = fileContent.split('\n')
|
| 136 |
+
.map(line => line.trim())
|
| 137 |
+
.filter(line => line && !line.startsWith('#'));
|
| 138 |
+
|
| 139 |
+
console.log('===== ДОСТУПНЫЕ МОДЕЛИ =====');
|
| 140 |
+
models.forEach(model => console.log(`- ${model}`));
|
| 141 |
+
console.log('============================');
|
| 142 |
+
|
| 143 |
+
return models;
|
| 144 |
+
} catch (error) {
|
| 145 |
+
console.error('Ошибка при чтении файла с моделями:', error);
|
| 146 |
+
return ['qwen-max-latest'];
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function getAuthKeysFromFile() {
|
| 151 |
+
try {
|
| 152 |
+
if (!fs.existsSync(AUTH_KEYS_FILE)) {
|
| 153 |
+
const template = `# Файл API-ключей для прокси\n# --------------------------------------------\n# В этом файле перечислены токены, которые\n# прокси будет считать «действительными».\n# Один ключ — одна строка без пробелов.\n#\n# 1) Хотите ОТКЛЮЧИТЬ авторизацию целиком?\n# Оставьте файл пустым — сервер перестанет\n# проверять заголовок Authorization.\n#\n# 2) Хотите разрешить доступ нескольким людям?\n# Впишите каждый ключ в отдельной строке:\n# d35ab3e1-a6f9-4d...\n# f2b1cd9c-1b2e-4a...\n#\n# Пустые строки и строки, начинающиеся с «#»,\n# игнорируются.`;
|
| 154 |
+
try {
|
| 155 |
+
fs.writeFileSync(AUTH_KEYS_FILE, template, { encoding: 'utf8', flag: 'wx' });
|
| 156 |
+
console.log(`Создан шаблон файла ключей: ${AUTH_KEYS_FILE}`);
|
| 157 |
+
} catch (e) {
|
| 158 |
+
console.error('Не удалось создать шаблон Authorization.txt:', e);
|
| 159 |
+
}
|
| 160 |
+
return [];
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
const fileContent = fs.readFileSync(AUTH_KEYS_FILE, 'utf8');
|
| 164 |
+
const keys = fileContent.split('\n')
|
| 165 |
+
.map(line => line.trim())
|
| 166 |
+
.filter(line => line && !line.startsWith('#'));
|
| 167 |
+
|
| 168 |
+
return keys;
|
| 169 |
+
} catch (error) {
|
| 170 |
+
console.error('Ошибка при чтении файла с ключами авторизации:', error);
|
| 171 |
+
return [];
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
export function isValidModel(modelName) {
|
| 176 |
+
if (!availableModels) {
|
| 177 |
+
availableModels = getAvailableModelsFromFile();
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
return availableModels.includes(modelName);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export function getAllModels() {
|
| 184 |
+
if (!availableModels) {
|
| 185 |
+
availableModels = getAvailableModelsFromFile();
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
return {
|
| 189 |
+
models: availableModels.map(model => ({
|
| 190 |
+
id: model,
|
| 191 |
+
name: model,
|
| 192 |
+
description: `Модель ${model}`
|
| 193 |
+
}))
|
| 194 |
+
};
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
export function getApiKeys() {
|
| 198 |
+
if (!authKeys) {
|
| 199 |
+
authKeys = getAuthKeysFromFile();
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
return authKeys;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
export async function sendMessage(message, model = "qwen-max-latest", chatId = null, parentId = null, files = null, tools = null, toolChoice = null, systemMessage = null) {
|
| 206 |
+
|
| 207 |
+
if (!availableModels) {
|
| 208 |
+
availableModels = getAvailableModelsFromFile();
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Создаём новый чат, если не передан
|
| 212 |
+
if (!chatId) {
|
| 213 |
+
const newChatResult = await createChatV2(model);
|
| 214 |
+
if (newChatResult.error) {
|
| 215 |
+
return { error: 'Не удалось создать чат: ' + newChatResult.error };
|
| 216 |
+
}
|
| 217 |
+
chatId = newChatResult.chatId;
|
| 218 |
+
console.log(`Создан новый чат v2 с ID: ${chatId}`);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
// Валидация сообщения
|
| 222 |
+
let messageContent = message;
|
| 223 |
+
try {
|
| 224 |
+
if (message === null || message === undefined) {
|
| 225 |
+
console.error('Сообщение пустое');
|
| 226 |
+
return { error: 'Сообщение не может быть пустым', chatId };
|
| 227 |
+
} else if (typeof message === 'string') {
|
| 228 |
+
messageContent = message;
|
| 229 |
+
} else if (Array.isArray(message)) {
|
| 230 |
+
const isValid = message.every(item =>
|
| 231 |
+
(item.type === 'text' && typeof item.text === 'string') ||
|
| 232 |
+
(item.type === 'image' && typeof item.image === 'string') ||
|
| 233 |
+
(item.type === 'file' && typeof item.file === 'string')
|
| 234 |
+
);
|
| 235 |
+
|
| 236 |
+
if (!isValid) {
|
| 237 |
+
console.error('Некорректная структура составного сообщения');
|
| 238 |
+
return { error: 'Некорректная структура составного сообщения', chatId };
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
messageContent = message;
|
| 242 |
+
} else {
|
| 243 |
+
console.error('Неподдерживаемый формат сообщения:', message);
|
| 244 |
+
return { error: 'Неподдерживаемый формат сообщения', chatId };
|
| 245 |
+
}
|
| 246 |
+
} catch (error) {
|
| 247 |
+
console.error('Ошибка при обработке сообщения:', error);
|
| 248 |
+
return { error: 'Ошибка при обработке сообщения: ' + error.message, chatId };
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
if (!model || model.trim() === "") {
|
| 252 |
+
model = "qwen-max-latest";
|
| 253 |
+
} else {
|
| 254 |
+
if (!isValidModel(model)) {
|
| 255 |
+
console.warn(`Предупреждение: Указанная модель "${model}" не найдена в списке доступных моделей. Используется модель по умолчанию.`);
|
| 256 |
+
model = "qwen-max-latest";
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
console.log(`Используемая модель: "${model}"`);
|
| 261 |
+
|
| 262 |
+
let tokenObj = await getAvailableToken();
|
| 263 |
+
if (tokenObj && tokenObj.token) {
|
| 264 |
+
authToken = tokenObj.token;
|
| 265 |
+
console.log(`Используется аккаунт: ${tokenObj.id}`);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const browserContext = getBrowserContext();
|
| 269 |
+
if (!browserContext) {
|
| 270 |
+
return { error: 'Браузер не инициализирован', chatId };
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if (!getAuthenticationStatus()) {
|
| 274 |
+
console.log('Проверка авторизации...');
|
| 275 |
+
const authCheck = await checkAuthentication(browserContext);
|
| 276 |
+
if (!authCheck) {
|
| 277 |
+
return { error: 'Требуется авторизация. Пожалуйста, авторизуйтесь в открытом браузере.', chatId };
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
if (!authToken) {
|
| 282 |
+
console.log('Получение токена авторизации...');
|
| 283 |
+
authToken = await extractAuthToken(browserContext);
|
| 284 |
+
if (!authToken) {
|
| 285 |
+
console.error('Не удалось получить токен авторизации');
|
| 286 |
+
return { error: 'Ошибка авторизации: не удалось получить токен', chatId };
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
let page = null;
|
| 291 |
+
try {
|
| 292 |
+
page = await pagePool.getPage(browserContext);
|
| 293 |
+
|
| 294 |
+
const verificationNeeded = await checkVerification(page);
|
| 295 |
+
if (verificationNeeded) {
|
| 296 |
+
await page.reload({ waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
if (!authToken) {
|
| 300 |
+
console.error('Токен отсутствует перед отправкой запроса');
|
| 301 |
+
authToken = await page.evaluate(() => localStorage.getItem('token'));
|
| 302 |
+
if (!authToken) {
|
| 303 |
+
return { error: 'Токен авторизации не найден. Требуется перезапуск в ручном режиме.', chatId };
|
| 304 |
+
} else {
|
| 305 |
+
saveAuthToken(authToken);
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
console.log('Отправка запроса к API v2...');
|
| 310 |
+
|
| 311 |
+
// Формируем новое сообщение для v2 API
|
| 312 |
+
const userMessageId = crypto.randomUUID();
|
| 313 |
+
const assistantChildId = crypto.randomUUID();
|
| 314 |
+
|
| 315 |
+
const newMessage = {
|
| 316 |
+
fid: userMessageId,
|
| 317 |
+
parentId: parentId,
|
| 318 |
+
parent_id: parentId,
|
| 319 |
+
role: "user",
|
| 320 |
+
content: messageContent,
|
| 321 |
+
chat_type: "t2t",
|
| 322 |
+
sub_chat_type: "t2t",
|
| 323 |
+
timestamp: Math.floor(Date.now() / 1000),
|
| 324 |
+
user_action: "chat",
|
| 325 |
+
models: [model],
|
| 326 |
+
files: files || [],
|
| 327 |
+
childrenIds: [assistantChildId],
|
| 328 |
+
extra: {
|
| 329 |
+
meta: {
|
| 330 |
+
subChatType: "t2t"
|
| 331 |
+
}
|
| 332 |
+
},
|
| 333 |
+
feature_config: {
|
| 334 |
+
thinking_enabled: false,
|
| 335 |
+
output_schema: "phase"
|
| 336 |
+
}
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
// Формируем payload для v2 API
|
| 340 |
+
const payload = {
|
| 341 |
+
stream: true,
|
| 342 |
+
incremental_output: true,
|
| 343 |
+
chat_id: chatId,
|
| 344 |
+
chat_mode: "normal",
|
| 345 |
+
messages: [newMessage],
|
| 346 |
+
model: model,
|
| 347 |
+
parent_id: parentId,
|
| 348 |
+
timestamp: Math.floor(Date.now() / 1000)
|
| 349 |
+
};
|
| 350 |
+
|
| 351 |
+
// Добавляем system message если есть
|
| 352 |
+
if (systemMessage) {
|
| 353 |
+
payload.system_message = systemMessage;
|
| 354 |
+
console.log(`System message: ${systemMessage.substring(0, 100)}${systemMessage.length > 100 ? '...' : ''}`);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Добавляем tools если есть
|
| 358 |
+
if (tools && Array.isArray(tools) && tools.length > 0) {
|
| 359 |
+
payload.tools = tools;
|
| 360 |
+
payload.tool_choice = toolChoice || "auto";
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
console.log('=== PAYLOAD V2 ===\n' + JSON.stringify(payload, null, 2));
|
| 364 |
+
console.log(`Отправка сообщения в чат ${chatId} с parent_id: ${parentId || 'null'}`);
|
| 365 |
+
|
| 366 |
+
const apiUrl = `${CHAT_API_URL_V2}?chat_id=${chatId}`;
|
| 367 |
+
const evalData = {
|
| 368 |
+
apiUrl: apiUrl,
|
| 369 |
+
payload: payload,
|
| 370 |
+
token: authToken
|
| 371 |
+
};
|
| 372 |
+
|
| 373 |
+
console.log(`Используем токен: ${authToken ? 'Токен существует' : 'Токен отсутствует'}`);
|
| 374 |
+
console.log(`API URL: ${apiUrl}`);
|
| 375 |
+
|
| 376 |
+
// Выполняем запрос через браузер и парсим SSE
|
| 377 |
+
let response = await page.evaluate(async (data) => {
|
| 378 |
+
try {
|
| 379 |
+
const token = data.token;
|
| 380 |
+
if (!token) {
|
| 381 |
+
return { success: false, error: 'Токен авторизации не найден' };
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
const response = await fetch(data.apiUrl, {
|
| 385 |
+
method: 'POST',
|
| 386 |
+
headers: {
|
| 387 |
+
'Content-Type': 'application/json',
|
| 388 |
+
'Authorization': `Bearer ${token}`,
|
| 389 |
+
'Accept': '*/*'
|
| 390 |
+
},
|
| 391 |
+
body: JSON.stringify(data.payload)
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
if (response.ok) {
|
| 395 |
+
const reader = response.body.getReader();
|
| 396 |
+
const decoder = new TextDecoder();
|
| 397 |
+
let buffer = '';
|
| 398 |
+
let fullContent = '';
|
| 399 |
+
let responseId = null;
|
| 400 |
+
let usage = null;
|
| 401 |
+
let finished = false;
|
| 402 |
+
|
| 403 |
+
while (!finished) {
|
| 404 |
+
const { done, value } = await reader.read();
|
| 405 |
+
if (done) break;
|
| 406 |
+
|
| 407 |
+
buffer += decoder.decode(value, { stream: true });
|
| 408 |
+
const lines = buffer.split('\n');
|
| 409 |
+
buffer = lines.pop() || '';
|
| 410 |
+
|
| 411 |
+
for (const line of lines) {
|
| 412 |
+
if (!line.trim() || !line.startsWith('data: ')) continue;
|
| 413 |
+
|
| 414 |
+
const jsonStr = line.substring(6).trim();
|
| 415 |
+
if (!jsonStr) continue;
|
| 416 |
+
|
| 417 |
+
try {
|
| 418 |
+
const chunk = JSON.parse(jsonStr);
|
| 419 |
+
|
| 420 |
+
// Первый чанк с метаданными
|
| 421 |
+
if (chunk['response.created']) {
|
| 422 |
+
responseId = chunk['response.created'].response_id;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// Чанки с контентом
|
| 426 |
+
if (chunk.choices && chunk.choices[0]) {
|
| 427 |
+
const delta = chunk.choices[0].delta;
|
| 428 |
+
if (delta && delta.content) {
|
| 429 |
+
fullContent += delta.content;
|
| 430 |
+
}
|
| 431 |
+
if (delta && delta.status === 'finished') {
|
| 432 |
+
finished = true;
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
// Обновляем usage
|
| 437 |
+
if (chunk.usage) {
|
| 438 |
+
usage = chunk.usage;
|
| 439 |
+
}
|
| 440 |
+
} catch (e) {
|
| 441 |
+
// Игнорируем ошибки парсинга отдельных чанков
|
| 442 |
+
}
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
return {
|
| 447 |
+
success: true,
|
| 448 |
+
data: {
|
| 449 |
+
id: responseId || 'chatcmpl-' + Date.now(),
|
| 450 |
+
object: 'chat.completion',
|
| 451 |
+
created: Math.floor(Date.now() / 1000),
|
| 452 |
+
model: data.payload.model,
|
| 453 |
+
choices: [{
|
| 454 |
+
index: 0,
|
| 455 |
+
message: {
|
| 456 |
+
role: 'assistant',
|
| 457 |
+
content: fullContent
|
| 458 |
+
},
|
| 459 |
+
finish_reason: 'stop'
|
| 460 |
+
}],
|
| 461 |
+
usage: usage || {
|
| 462 |
+
prompt_tokens: 0,
|
| 463 |
+
completion_tokens: 0,
|
| 464 |
+
total_tokens: 0
|
| 465 |
+
},
|
| 466 |
+
response_id: responseId
|
| 467 |
+
}
|
| 468 |
+
};
|
| 469 |
+
} else {
|
| 470 |
+
const errorBody = await response.text();
|
| 471 |
+
return {
|
| 472 |
+
success: false,
|
| 473 |
+
status: response.status,
|
| 474 |
+
statusText: response.statusText,
|
| 475 |
+
errorBody: errorBody
|
| 476 |
+
};
|
| 477 |
+
}
|
| 478 |
+
} catch (error) {
|
| 479 |
+
return { success: false, error: error.toString() };
|
| 480 |
+
}
|
| 481 |
+
}, evalData);
|
| 482 |
+
|
| 483 |
+
// --- TEST: симуляция ответа RateLimited ---
|
| 484 |
+
if (global.simulateRateLimit && !global.__rateLimitedTested) {
|
| 485 |
+
global.__rateLimitedTested = true;
|
| 486 |
+
response = {
|
| 487 |
+
success: false,
|
| 488 |
+
status: 429,
|
| 489 |
+
errorBody: JSON.stringify({
|
| 490 |
+
code: 'RateLimited',
|
| 491 |
+
detail: "You've reached the upper limit for today's usage.",
|
| 492 |
+
template: 'You have reached the daily usage limit. Please wait {{num}} hours before trying again.',
|
| 493 |
+
num: 4
|
| 494 |
+
})
|
| 495 |
+
};
|
| 496 |
+
console.log('*** Симуляция ответа RateLimited активирована ***');
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
pagePool.releasePage(page);
|
| 500 |
+
page = null;
|
| 501 |
+
|
| 502 |
+
if (response.success) {
|
| 503 |
+
// Логируем сырой ответ от модели
|
| 504 |
+
logRaw(JSON.stringify(response.data));
|
| 505 |
+
console.log('Ответ получен успешно');
|
| 506 |
+
|
| 507 |
+
// Добавляем метаданные для клиента
|
| 508 |
+
response.data.chatId = chatId;
|
| 509 |
+
response.data.parentId = response.data.response_id; // Для следующего сообщения
|
| 510 |
+
response.data.id = response.data.id || "chatcmpl-" + Date.now();
|
| 511 |
+
|
| 512 |
+
return response.data;
|
| 513 |
+
} else {
|
| 514 |
+
// Логируем ошибочный сырой ответ
|
| 515 |
+
logRaw(JSON.stringify(response));
|
| 516 |
+
console.error('Ошибка при получении ответа:', response.error || response.statusText);
|
| 517 |
+
|
| 518 |
+
if (response.errorBody) {
|
| 519 |
+
console.error('Тело ответа с ошибкой:', response.errorBody);
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
if (response.html && response.html.includes('Verification')) {
|
| 523 |
+
setAuthenticationStatus(false);
|
| 524 |
+
console.log('Обнаружена необходимость верификации, перезапуск браузера в видимом режиме...');
|
| 525 |
+
|
| 526 |
+
await pagePool.clear();
|
| 527 |
+
|
| 528 |
+
authToken = null;
|
| 529 |
+
|
| 530 |
+
await shutdownBrowser();
|
| 531 |
+
await initBrowser(true);
|
| 532 |
+
|
| 533 |
+
return { error: 'Требуется верификация. Браузер запущен в видимом режиме.', verification: true, chatId };
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
// ----- Новая обработка истекшего токена / 401 Unauthorized -----
|
| 537 |
+
if ((response.status === 401) || (response.errorBody && (response.errorBody.includes('Unauthorized') || response.errorBody.includes('Token has expired')))) {
|
| 538 |
+
console.log('Токен', tokenObj?.id, 'недействителен (401). Удаляем и пробуем другой.');
|
| 539 |
+
|
| 540 |
+
// Удаляем токен из пула
|
| 541 |
+
authToken = null;
|
| 542 |
+
if (tokenObj && tokenObj.id) {
|
| 543 |
+
const { markInvalid } = await import('./tokenManager.js');
|
| 544 |
+
markInvalid(tokenObj.id);
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// Есть ли ещё токены?
|
| 548 |
+
const { hasValidTokens } = await import('./tokenManager.js');
|
| 549 |
+
if (hasValidTokens()) {
|
| 550 |
+
return await sendMessage(message, model, chatId, files); // повторяем с новым токеном
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
console.error('Не осталось валидных токенов. Останавливаю прокси.');
|
| 554 |
+
await pagePool.clear();
|
| 555 |
+
await shutdownBrowser();
|
| 556 |
+
process.exit(1);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
if (response.errorBody && response.errorBody.includes('RateLimited')) {
|
| 560 |
+
try {
|
| 561 |
+
const rateInfo = JSON.parse(response.errorBody);
|
| 562 |
+
const hours = Number(rateInfo.num) || 24;
|
| 563 |
+
if (tokenObj && tokenObj.id) {
|
| 564 |
+
markRateLimited(tokenObj.id, hours);
|
| 565 |
+
console.log(`Токен ${tokenObj.id} достиг лимита. Помечаем на ${hours}ч и пробуем другой токен...`);
|
| 566 |
+
}
|
| 567 |
+
} catch (e) {
|
| 568 |
+
console.error('Не удалось распарсить тело ошибки RateLimited:', e);
|
| 569 |
+
}
|
| 570 |
+
authToken = null;
|
| 571 |
+
return await sendMessage(message, model, chatId, files);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
return { error: response.error || response.statusText, details: response.errorBody || 'Нет дополнительных деталей', chatId };
|
| 575 |
+
}
|
| 576 |
+
} catch (error) {
|
| 577 |
+
console.error('Ошибка при отправке сообщения:', error);
|
| 578 |
+
return { error: error.toString(), chatId };
|
| 579 |
+
} finally {
|
| 580 |
+
if (page) {
|
| 581 |
+
try {
|
| 582 |
+
if (typeof getBrowserContext().newPage === 'function') {
|
| 583 |
+
await page.close();
|
| 584 |
+
}
|
| 585 |
+
} catch (e) {
|
| 586 |
+
console.error('Ошибка при закрытии страницы:', e);
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
export async function clearPagePool() {
|
| 593 |
+
await pagePool.clear();
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
export function getAuthToken() {
|
| 597 |
+
return authToken;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
export async function listModels(browserContext) {
|
| 601 |
+
return await getAvailableModels(browserContext);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
// Создание нового чата через v2 API
|
| 605 |
+
export async function createChatV2(model = "qwen-max-latest", title = "Новый чат") {
|
| 606 |
+
const browserContext = getBrowserContext();
|
| 607 |
+
if (!browserContext) {
|
| 608 |
+
return { error: 'Браузер не инициализирован' };
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
// Получаем токен из tokenManager
|
| 612 |
+
let tokenObj = await getAvailableToken();
|
| 613 |
+
if (tokenObj && tokenObj.token) {
|
| 614 |
+
authToken = tokenObj.token;
|
| 615 |
+
console.log(`Используется аккаунт для создания чата: ${tokenObj.id}`);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
if (!authToken) {
|
| 619 |
+
console.log('Получение токена авторизации для создания чата...');
|
| 620 |
+
authToken = await extractAuthToken(browserContext);
|
| 621 |
+
if (!authToken) {
|
| 622 |
+
return { error: 'Не удалось получить токен авторизации' };
|
| 623 |
+
}
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
let page = null;
|
| 627 |
+
try {
|
| 628 |
+
page = await pagePool.getPage(browserContext);
|
| 629 |
+
|
| 630 |
+
const payload = {
|
| 631 |
+
title: title,
|
| 632 |
+
models: [model],
|
| 633 |
+
chat_mode: "normal",
|
| 634 |
+
chat_type: "t2t",
|
| 635 |
+
timestamp: Date.now()
|
| 636 |
+
};
|
| 637 |
+
|
| 638 |
+
const evalData = {
|
| 639 |
+
apiUrl: CREATE_CHAT_URL,
|
| 640 |
+
payload: payload,
|
| 641 |
+
token: authToken
|
| 642 |
+
};
|
| 643 |
+
|
| 644 |
+
const result = await page.evaluate(async (data) => {
|
| 645 |
+
try {
|
| 646 |
+
const response = await fetch(data.apiUrl, {
|
| 647 |
+
method: 'POST',
|
| 648 |
+
headers: {
|
| 649 |
+
'Content-Type': 'application/json',
|
| 650 |
+
'Authorization': `Bearer ${data.token}`
|
| 651 |
+
},
|
| 652 |
+
body: JSON.stringify(data.payload)
|
| 653 |
+
});
|
| 654 |
+
|
| 655 |
+
if (response.ok) {
|
| 656 |
+
const result = await response.json();
|
| 657 |
+
return { success: true, data: result };
|
| 658 |
+
} else {
|
| 659 |
+
const errorBody = await response.text();
|
| 660 |
+
return {
|
| 661 |
+
success: false,
|
| 662 |
+
status: response.status,
|
| 663 |
+
errorBody: errorBody
|
| 664 |
+
};
|
| 665 |
+
}
|
| 666 |
+
} catch (error) {
|
| 667 |
+
return { success: false, error: error.toString() };
|
| 668 |
+
}
|
| 669 |
+
}, evalData);
|
| 670 |
+
|
| 671 |
+
pagePool.releasePage(page);
|
| 672 |
+
page = null;
|
| 673 |
+
|
| 674 |
+
if (result.success && result.data.success) {
|
| 675 |
+
console.log(`Чат создан: ${result.data.data.id}`);
|
| 676 |
+
return {
|
| 677 |
+
success: true,
|
| 678 |
+
chatId: result.data.data.id,
|
| 679 |
+
requestId: result.data.request_id
|
| 680 |
+
};
|
| 681 |
+
} else {
|
| 682 |
+
console.error('Ошибка при создании чата:', result);
|
| 683 |
+
return { error: result.errorBody || result.error || 'Неизвестная ошибка' };
|
| 684 |
+
}
|
| 685 |
+
} catch (error) {
|
| 686 |
+
console.error('Ошибка при создании чата:', error);
|
| 687 |
+
return { error: error.toString() };
|
| 688 |
+
} finally {
|
| 689 |
+
if (page) {
|
| 690 |
+
try {
|
| 691 |
+
if (typeof getBrowserContext().newPage === 'function') {
|
| 692 |
+
await page.close();
|
| 693 |
+
}
|
| 694 |
+
} catch (e) {
|
| 695 |
+
console.error('Ошибка при закрытии страницы:', e);
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
export async function testToken(token) {
|
| 702 |
+
const browserContext = getBrowserContext();
|
| 703 |
+
if (!browserContext) return 'ERROR';
|
| 704 |
+
|
| 705 |
+
let page;
|
| 706 |
+
try {
|
| 707 |
+
page = await getPage(browserContext);
|
| 708 |
+
await page.goto(CHAT_PAGE_URL, { waitUntil: 'domcontentloaded' });
|
| 709 |
+
|
| 710 |
+
const evalData = {
|
| 711 |
+
apiUrl: CHAT_API_URL_V2,
|
| 712 |
+
token,
|
| 713 |
+
payload: {
|
| 714 |
+
chat_type: 't2t',
|
| 715 |
+
messages: [{ role: 'user', content: 'ping', chat_type: 't2t' }],
|
| 716 |
+
model: 'qwen-max-latest',
|
| 717 |
+
stream: false
|
| 718 |
+
}
|
| 719 |
+
};
|
| 720 |
+
|
| 721 |
+
const result = await page.evaluate(async (data) => {
|
| 722 |
+
try {
|
| 723 |
+
const res = await fetch(data.apiUrl, {
|
| 724 |
+
method: 'POST',
|
| 725 |
+
headers: {
|
| 726 |
+
'Content-Type': 'application/json',
|
| 727 |
+
'Authorization': `Bearer ${data.token}`
|
| 728 |
+
},
|
| 729 |
+
body: JSON.stringify(data.payload)
|
| 730 |
+
});
|
| 731 |
+
return { ok: res.ok, status: res.status };
|
| 732 |
+
} catch (e) {
|
| 733 |
+
return { ok: false, status: 0, error: e.toString() };
|
| 734 |
+
}
|
| 735 |
+
}, evalData);
|
| 736 |
+
|
| 737 |
+
if (result.ok || result.status === 400) return 'OK';
|
| 738 |
+
if (result.status === 401 || result.status === 403) return 'UNAUTHORIZED';
|
| 739 |
+
if (result.status === 429) return 'RATELIMIT';
|
| 740 |
+
return 'ERROR';
|
| 741 |
+
} catch (e) {
|
| 742 |
+
console.error('testToken error:', e);
|
| 743 |
+
return 'ERROR';
|
| 744 |
+
} finally {
|
| 745 |
+
if (page) {
|
| 746 |
+
try {
|
| 747 |
+
if (typeof browserContext.newPage === 'function') {
|
| 748 |
+
await page.close();
|
| 749 |
+
}
|
| 750 |
+
} catch { }
|
| 751 |
+
}
|
| 752 |
+
}
|
| 753 |
+
}
|
src/api/chatHistory.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
+
import crypto from 'crypto';
|
| 5 |
+
import { logInfo, logError, logDebug } from '../logger/index.js';
|
| 6 |
+
|
| 7 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 8 |
+
const __dirname = path.dirname(__filename);
|
| 9 |
+
|
| 10 |
+
const HISTORY_DIR = path.join(__dirname, '..', '..', 'session', 'history');
|
| 11 |
+
|
| 12 |
+
const MAX_HISTORY_LENGTH = 100;
|
| 13 |
+
|
| 14 |
+
export function initHistoryDirectory() {
|
| 15 |
+
if (!fs.existsSync(HISTORY_DIR)) {
|
| 16 |
+
fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
| 17 |
+
logInfo(`Создана директория для истории чатов: ${HISTORY_DIR}`);
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export function generateChatId() {
|
| 22 |
+
return crypto.randomUUID();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export function createChat(chatName) {
|
| 26 |
+
const chatId = generateChatId();
|
| 27 |
+
const chatInfo = {
|
| 28 |
+
id: chatId,
|
| 29 |
+
name: chatName || `Новый чат ${new Date().toLocaleString()}`,
|
| 30 |
+
created: Date.now(),
|
| 31 |
+
messages: []
|
| 32 |
+
};
|
| 33 |
+
saveHistory(chatId, chatInfo);
|
| 34 |
+
logInfo(`Создан новый чат [${chatId}] с именем "${chatInfo.name}"`);
|
| 35 |
+
return chatId;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function getHistoryFilePath(chatId) {
|
| 39 |
+
return path.join(HISTORY_DIR, `${chatId}.json`);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export function saveHistory(chatId, data) {
|
| 43 |
+
try {
|
| 44 |
+
initHistoryDirectory();
|
| 45 |
+
const historyFilePath = getHistoryFilePath(chatId);
|
| 46 |
+
fs.writeFileSync(historyFilePath, JSON.stringify(data, null, 2), 'utf8');
|
| 47 |
+
logDebug(`История чата ${chatId} успешно сохранена`);
|
| 48 |
+
return true;
|
| 49 |
+
} catch (error) {
|
| 50 |
+
logError(`Ошибка при сохранении истории чата ${chatId}`, error);
|
| 51 |
+
return false;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export function loadHistory(chatId) {
|
| 56 |
+
try {
|
| 57 |
+
const historyFilePath = getHistoryFilePath(chatId);
|
| 58 |
+
if (fs.existsSync(historyFilePath)) {
|
| 59 |
+
const rawData = fs.readFileSync(historyFilePath, 'utf8');
|
| 60 |
+
logDebug(`Данные чата ${chatId} успешно загружены`);
|
| 61 |
+
|
| 62 |
+
let data;
|
| 63 |
+
try {
|
| 64 |
+
data = JSON.parse(rawData);
|
| 65 |
+
logDebug(`Данные чата ${chatId} успешно распарсены`);
|
| 66 |
+
} catch (parseErr) {
|
| 67 |
+
logError(`Ошибка при парсинге данных чата ${chatId}`, parseErr);
|
| 68 |
+
return {
|
| 69 |
+
id: chatId,
|
| 70 |
+
name: `Восстановленный чат ${new Date().toLocaleString()}`,
|
| 71 |
+
created: Date.now(),
|
| 72 |
+
messages: []
|
| 73 |
+
};
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Поддержка обратной совместимости со старым форматом
|
| 77 |
+
if (Array.isArray(data)) {
|
| 78 |
+
logDebug(`Чат ${chatId} использует устаревший формат, выполняется конвертация`);
|
| 79 |
+
return {
|
| 80 |
+
id: chatId,
|
| 81 |
+
name: `Чат от ${new Date().toLocaleString()}`,
|
| 82 |
+
created: Date.now(),
|
| 83 |
+
messages: data,
|
| 84 |
+
wasConverted: true
|
| 85 |
+
};
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Проверяем наличие обязательных полей
|
| 89 |
+
if (!data.messages) {
|
| 90 |
+
logInfo(`Чат ${chatId} не содержит сообщений, инициализируем пустой массив`);
|
| 91 |
+
data.messages = [];
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (!data.name) {
|
| 95 |
+
data.name = `Чат ${chatId.substring(0, 6)}`;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
if (!data.created) {
|
| 99 |
+
data.created = Date.now();
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if (!data.id) {
|
| 103 |
+
data.id = chatId;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return data;
|
| 107 |
+
} else {
|
| 108 |
+
logInfo(`Файл истории для чата ${chatId} не найден`);
|
| 109 |
+
}
|
| 110 |
+
} catch (error) {
|
| 111 |
+
logError(`Ошибка при загрузке истории чата ${chatId}`, error);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Если не удалось загрузить, создаем новые данные
|
| 115 |
+
logInfo(`Создаем новую историю для чата ${chatId}`);
|
| 116 |
+
return {
|
| 117 |
+
id: chatId,
|
| 118 |
+
name: `Новый чат ${new Date().toLocaleString()}`,
|
| 119 |
+
created: Date.now(),
|
| 120 |
+
messages: []
|
| 121 |
+
};
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
export function chatExists(chatId) {
|
| 125 |
+
const historyFilePath = getHistoryFilePath(chatId);
|
| 126 |
+
const exists = fs.existsSync(historyFilePath);
|
| 127 |
+
logDebug(`Проверка существования чата ${chatId}: ${exists ? 'найден' : 'не найден'}`);
|
| 128 |
+
return exists;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
export function renameChat(chatId, newName) {
|
| 132 |
+
try {
|
| 133 |
+
if (!chatExists(chatId)) {
|
| 134 |
+
logError(`Попытка переименовать несуществующий чат ${chatId}`);
|
| 135 |
+
return false;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const chatData = loadHistory(chatId);
|
| 139 |
+
const oldName = chatData.name;
|
| 140 |
+
chatData.name = newName;
|
| 141 |
+
const success = saveHistory(chatId, chatData);
|
| 142 |
+
if (success) {
|
| 143 |
+
logInfo(`Чат ${chatId} переименован: "${oldName}" -> "${newName}"`);
|
| 144 |
+
} else {
|
| 145 |
+
logError(`Не удалось переименовать чат ${chatId}`);
|
| 146 |
+
}
|
| 147 |
+
return success;
|
| 148 |
+
} catch (error) {
|
| 149 |
+
logError(`Ошибка при переименовании чата ${chatId}`, error);
|
| 150 |
+
return false;
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
export function addUserMessage(chatId, content) {
|
| 155 |
+
const timestamp = Math.floor(Date.now() / 1000);
|
| 156 |
+
const messageId = crypto.randomUUID();
|
| 157 |
+
|
| 158 |
+
// Определяем тип содержимого и его длину для логирования
|
| 159 |
+
let contentDesc;
|
| 160 |
+
if (Array.isArray(content)) {
|
| 161 |
+
// Составное сообщение (текст + изображения)
|
| 162 |
+
const textParts = content.filter(item => item.type === 'text');
|
| 163 |
+
const imageParts = content.filter(item => item.type === 'image');
|
| 164 |
+
const fileParts = content.filter(item => item.type === 'file');
|
| 165 |
+
|
| 166 |
+
contentDesc = `составное сообщение (${textParts.length} текст., ${imageParts.length} изобр., ${fileParts.length} файл.)`;
|
| 167 |
+
} else if (typeof content === 'object' && content !== null) {
|
| 168 |
+
contentDesc = 'объект-сообщение';
|
| 169 |
+
} else {
|
| 170 |
+
contentDesc = `текст длиной ${String(content).length}`;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const message = {
|
| 174 |
+
id: messageId,
|
| 175 |
+
role: "user",
|
| 176 |
+
content: content,
|
| 177 |
+
timestamp: timestamp,
|
| 178 |
+
chat_type: "t2t"
|
| 179 |
+
};
|
| 180 |
+
|
| 181 |
+
logInfo(`Добавление сообщения пользователя в чат ${chatId}: ${contentDesc}`);
|
| 182 |
+
return addMessageToHistory(chatId, message);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
export function addAssistantMessage(chatId, content, info = {}) {
|
| 186 |
+
const timestamp = Math.floor(Date.now() / 1000);
|
| 187 |
+
const messageId = crypto.randomUUID();
|
| 188 |
+
|
| 189 |
+
const message = {
|
| 190 |
+
id: messageId,
|
| 191 |
+
role: "assistant",
|
| 192 |
+
content: content,
|
| 193 |
+
timestamp: timestamp,
|
| 194 |
+
info: info,
|
| 195 |
+
chat_type: "t2t"
|
| 196 |
+
};
|
| 197 |
+
|
| 198 |
+
logInfo(`Добавление ответа ассистента в чат ${chatId}, длина: ${content.length}`);
|
| 199 |
+
return addMessageToHistory(chatId, message);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function addMessageToHistory(chatId, message) {
|
| 203 |
+
try {
|
| 204 |
+
let chatData = loadHistory(chatId);
|
| 205 |
+
|
| 206 |
+
if (chatData.messages.length >= MAX_HISTORY_LENGTH) {
|
| 207 |
+
logInfo(`Чат ${chatId} достиг максимальной длины (${MAX_HISTORY_LENGTH}), удаляем старые сообщения`);
|
| 208 |
+
chatData.messages = [chatData.messages[0], ...chatData.messages.slice(chatData.messages.length - MAX_HISTORY_LENGTH + 2)];
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
chatData.messages.push(message);
|
| 212 |
+
saveHistory(chatId, chatData);
|
| 213 |
+
logDebug(`Сообщение ${message.id} успешно добавлено в чат ${chatId}`);
|
| 214 |
+
|
| 215 |
+
return message.id;
|
| 216 |
+
} catch (error) {
|
| 217 |
+
logError(`Ошибка при добавлении сообщения в историю чата ${chatId}`, error);
|
| 218 |
+
return null;
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
export function getAllChats() {
|
| 223 |
+
try {
|
| 224 |
+
initHistoryDirectory();
|
| 225 |
+
const files = fs.readdirSync(HISTORY_DIR);
|
| 226 |
+
logDebug(`Получен список файлов чатов: ${files.length} файлов`);
|
| 227 |
+
|
| 228 |
+
let convertedCount = 0;
|
| 229 |
+
const chats = files
|
| 230 |
+
.filter(file => file.endsWith('.json'))
|
| 231 |
+
.map(file => {
|
| 232 |
+
const chatId = file.replace('.json', '');
|
| 233 |
+
const chatData = loadHistory(chatId);
|
| 234 |
+
|
| 235 |
+
if (chatData.wasConverted) {
|
| 236 |
+
convertedCount++;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
return {
|
| 240 |
+
id: chatId,
|
| 241 |
+
name: chatData.name || `Чат ${chatId.substring(0, 6)}`,
|
| 242 |
+
created: chatData.created || 0,
|
| 243 |
+
messageCount: chatData.messages ? chatData.messages.length : 0,
|
| 244 |
+
userMessageCount: chatData.messages ?
|
| 245 |
+
chatData.messages.filter(m => m.role === 'user').length : 0
|
| 246 |
+
};
|
| 247 |
+
});
|
| 248 |
+
|
| 249 |
+
if (convertedCount > 0) {
|
| 250 |
+
logInfo(`Конвертировано ${convertedCount} чатов из устаревшего формата`);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
logInfo(`Обработано ${chats.length} чатов`);
|
| 254 |
+
return chats.sort((a, b) => b.created - a.created);
|
| 255 |
+
} catch (error) {
|
| 256 |
+
logError('Ошибка при получении списка чатов', error);
|
| 257 |
+
return [];
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
export function deleteChat(chatId) {
|
| 262 |
+
try {
|
| 263 |
+
const historyFilePath = getHistoryFilePath(chatId);
|
| 264 |
+
if (fs.existsSync(historyFilePath)) {
|
| 265 |
+
fs.unlinkSync(historyFilePath);
|
| 266 |
+
logInfo(`Чат ${chatId} успешно удален`);
|
| 267 |
+
return true;
|
| 268 |
+
} else {
|
| 269 |
+
logError(`Попытка удаления несуществующего чата ${chatId}`);
|
| 270 |
+
}
|
| 271 |
+
} catch (error) {
|
| 272 |
+
logError(`Ошибка при удалении чата ${chatId}`, error);
|
| 273 |
+
}
|
| 274 |
+
return false;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
export function deleteChatsAutomatically(criteria = {}) {
|
| 278 |
+
try {
|
| 279 |
+
const { olderThan, userMessageCountLessThan, messageCountLessThan, maxChats } = criteria;
|
| 280 |
+
logInfo(`Автоудаление чатов с критериями: ${JSON.stringify(criteria)}`);
|
| 281 |
+
|
| 282 |
+
const chats = getAllChats();
|
| 283 |
+
logInfo(`Найдено ${chats.length} чатов для проверки`);
|
| 284 |
+
|
| 285 |
+
let chatsToDelete = [...chats];
|
| 286 |
+
|
| 287 |
+
// Фильтрация по возрасту (в миллисекундах)
|
| 288 |
+
if (olderThan) {
|
| 289 |
+
const cutoffTime = Date.now() - olderThan;
|
| 290 |
+
const oldChatsCount = chatsToDelete.filter(chat => chat.created < cutoffTime).length;
|
| 291 |
+
logInfo(`Чатов старше ${olderThan}мс (${new Date(cutoffTime).toLocaleString()}): ${oldChatsCount}`);
|
| 292 |
+
chatsToDelete = chatsToDelete.filter(chat => chat.created < cutoffTime);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if (userMessageCountLessThan !== undefined) {
|
| 296 |
+
const lowUserMsgChatsCount = chatsToDelete.filter(chat =>
|
| 297 |
+
chat.userMessageCount < userMessageCountLessThan).length;
|
| 298 |
+
logInfo(`Чатов с менее чем ${userMessageCountLessThan} сообщений пользователя: ${lowUserMsgChatsCount}`);
|
| 299 |
+
chatsToDelete = chatsToDelete.filter(chat =>
|
| 300 |
+
chat.userMessageCount < userMessageCountLessThan);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
if (messageCountLessThan !== undefined) {
|
| 304 |
+
const lowMsgChatsCount = chatsToDelete.filter(chat =>
|
| 305 |
+
chat.messageCount < messageCountLessThan).length;
|
| 306 |
+
logInfo(`Чатов с менее чем ${messageCountLessThan} сообщений всего: ${lowMsgChatsCount}`);
|
| 307 |
+
chatsToDelete = chatsToDelete.filter(chat =>
|
| 308 |
+
chat.messageCount < messageCountLessThan);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
if (maxChats && chats.length > maxChats) {
|
| 312 |
+
logInfo(`Общее количество чатов (${chats.length}) превышает лимит (${maxChats}), удаляем старые чаты`);
|
| 313 |
+
const sortedChats = [...chats].sort((a, b) => a.created - b.created);
|
| 314 |
+
const oldestChats = sortedChats.slice(0, chats.length - maxChats);
|
| 315 |
+
|
| 316 |
+
oldestChats.forEach(chat => {
|
| 317 |
+
if (!chatsToDelete.some(c => c.id === chat.id)) {
|
| 318 |
+
chatsToDelete.push(chat);
|
| 319 |
+
}
|
| 320 |
+
});
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// Удаление выбранных чатов
|
| 324 |
+
const deletedChats = [];
|
| 325 |
+
logInfo(`Найдено ${chatsToDelete.length} чатов для удаления`);
|
| 326 |
+
|
| 327 |
+
for (const chat of chatsToDelete) {
|
| 328 |
+
if (deleteChat(chat.id)) {
|
| 329 |
+
deletedChats.push(chat.id);
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
logInfo(`Удалено ${deletedChats.length} чатов`);
|
| 334 |
+
return {
|
| 335 |
+
success: true,
|
| 336 |
+
deletedCount: deletedChats.length,
|
| 337 |
+
deletedChats
|
| 338 |
+
};
|
| 339 |
+
} catch (error) {
|
| 340 |
+
logError('Ошибка при автоматическом удалении чатов', error);
|
| 341 |
+
return {
|
| 342 |
+
success: false,
|
| 343 |
+
error: error.message
|
| 344 |
+
};
|
| 345 |
+
}
|
| 346 |
+
}
|
src/api/fileUpload.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// FileUpload.js - Модуль для загрузки файлов в чат Qwen.ai
|
| 2 |
+
import { getBrowserContext } from '../browser/browser.js';
|
| 3 |
+
import { logInfo, logError } from '../logger/index.js';
|
| 4 |
+
import { getAuthToken, extractAuthToken, pagePool } from './chat.js';
|
| 5 |
+
import { getAvailableToken } from './tokenManager.js';
|
| 6 |
+
|
| 7 |
+
import fs from 'fs';
|
| 8 |
+
import path from 'path';
|
| 9 |
+
import { fileURLToPath } from 'url';
|
| 10 |
+
|
| 11 |
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 12 |
+
const UPLOAD_DIR = path.join(__dirname, '../../uploads');
|
| 13 |
+
|
| 14 |
+
const STS_TOKEN_API_URL = 'https://chat.qwen.ai/api/v1/files/getstsToken';
|
| 15 |
+
const OSS_SDK_URL = 'https://gosspublic.alicdn.com/aliyun-oss-sdk-6.20.0.min.js';
|
| 16 |
+
|
| 17 |
+
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
| 18 |
+
const DOCUMENT_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt'];
|
| 19 |
+
const DEFAULT_FILE_TYPE = 'file';
|
| 20 |
+
const IMAGE_FILE_TYPE = 'image';
|
| 21 |
+
const DOCUMENT_FILE_TYPE = 'document';
|
| 22 |
+
|
| 23 |
+
// Убедимся, что директория для загрузок существует
|
| 24 |
+
if (!fs.existsSync(UPLOAD_DIR)) {
|
| 25 |
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Получает и валидирует browser context
|
| 30 |
+
* @returns {Object} - Browser context
|
| 31 |
+
* @throws {Error} - Если браузер не инициализирован
|
| 32 |
+
*/
|
| 33 |
+
function validateBrowserContext() {
|
| 34 |
+
const browserContext = getBrowserContext();
|
| 35 |
+
if (!browserContext) {
|
| 36 |
+
throw new Error('Браузер не инициализирован');
|
| 37 |
+
}
|
| 38 |
+
return browserContext;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* Получает токен авторизации, извлекая из браузера при необходимости
|
| 43 |
+
* @param {Object} browserContext - Browser context
|
| 44 |
+
* @returns {Promise<string>} - Токен авторизации
|
| 45 |
+
* @throws {Error} - Если не удалось получить токен
|
| 46 |
+
*/
|
| 47 |
+
async function validateAuthToken(browserContext) {
|
| 48 |
+
let tokenObj = await getAvailableToken();
|
| 49 |
+
let token = null;
|
| 50 |
+
|
| 51 |
+
if (tokenObj && tokenObj.token) {
|
| 52 |
+
token = tokenObj.token;
|
| 53 |
+
logInfo(`Используется токен из tokenManager: ${tokenObj.id}`);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (!token) {
|
| 57 |
+
token = getAuthToken();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (!token) {
|
| 61 |
+
logInfo('Токен авторизации не найден в памяти, пытаемся извлечь из браузера');
|
| 62 |
+
token = await extractAuthToken(browserContext);
|
| 63 |
+
if (!token) {
|
| 64 |
+
throw new Error('Не удалось получить токен авторизации');
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
return token;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Получает STS токен доступа для загрузки файлов
|
| 73 |
+
* @param {Object} fileInfo - Информация о файле (имя, размер, тип)
|
| 74 |
+
* @returns {Promise<Object>} - Объект с данными токена доступа
|
| 75 |
+
*/
|
| 76 |
+
export async function getStsToken(fileInfo) {
|
| 77 |
+
const browserContext = validateBrowserContext();
|
| 78 |
+
const token = await validateAuthToken(browserContext);
|
| 79 |
+
|
| 80 |
+
logInfo(`Запрос STS токена для файла: ${fileInfo.filename}`);
|
| 81 |
+
|
| 82 |
+
let page = null;
|
| 83 |
+
try {
|
| 84 |
+
page = await pagePool.getPage(browserContext);
|
| 85 |
+
|
| 86 |
+
const result = await page.evaluate(async (data) => {
|
| 87 |
+
try {
|
| 88 |
+
const response = await fetch(data.apiUrl, {
|
| 89 |
+
method: 'POST',
|
| 90 |
+
headers: {
|
| 91 |
+
'Content-Type': 'application/json',
|
| 92 |
+
'Authorization': `Bearer ${data.token}`,
|
| 93 |
+
'Accept': 'application/json'
|
| 94 |
+
},
|
| 95 |
+
body: JSON.stringify(data.fileInfo)
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
if (response.ok) {
|
| 99 |
+
return { success: true, data: await response.json()};
|
| 100 |
+
} else {
|
| 101 |
+
return {
|
| 102 |
+
success: false,
|
| 103 |
+
status: response.status,
|
| 104 |
+
statusText: response.statusText,
|
| 105 |
+
errorBody: await response.text()
|
| 106 |
+
};
|
| 107 |
+
}
|
| 108 |
+
} catch (error) {
|
| 109 |
+
return { success: false, error: error.toString() };
|
| 110 |
+
}
|
| 111 |
+
}, { apiUrl: STS_TOKEN_API_URL, token, fileInfo });
|
| 112 |
+
|
| 113 |
+
if (result.success) {
|
| 114 |
+
logInfo(`STS токен успешно получен для файла: ${fileInfo.filename}`);
|
| 115 |
+
return result.data;
|
| 116 |
+
} else {
|
| 117 |
+
logError(`Ошибка при получении STS токена: status=${result.status}, error=${result.errorBody || result.error}`);
|
| 118 |
+
throw new Error(`Ошибка получения STS токена: ${result.statusText || result.error}`);
|
| 119 |
+
}
|
| 120 |
+
} catch (error) {
|
| 121 |
+
logError(`Ошибка при получении STS токена: ${error.message}`, error);
|
| 122 |
+
throw error;
|
| 123 |
+
} finally {
|
| 124 |
+
if (page) {
|
| 125 |
+
try {
|
| 126 |
+
pagePool.releasePage(page);
|
| 127 |
+
} catch (e) {
|
| 128 |
+
logError('Ошибка при возврате страницы в пул:', e);
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* Загружает файл на URL, полученный с STS токеном
|
| 136 |
+
* @param {string} filePath - Путь к файлу для загрузки
|
| 137 |
+
* @param {Object} stsData - Данные STS токена
|
| 138 |
+
* @returns {Promise<Object>} - Результат загрузки файла
|
| 139 |
+
*/
|
| 140 |
+
export async function uploadFile(filePath, stsData) {
|
| 141 |
+
const browserContext = validateBrowserContext();
|
| 142 |
+
|
| 143 |
+
logInfo(`Начало загрузки файла: ${filePath}`);
|
| 144 |
+
|
| 145 |
+
if (!stsData?.file_path || !stsData?.access_key_id || !stsData?.access_key_secret ||
|
| 146 |
+
!stsData?.security_token || !stsData?.region || !stsData?.bucketname) {
|
| 147 |
+
throw new Error('Некорректные или неполные данные STS токена');
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
logInfo(`[OSS] Загрузка через браузер`);
|
| 151 |
+
logInfo(`[OSS] Регион: ${stsData.region}, Бакет: ${stsData.bucketname}`);
|
| 152 |
+
if (stsData.endpoint) {
|
| 153 |
+
logInfo(`[OSS] Endpoint: ${stsData.endpoint}`);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
const fileBuffer = fs.readFileSync(filePath);
|
| 157 |
+
const fileBase64 = fileBuffer.toString('base64');
|
| 158 |
+
|
| 159 |
+
logInfo(`[OSS] Размер файла: ${fileBuffer.length} байт`);
|
| 160 |
+
|
| 161 |
+
let page = null;
|
| 162 |
+
try {
|
| 163 |
+
page = await pagePool.getPage(browserContext);
|
| 164 |
+
|
| 165 |
+
const result = await page.evaluate(async (data) => {
|
| 166 |
+
try {
|
| 167 |
+
if (typeof window.OSS === 'undefined') {
|
| 168 |
+
await new Promise((resolve, reject) => {
|
| 169 |
+
const script = document.createElement('script');
|
| 170 |
+
script.src = data.ossSdkUrl;
|
| 171 |
+
script.onload = resolve;
|
| 172 |
+
script.onerror = reject;
|
| 173 |
+
document.head.appendChild(script);
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
const blob = new Blob([Uint8Array.from(atob(data.fileBase64), c => c.charCodeAt(0))])
|
| 177 |
+
|
| 178 |
+
const client = new window.OSS({
|
| 179 |
+
region: data.stsData.region,
|
| 180 |
+
accessKeyId: data.stsData.access_key_id,
|
| 181 |
+
accessKeySecret: data.stsData.access_key_secret,
|
| 182 |
+
stsToken: data.stsData.security_token,
|
| 183 |
+
bucket: data.stsData.bucketname,
|
| 184 |
+
secure: true
|
| 185 |
+
});
|
| 186 |
+
|
| 187 |
+
await client.put(data.stsData.file_path, blob);
|
| 188 |
+
return { success: true };
|
| 189 |
+
} catch (error) {
|
| 190 |
+
return { success: false, error: error.toString() };
|
| 191 |
+
}
|
| 192 |
+
}, {
|
| 193 |
+
fileBase64,
|
| 194 |
+
ossSdkUrl: OSS_SDK_URL,
|
| 195 |
+
stsData: {
|
| 196 |
+
region: stsData.region,
|
| 197 |
+
bucketname: stsData.bucketname,
|
| 198 |
+
file_path: stsData.file_path,
|
| 199 |
+
access_key_id: stsData.access_key_id,
|
| 200 |
+
access_key_secret: stsData.access_key_secret,
|
| 201 |
+
security_token: stsData.security_token
|
| 202 |
+
}
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
if (result.success) {
|
| 206 |
+
return {
|
| 207 |
+
success: true,
|
| 208 |
+
fileName: path.basename(filePath),
|
| 209 |
+
url: stsData.file_url,
|
| 210 |
+
fileId: stsData.file_id,
|
| 211 |
+
filePath: stsData.file_path
|
| 212 |
+
};
|
| 213 |
+
} else {
|
| 214 |
+
logError(`[OSS] Ошибка загрузки: ${result.error}`);
|
| 215 |
+
throw new Error(`Ошибка загрузки в OSS: ${result.error}`);
|
| 216 |
+
}
|
| 217 |
+
} catch (error) {
|
| 218 |
+
logError(`Ошибка при загрузке файла в OSS: ${error.message}`, error);
|
| 219 |
+
throw error;
|
| 220 |
+
} finally {
|
| 221 |
+
if (page) {
|
| 222 |
+
try {
|
| 223 |
+
pagePool.releasePage(page);
|
| 224 |
+
} catch (e) {
|
| 225 |
+
logError('Ошибка при возврате страницы в пул:', e);
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* Полный процесс загрузки файла: получение токена и загрузка
|
| 233 |
+
* @param {string} filePath - Путь к файлу для загрузки
|
| 234 |
+
* @returns {Promise<Object>} - Результат загрузки файла
|
| 235 |
+
*/
|
| 236 |
+
export async function uploadFileToQwen(filePath) {
|
| 237 |
+
try {
|
| 238 |
+
// Проверяем существование файла
|
| 239 |
+
if (!fs.existsSync(filePath)) {
|
| 240 |
+
throw new Error(`Файл не найден: ${filePath}`);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
const fileName = path.basename(filePath);
|
| 244 |
+
const fileSize = fs.statSync(filePath).size;
|
| 245 |
+
const fileExt = path.extname(fileName).toLowerCase();
|
| 246 |
+
|
| 247 |
+
// Определяем тип файла
|
| 248 |
+
let fileType = DEFAULT_FILE_TYPE;
|
| 249 |
+
if (IMAGE_EXTENSIONS.includes(fileExt)) {
|
| 250 |
+
fileType = IMAGE_FILE_TYPE;
|
| 251 |
+
} else if (DOCUMENT_EXTENSIONS.includes(fileExt)) {
|
| 252 |
+
fileType = DOCUMENT_FILE_TYPE;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
// Запрашиваем STS токен
|
| 256 |
+
const fileInfo = {
|
| 257 |
+
filename: fileName,
|
| 258 |
+
filesize: fileSize,
|
| 259 |
+
filetype: fileType
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
const stsData = await getStsToken(fileInfo);
|
| 263 |
+
|
| 264 |
+
const uploadResult = await uploadFile(filePath, stsData);
|
| 265 |
+
|
| 266 |
+
return {
|
| 267 |
+
...uploadResult,
|
| 268 |
+
fileInfo,
|
| 269 |
+
stsData
|
| 270 |
+
};
|
| 271 |
+
} catch (error) {
|
| 272 |
+
logError(`Ошибка в процессе загрузки файла: ${error.message}`, error);
|
| 273 |
+
return {
|
| 274 |
+
success: false,
|
| 275 |
+
error: error.message
|
| 276 |
+
};
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
export default {
|
| 281 |
+
getStsToken,
|
| 282 |
+
uploadFile,
|
| 283 |
+
uploadFileToQwen
|
| 284 |
+
};
|
src/api/modelMapping.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CANONICAL_MODELS = Object.freeze([
|
| 2 |
+
"qwen3-max",
|
| 3 |
+
"qwen3-vl-plus",
|
| 4 |
+
"qwen3-coder-plus",
|
| 5 |
+
"qwen3-omni-flash",
|
| 6 |
+
"qwen-plus-2025-09-11",
|
| 7 |
+
"qwen3-235b-a22b",
|
| 8 |
+
"qwen3-30b-a3b",
|
| 9 |
+
"qwen3-coder-30b-a3b-instruct",
|
| 10 |
+
"qwen-max-latest",
|
| 11 |
+
"qwen-plus-2025-01-25",
|
| 12 |
+
"qwq-32b",
|
| 13 |
+
"qwen-turbo-2025-02-11",
|
| 14 |
+
"qwen2.5-omni-7b",
|
| 15 |
+
"qvq-72b-preview-0310",
|
| 16 |
+
"qwen2.5-vl-32b-instruct",
|
| 17 |
+
"qwen2.5-14b-instruct-1m",
|
| 18 |
+
"qwen2.5-coder-32b-instruct",
|
| 19 |
+
"qwen2.5-72b-instruct"
|
| 20 |
+
]);
|
| 21 |
+
|
| 22 |
+
const CANONICAL_MODEL_SET = new Set(CANONICAL_MODELS);
|
| 23 |
+
|
| 24 |
+
const ALIAS_GROUPS = Object.freeze({
|
| 25 |
+
"qwen3-max": [
|
| 26 |
+
"qwen-max",
|
| 27 |
+
"Qwen3-Max",
|
| 28 |
+
"Qwen3-Maximum",
|
| 29 |
+
"qwen3-max-preview",
|
| 30 |
+
"Qwen3-Max-Preview"
|
| 31 |
+
],
|
| 32 |
+
"qwen3-vl-plus": [
|
| 33 |
+
"qwen-vl",
|
| 34 |
+
"qwen-vl-plus",
|
| 35 |
+
"qwen-vl-plus-latest",
|
| 36 |
+
"qwen-vl-max",
|
| 37 |
+
"qwen-vl-max-latest",
|
| 38 |
+
"Qwen3-VL-235B-A22B",
|
| 39 |
+
"qwen3-vl-235b-a22b"
|
| 40 |
+
],
|
| 41 |
+
"qwen3-coder-plus": [
|
| 42 |
+
"qwen3-coder",
|
| 43 |
+
"qwen-coder-plus",
|
| 44 |
+
"qwen-coder-plus-latest",
|
| 45 |
+
"Qwen3-Coder-Plus",
|
| 46 |
+
"qwen2.5-coder-3b-instruct",
|
| 47 |
+
"qwen2.5-coder-1.5b-instruct",
|
| 48 |
+
"qwen2.5-coder-0.5b-instruct",
|
| 49 |
+
"Qwen3-Coder"
|
| 50 |
+
],
|
| 51 |
+
"qwen3-omni-flash": [
|
| 52 |
+
"qwen3-omni",
|
| 53 |
+
"qwen3-omni-latest",
|
| 54 |
+
"Qwen3-omni-flash",
|
| 55 |
+
"Qwen3-Omni-Flash",
|
| 56 |
+
"Qwen3-Omni"
|
| 57 |
+
],
|
| 58 |
+
"qwen-plus-2025-09-11": [
|
| 59 |
+
"qwen-plus",
|
| 60 |
+
"qwen-plus-latest",
|
| 61 |
+
"Qwen3-Next",
|
| 62 |
+
"Qwen3-Next-80B-A3B",
|
| 63 |
+
"Qwen3-Next-80B-A3Bб",
|
| 64 |
+
"qwen3-next",
|
| 65 |
+
"qwen3-next-80b-a3b"
|
| 66 |
+
],
|
| 67 |
+
"qwen3-235b-a22b": [
|
| 68 |
+
"qwen3",
|
| 69 |
+
"qwen-3",
|
| 70 |
+
"qwen3-235b",
|
| 71 |
+
"Qwen3-235B-A22B",
|
| 72 |
+
"Qwen3-235B-A22B-2507",
|
| 73 |
+
"qwen3-235b-a22b-2507"
|
| 74 |
+
],
|
| 75 |
+
"qwen3-30b-a3b": [
|
| 76 |
+
"qwen3-plus",
|
| 77 |
+
"qwen3-30b",
|
| 78 |
+
"Qwen3-30B-A3B",
|
| 79 |
+
"Qwen3-30B-A3B-2507",
|
| 80 |
+
"qwen3-30b-a3b-2507"
|
| 81 |
+
],
|
| 82 |
+
"qwen3-coder-30b-a3b-instruct": [
|
| 83 |
+
"qwen3-coder-flash",
|
| 84 |
+
"Qwen3-Coder-Flash",
|
| 85 |
+
"qwen3-coder-30b",
|
| 86 |
+
"Qwen3-Coder-30B-A3B-Instruct"
|
| 87 |
+
],
|
| 88 |
+
"qwen-max-latest": [
|
| 89 |
+
"Qwen2.5-Max",
|
| 90 |
+
"qwen2.5-max"
|
| 91 |
+
],
|
| 92 |
+
"qwen-plus-2025-01-25": [
|
| 93 |
+
"Qwen2.5-Plus",
|
| 94 |
+
"qwen2.5-plus"
|
| 95 |
+
],
|
| 96 |
+
"qwq-32b": [
|
| 97 |
+
"qwq",
|
| 98 |
+
"QwQ-32B",
|
| 99 |
+
"qwq-32b-preview"
|
| 100 |
+
],
|
| 101 |
+
"qwen-turbo-2025-02-11": [
|
| 102 |
+
"qwen-turbo",
|
| 103 |
+
"qwen-turbo-latest",
|
| 104 |
+
"Qwen2.5-Turbo"
|
| 105 |
+
],
|
| 106 |
+
"qwen2.5-omni-7b": [
|
| 107 |
+
"qwen2.5-omni",
|
| 108 |
+
"Qwen2.5-Omni-7B",
|
| 109 |
+
"qwen-omni-7b"
|
| 110 |
+
],
|
| 111 |
+
"qvq-72b-preview-0310": [
|
| 112 |
+
"qvq",
|
| 113 |
+
"QVQ-Max",
|
| 114 |
+
"qvq-72b"
|
| 115 |
+
],
|
| 116 |
+
"qwen2.5-vl-32b-instruct": [
|
| 117 |
+
"qwen2.5-vl",
|
| 118 |
+
"Qwen2.5-VL-32B-Instruct"
|
| 119 |
+
],
|
| 120 |
+
"qwen2.5-14b-instruct-1m": [
|
| 121 |
+
"qwen2.5-14b",
|
| 122 |
+
"qwen2.5-coder-14b-instruct",
|
| 123 |
+
"Qwen2.5-14B-Instruct-1M"
|
| 124 |
+
],
|
| 125 |
+
"qwen2.5-coder-32b-instruct": [
|
| 126 |
+
"qwen2.5-coder",
|
| 127 |
+
"qwen2.5-coder-plus",
|
| 128 |
+
"Qwen2.5-Coder-32B-Instruct"
|
| 129 |
+
],
|
| 130 |
+
"qwen2.5-72b-instruct": [
|
| 131 |
+
"qwen2.5-72b",
|
| 132 |
+
"Qwen2.5-72B-Instruct"
|
| 133 |
+
]
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
const buildModelMapping = () => {
|
| 137 |
+
const mapping = Object.create(null);
|
| 138 |
+
|
| 139 |
+
for (const model of CANONICAL_MODELS) {
|
| 140 |
+
mapping[model] = model;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
for (const [target, aliases] of Object.entries(ALIAS_GROUPS)) {
|
| 144 |
+
if (!CANONICAL_MODEL_SET.has(target)) {
|
| 145 |
+
continue;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
for (const alias of aliases) {
|
| 149 |
+
if (!alias) continue;
|
| 150 |
+
mapping[alias] = target;
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return Object.freeze(mapping);
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
export const MODEL_MAPPING = buildModelMapping();
|
| 158 |
+
|
| 159 |
+
/**
|
| 160 |
+
* Получить соответствующую доступную модель
|
| 161 |
+
* @param {string} requestedModel - Запрошенная модель
|
| 162 |
+
* @param {string} defaultModel - Модель по умолчанию
|
| 163 |
+
* @returns {string} - Доступная модель
|
| 164 |
+
*/
|
| 165 |
+
export function getMappedModel(requestedModel, defaultModel = "qwen-max-latest") {
|
| 166 |
+
if (!requestedModel) return defaultModel;
|
| 167 |
+
|
| 168 |
+
// Проверяем точное соответствие в словаре
|
| 169 |
+
if (MODEL_MAPPING[requestedModel]) {
|
| 170 |
+
return MODEL_MAPPING[requestedModel];
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Проверяем, является ли запрошенная модель уже доступной
|
| 174 |
+
const availableModels = Object.values(MODEL_MAPPING);
|
| 175 |
+
if (availableModels.includes(requestedModel)) {
|
| 176 |
+
return requestedModel;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Возвращаем модель по умолчанию, если соответствие не найдено
|
| 180 |
+
return defaultModel;
|
| 181 |
+
}
|
src/api/routes.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// routes.js - Модуль с маршрутами для API
|
| 2 |
+
import express from 'express';
|
| 3 |
+
import { sendMessage, getAllModels, getApiKeys, createChatV2 } from './chat.js';
|
| 4 |
+
import { getAuthenticationStatus } from '../browser/browser.js';
|
| 5 |
+
import { checkAuthentication } from '../browser/auth.js';
|
| 6 |
+
import { getBrowserContext } from '../browser/browser.js';
|
| 7 |
+
import { logInfo, logError, logDebug } from '../logger/index.js';
|
| 8 |
+
import { getMappedModel } from './modelMapping.js';
|
| 9 |
+
import { getStsToken, uploadFileToQwen } from './fileUpload.js';
|
| 10 |
+
import multer from 'multer';
|
| 11 |
+
import path from 'path';
|
| 12 |
+
import fs from 'fs';
|
| 13 |
+
import crypto from 'crypto';
|
| 14 |
+
import { listTokens, markInvalid, markRateLimited, markValid } from './tokenManager.js';
|
| 15 |
+
import { testToken } from './chat.js';
|
| 16 |
+
|
| 17 |
+
const router = express.Router();
|
| 18 |
+
|
| 19 |
+
// Настройка multer для загрузки файлов
|
| 20 |
+
const storage = multer.diskStorage({
|
| 21 |
+
destination: function (req, file, cb) {
|
| 22 |
+
const uploadDir = path.join(process.cwd(), 'uploads');
|
| 23 |
+
if (!fs.existsSync(uploadDir)) {
|
| 24 |
+
fs.mkdirSync(uploadDir, { recursive: true });
|
| 25 |
+
}
|
| 26 |
+
cb(null, uploadDir);
|
| 27 |
+
},
|
| 28 |
+
filename: function (req, file, cb) {
|
| 29 |
+
const uniqueSuffix = Date.now() + '-' + crypto.randomBytes(8).toString('hex');
|
| 30 |
+
cb(null, uniqueSuffix + '-' + file.originalname);
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
const upload = multer({
|
| 35 |
+
storage: storage,
|
| 36 |
+
limits: { fileSize: 10 * 1024 * 1024 } // 10MB макс. размер
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
function authMiddleware(req, res, next) {
|
| 40 |
+
const apiKeys = getApiKeys();
|
| 41 |
+
|
| 42 |
+
if (apiKeys.length === 0) {
|
| 43 |
+
return next();
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const authHeader = req.headers.authorization;
|
| 47 |
+
const apiKeyHeaderPrefix = 'Bearer ';
|
| 48 |
+
|
| 49 |
+
if (!authHeader || !authHeader.startsWith(apiKeyHeaderPrefix)) {
|
| 50 |
+
logError('Отсутствует или некорректный заголовок авторизации');
|
| 51 |
+
return res.status(401).json({ error: 'Требуется авторизация' });
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const token = authHeader.substring(apiKeyHeaderPrefix.length).trim();
|
| 55 |
+
|
| 56 |
+
if (!apiKeys.includes(token)) {
|
| 57 |
+
logError('Предоставлен недействительный API ключ');
|
| 58 |
+
return res.status(401).json({ error: 'Недействительный токен' });
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
next();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
router.use(authMiddleware);
|
| 65 |
+
router.use((req, res, next) => {
|
| 66 |
+
req.url = req.url
|
| 67 |
+
.replace(/\/v[12](?=\/|$)/g, '')
|
| 68 |
+
.replace(/\/+/g, '/');
|
| 69 |
+
next();
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
router.post('/chat', async (req, res) => {
|
| 73 |
+
try {
|
| 74 |
+
const { message, messages, model, chatId, parentId } = req.body;
|
| 75 |
+
|
| 76 |
+
// Поддержка как message, так и messages для совместимости
|
| 77 |
+
let messageContent = message;
|
| 78 |
+
let systemMessage = null;
|
| 79 |
+
|
| 80 |
+
// Если указан параметр messages (множественное число), используем его в приоритете
|
| 81 |
+
if (messages && Array.isArray(messages)) {
|
| 82 |
+
// Извлекаем system message если есть
|
| 83 |
+
const systemMsg = messages.find(msg => msg.role === 'system');
|
| 84 |
+
if (systemMsg) {
|
| 85 |
+
systemMessage = systemMsg.content;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Преобразуем формат messages в формат сообщения, понятный нашему прокси
|
| 89 |
+
if (messages.length > 0) {
|
| 90 |
+
const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
|
| 91 |
+
if (lastUserMessage) {
|
| 92 |
+
if (Array.isArray(lastUserMessage.content)) {
|
| 93 |
+
messageContent = lastUserMessage.content;
|
| 94 |
+
} else {
|
| 95 |
+
messageContent = lastUserMessage.content;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (!messageContent) {
|
| 102 |
+
logError('Запрос без сообщения');
|
| 103 |
+
return res.status(400).json({ error: 'Сообщение не указано' });
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
logInfo(`Получен запрос: ${typeof messageContent === 'string' ? messageContent.substring(0, 50) + (messageContent.length > 50 ? '...' : '') : 'Составное сообщение'}`);
|
| 107 |
+
if (systemMessage) {
|
| 108 |
+
logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`);
|
| 109 |
+
}
|
| 110 |
+
if (chatId) {
|
| 111 |
+
logInfo(`Используется chatId: ${chatId}, parentId: ${parentId || 'null'}`);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
let mappedModel = model || "qwen-max-latest";
|
| 115 |
+
if (model) {
|
| 116 |
+
mappedModel = getMappedModel(model);
|
| 117 |
+
if (mappedModel !== model) {
|
| 118 |
+
logInfo(`Модель "${model}" заменена на "${mappedModel}"`);
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
logInfo(`Используется модель: ${mappedModel}`);
|
| 122 |
+
|
| 123 |
+
const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, null, null, systemMessage);
|
| 124 |
+
|
| 125 |
+
if (result.choices && result.choices[0] && result.choices[0].message) {
|
| 126 |
+
const responseLength = result.choices[0].message.content ? result.choices[0].message.content.length : 0;
|
| 127 |
+
logInfo(`Ответ успешно сформирован для запроса, длина ответа: ${responseLength}`);
|
| 128 |
+
} else if (result.error) {
|
| 129 |
+
logInfo(`Получена ошибка в ответе: ${result.error}`);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
res.json(result);
|
| 133 |
+
} catch (error) {
|
| 134 |
+
logError('Ошибка при обработке запроса', error);
|
| 135 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
router.get('/models', async (req, res) => {
|
| 140 |
+
try {
|
| 141 |
+
logInfo('Запрос на получение списка моделей');
|
| 142 |
+
const modelsRaw = getAllModels();
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
const openAiModels = {
|
| 146 |
+
object: 'list',
|
| 147 |
+
data: modelsRaw.models.map(m => ({
|
| 148 |
+
id: m.id || m.name || m,
|
| 149 |
+
object: 'model',
|
| 150 |
+
created: 0,
|
| 151 |
+
owned_by: 'openai',
|
| 152 |
+
permission: []
|
| 153 |
+
}))
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
logInfo(`Возвращено ${openAiModels.data.length} моделей (OpenAI формат)`);
|
| 157 |
+
res.json(openAiModels);
|
| 158 |
+
} catch (error) {
|
| 159 |
+
logError('Ошибка при получении списка моделей', error);
|
| 160 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 161 |
+
}
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
router.get('/status', async (req, res) => {
|
| 166 |
+
try {
|
| 167 |
+
logInfo('Запрос статуса авторизации');
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
const tokens = listTokens();
|
| 171 |
+
const accounts = await Promise.all(tokens.map(async t => {
|
| 172 |
+
const accInfo = { id: t.id, status: 'UNKNOWN', resetAt: t.resetAt || null };
|
| 173 |
+
|
| 174 |
+
if (t.resetAt) {
|
| 175 |
+
const resetTime = new Date(t.resetAt).getTime();
|
| 176 |
+
if (resetTime > Date.now()) {
|
| 177 |
+
accInfo.status = 'WAIT';
|
| 178 |
+
return accInfo;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
const testResult = await testToken(t.token);
|
| 183 |
+
if (testResult === 'OK') {
|
| 184 |
+
accInfo.status = 'OK';
|
| 185 |
+
if (t.invalid || t.resetAt) markValid(t.id);
|
| 186 |
+
} else if (testResult === 'RATELIMIT') {
|
| 187 |
+
accInfo.status = 'WAIT';
|
| 188 |
+
markRateLimited(t.id, 24);
|
| 189 |
+
} else if (testResult === 'UNAUTHORIZED') {
|
| 190 |
+
accInfo.status = 'INVALID';
|
| 191 |
+
if (!t.invalid) markInvalid(t.id);
|
| 192 |
+
} else {
|
| 193 |
+
accInfo.status = 'ERROR';
|
| 194 |
+
}
|
| 195 |
+
return accInfo;
|
| 196 |
+
}));
|
| 197 |
+
|
| 198 |
+
const browserContext = getBrowserContext();
|
| 199 |
+
if (!browserContext) {
|
| 200 |
+
logError('Браузер не инициализирован');
|
| 201 |
+
return res.json({ authenticated: false, message: 'Браузер не инициализирован', accounts });
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
if (getAuthenticationStatus()) {
|
| 205 |
+
return res.json({
|
| 206 |
+
accounts
|
| 207 |
+
});
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
await checkAuthentication(browserContext);
|
| 211 |
+
const isAuthenticated = getAuthenticationStatus();
|
| 212 |
+
logInfo(`Статус авторизации: ${isAuthenticated ? 'активна' : 'требуется авторизация'}`);
|
| 213 |
+
|
| 214 |
+
res.json({
|
| 215 |
+
authenticated: isAuthenticated,
|
| 216 |
+
message: isAuthenticated ? 'Авторизация активна' : 'Требуется авторизация',
|
| 217 |
+
accounts
|
| 218 |
+
});
|
| 219 |
+
} catch (error) {
|
| 220 |
+
logError('Ошибка при проверке статуса авторизации', error);
|
| 221 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 222 |
+
}
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
router.post('/chats', async (req, res) => {
|
| 226 |
+
try {
|
| 227 |
+
const { name, model } = req.body;
|
| 228 |
+
const chatModel = model ? getMappedModel(model) : 'qwen-max-latest';
|
| 229 |
+
logInfo(`Создание нового чата${name ? ` с именем: ${name}` : ''}, модель: ${chatModel}`);
|
| 230 |
+
|
| 231 |
+
const result = await createChatV2(chatModel, name || "Новый чат");
|
| 232 |
+
|
| 233 |
+
if (result.error) {
|
| 234 |
+
logError(`Ошибка создания чата: ${result.error}`);
|
| 235 |
+
return res.status(500).json({ error: result.error });
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
logInfo(`Создан новый чат v2 с ID: ${result.chatId}`);
|
| 239 |
+
res.json({ chatId: result.chatId, success: true });
|
| 240 |
+
} catch (error) {
|
| 241 |
+
logError('Ошибка при создании чата', error);
|
| 242 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 243 |
+
}
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
router.post('/analyze/network', (req, res) => {
|
| 247 |
+
try {
|
| 248 |
+
return res.json({ success: true });
|
| 249 |
+
} catch (error) {
|
| 250 |
+
logError('Ошибка при анализе с��ти', error);
|
| 251 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 252 |
+
}
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
router.post('/chat/completions', async (req, res) => {
|
| 256 |
+
try {
|
| 257 |
+
const { messages, model, stream, tools, functions, tool_choice, chatId, parentId } = req.body;
|
| 258 |
+
|
| 259 |
+
logInfo(`Получен OpenAI-совместимый запрос${stream ? ' (stream)' : ''}`);
|
| 260 |
+
|
| 261 |
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
| 262 |
+
logError('Запрос без сообщений');
|
| 263 |
+
return res.status(400).json({ error: 'Сообщения не указаны' });
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// Извлекаем system message если есть
|
| 267 |
+
const systemMsg = messages.find(msg => msg.role === 'system');
|
| 268 |
+
const systemMessage = systemMsg ? systemMsg.content : null;
|
| 269 |
+
|
| 270 |
+
const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
|
| 271 |
+
if (!lastUserMessage) {
|
| 272 |
+
logError('В запросе нет сообщений от пользователя');
|
| 273 |
+
return res.status(400).json({ error: 'В запросе нет сообщений от пользователя' });
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
const messageContent = lastUserMessage.content;
|
| 277 |
+
|
| 278 |
+
let mappedModel = model ? getMappedModel(model) : "qwen-max-latest";
|
| 279 |
+
if (model && mappedModel !== model) {
|
| 280 |
+
logInfo(`Модель "${model}" заменена на "${mappedModel}"`);
|
| 281 |
+
}
|
| 282 |
+
logInfo(`Используется модель: ${mappedModel}`);
|
| 283 |
+
|
| 284 |
+
if (systemMessage) {
|
| 285 |
+
logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
if (stream) {
|
| 289 |
+
res.setHeader('Content-Type', 'text/event-stream');
|
| 290 |
+
res.setHeader('Cache-Control', 'no-cache');
|
| 291 |
+
res.setHeader('Connection', 'keep-alive');
|
| 292 |
+
|
| 293 |
+
const writeSse = (payload) => {
|
| 294 |
+
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
| 295 |
+
};
|
| 296 |
+
|
| 297 |
+
writeSse({
|
| 298 |
+
id: 'chatcmpl-stream',
|
| 299 |
+
object: 'chat.completion.chunk',
|
| 300 |
+
created: Math.floor(Date.now() / 1000),
|
| 301 |
+
model: mappedModel || 'qwen-max-latest',
|
| 302 |
+
choices: [
|
| 303 |
+
{ index: 0, delta: { role: 'assistant' }, finish_reason: null }
|
| 304 |
+
]
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
try {
|
| 308 |
+
const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
|
| 309 |
+
const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, combinedTools, tool_choice, systemMessage);
|
| 310 |
+
|
| 311 |
+
if (result.error) {
|
| 312 |
+
writeSse({
|
| 313 |
+
id: 'chatcmpl-stream',
|
| 314 |
+
object: 'chat.completion.chunk',
|
| 315 |
+
created: Math.floor(Date.now() / 1000),
|
| 316 |
+
model: mappedModel || 'qwen-max-latest',
|
| 317 |
+
choices: [
|
| 318 |
+
{ index: 0, delta: { content: `Error: ${result.error}` }, finish_reason: null }
|
| 319 |
+
]
|
| 320 |
+
});
|
| 321 |
+
} else if (result.choices && result.choices[0] && result.choices[0].message) {
|
| 322 |
+
const content = String(result.choices[0].message.content || '');
|
| 323 |
+
|
| 324 |
+
const codePoints = Array.from(content);
|
| 325 |
+
const chunkSize = 16;
|
| 326 |
+
for (let i = 0; i < codePoints.length; i += chunkSize) {
|
| 327 |
+
const chunk = codePoints.slice(i, i + chunkSize).join('');
|
| 328 |
+
writeSse({
|
| 329 |
+
id: 'chatcmpl-stream',
|
| 330 |
+
object: 'chat.completion.chunk',
|
| 331 |
+
created: Math.floor(Date.now() / 1000),
|
| 332 |
+
model: mappedModel || 'qwen-max-latest',
|
| 333 |
+
choices: [
|
| 334 |
+
{ index: 0, delta: { content: chunk }, finish_reason: null }
|
| 335 |
+
]
|
| 336 |
+
});
|
| 337 |
+
|
| 338 |
+
await new Promise(resolve => setTimeout(resolve, 20));
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
writeSse({
|
| 343 |
+
id: 'chatcmpl-stream',
|
| 344 |
+
object: 'chat.completion.chunk',
|
| 345 |
+
created: Math.floor(Date.now() / 1000),
|
| 346 |
+
model: mappedModel || 'qwen-max-latest',
|
| 347 |
+
choices: [
|
| 348 |
+
{ index: 0, delta: {}, finish_reason: 'stop' }
|
| 349 |
+
]
|
| 350 |
+
});
|
| 351 |
+
res.write('data: [DONE]\n\n');
|
| 352 |
+
res.end();
|
| 353 |
+
|
| 354 |
+
} catch (error) {
|
| 355 |
+
logError('Ошибка при обработке потокового запроса', error);
|
| 356 |
+
writeSse({
|
| 357 |
+
id: 'chatcmpl-stream',
|
| 358 |
+
object: 'chat.completion.chunk',
|
| 359 |
+
created: Math.floor(Date.now() / 1000),
|
| 360 |
+
model: mappedModel || 'qwen-max-latest',
|
| 361 |
+
choices: [
|
| 362 |
+
{ index: 0, delta: { content: 'Internal server error' }, finish_reason: 'stop' }
|
| 363 |
+
]
|
| 364 |
+
});
|
| 365 |
+
res.write('data: [DONE]\n\n');
|
| 366 |
+
res.end();
|
| 367 |
+
}
|
| 368 |
+
} else {
|
| 369 |
+
const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
|
| 370 |
+
const result = await sendMessage(messageContent, mappedModel, chatId, parentId, null, combinedTools, tool_choice, systemMessage);
|
| 371 |
+
|
| 372 |
+
if (result.error) {
|
| 373 |
+
return res.status(500).json({
|
| 374 |
+
error: { message: result.error, type: "server_error" }
|
| 375 |
+
});
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
const openaiResponse = {
|
| 379 |
+
id: result.id || "chatcmpl-" + Date.now(),
|
| 380 |
+
object: "chat.completion",
|
| 381 |
+
created: Math.floor(Date.now() / 1000),
|
| 382 |
+
model: result.model || mappedModel || "qwen-max-latest",
|
| 383 |
+
choices: result.choices || [{
|
| 384 |
+
index: 0,
|
| 385 |
+
message: {
|
| 386 |
+
role: "assistant",
|
| 387 |
+
content: result.choices?.[0]?.message?.content || ""
|
| 388 |
+
},
|
| 389 |
+
finish_reason: "stop"
|
| 390 |
+
}],
|
| 391 |
+
usage: result.usage || {
|
| 392 |
+
prompt_tokens: 0,
|
| 393 |
+
completion_tokens: 0,
|
| 394 |
+
total_tokens: 0
|
| 395 |
+
},
|
| 396 |
+
chatId: result.chatId,
|
| 397 |
+
parentId: result.parentId
|
| 398 |
+
};
|
| 399 |
+
|
| 400 |
+
res.json(openaiResponse);
|
| 401 |
+
}
|
| 402 |
+
} catch (error) {
|
| 403 |
+
logError('Ошибка при обработке запроса', error);
|
| 404 |
+
res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: "server_error" } });
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
// Новый маршрут для получения STS токена
|
| 409 |
+
router.post('/files/getstsToken', async (req, res) => {
|
| 410 |
+
try {
|
| 411 |
+
logInfo(`Запрос на получение STS токена: ${JSON.stringify(req.body)}`);
|
| 412 |
+
|
| 413 |
+
const fileInfo = req.body;
|
| 414 |
+
if (!fileInfo || !fileInfo.filename || !fileInfo.filesize || !fileInfo.filetype) {
|
| 415 |
+
logError('Некорректные данные о файле');
|
| 416 |
+
return res.status(400).json({ error: 'Некорректные данные о файле' });
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
const stsToken = await getStsToken(fileInfo);
|
| 420 |
+
res.json(stsToken);
|
| 421 |
+
} catch (error) {
|
| 422 |
+
logError('Ошибка при получении STS токена', error);
|
| 423 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 424 |
+
}
|
| 425 |
+
});
|
| 426 |
+
|
| 427 |
+
// Маршрут для загрузки файла - работает
|
| 428 |
+
router.post('/files/upload', upload.single('file'), async (req, res) => {
|
| 429 |
+
try {
|
| 430 |
+
if (!req.file) {
|
| 431 |
+
logError('Файл не был загружен');
|
| 432 |
+
return res.status(400).json({ error: 'Файл не был загружен' });
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
logInfo(`Файл загружен на сервер: ${req.file.originalname} (${req.file.size} байт)`);
|
| 436 |
+
|
| 437 |
+
// Загружаем файл в Qwen OSS хранилище
|
| 438 |
+
const result = await uploadFileToQwen(req.file.path);
|
| 439 |
+
|
| 440 |
+
// Удаляем временный файл после успешной загрузки
|
| 441 |
+
fs.unlinkSync(req.file.path);
|
| 442 |
+
|
| 443 |
+
if (result.success) {
|
| 444 |
+
logInfo(`Файл успешно загружен в OSS: ${result.fileName}`);
|
| 445 |
+
res.json({
|
| 446 |
+
success: true,
|
| 447 |
+
file: {
|
| 448 |
+
name: result.fileName,
|
| 449 |
+
url: result.url,
|
| 450 |
+
size: req.file.size,
|
| 451 |
+
type: req.file.mimetype
|
| 452 |
+
}
|
| 453 |
+
});
|
| 454 |
+
} else {
|
| 455 |
+
logError(`Ошибка при загрузке файла в OSS: ${result.error}`);
|
| 456 |
+
res.status(500).json({ error: 'Ошибка при загрузке файла' });
|
| 457 |
+
}
|
| 458 |
+
} catch (error) {
|
| 459 |
+
logError('Ошибка при загрузке файла', error);
|
| 460 |
+
|
| 461 |
+
// Удаляем временный файл в случае ошибки
|
| 462 |
+
if (req.file && req.file.path && fs.existsSync(req.file.path)) {
|
| 463 |
+
fs.unlinkSync(req.file.path);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
| 467 |
+
}
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
export default router;
|
src/api/tokenManager.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
+
|
| 5 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
+
const __dirname = path.dirname(__filename);
|
| 7 |
+
let pointer = 0;
|
| 8 |
+
|
| 9 |
+
// Директория для хранения сессий и данных аккаунтов
|
| 10 |
+
const SESSION_DIR = path.join(__dirname, '..', '..', 'session');
|
| 11 |
+
const ACCOUNTS_DIR = path.join(SESSION_DIR, 'accounts');
|
| 12 |
+
const TOKENS_FILE = path.join(SESSION_DIR, 'tokens.json');
|
| 13 |
+
|
| 14 |
+
function ensureSessionDir() {
|
| 15 |
+
if (!fs.existsSync(SESSION_DIR)) {
|
| 16 |
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
| 17 |
+
}
|
| 18 |
+
if (!fs.existsSync(ACCOUNTS_DIR)) {
|
| 19 |
+
fs.mkdirSync(ACCOUNTS_DIR, { recursive: true });
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export function loadTokens() {
|
| 24 |
+
ensureSessionDir();
|
| 25 |
+
if (!fs.existsSync(TOKENS_FILE)) {
|
| 26 |
+
return [];
|
| 27 |
+
}
|
| 28 |
+
try {
|
| 29 |
+
const data = fs.readFileSync(TOKENS_FILE, 'utf8');
|
| 30 |
+
return JSON.parse(data);
|
| 31 |
+
} catch (e) {
|
| 32 |
+
console.error('TokenManager: ошибка чтения tokens.json:', e);
|
| 33 |
+
return [];
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export function saveTokens(tokens) {
|
| 38 |
+
ensureSessionDir();
|
| 39 |
+
try {
|
| 40 |
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2), 'utf8');
|
| 41 |
+
} catch (e) {
|
| 42 |
+
console.error('TokenManager: ошибка сохранения tokens.json:', e);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export async function getAvailableToken() {
|
| 47 |
+
const tokens = loadTokens();
|
| 48 |
+
const now = Date.now();
|
| 49 |
+
const valid = tokens.filter(t => (!t.resetAt || new Date(t.resetAt).getTime() <= now) && !t.invalid);
|
| 50 |
+
if (!valid.length) return null;
|
| 51 |
+
const token = valid[pointer % valid.length];
|
| 52 |
+
pointer = (pointer + 1) % valid.length;
|
| 53 |
+
return token;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export function hasValidTokens() {
|
| 57 |
+
const tokens = loadTokens();
|
| 58 |
+
const now = Date.now();
|
| 59 |
+
return tokens.some(t => (!t.resetAt || new Date(t.resetAt).getTime() <= now) && !t.invalid);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export function markRateLimited(id, hours = 24) {
|
| 63 |
+
const tokens = loadTokens();
|
| 64 |
+
const idx = tokens.findIndex(t => t.id === id);
|
| 65 |
+
if (idx !== -1) {
|
| 66 |
+
const until = new Date(Date.now() + hours * 3600 * 1000).toISOString();
|
| 67 |
+
tokens[idx].resetAt = until;
|
| 68 |
+
saveTokens(tokens);
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function removeToken(id) {
|
| 73 |
+
const tokens = loadTokens();
|
| 74 |
+
const filtered = tokens.filter(t => t.id !== id);
|
| 75 |
+
saveTokens(filtered);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export { removeToken as removeInvalidToken };
|
| 79 |
+
|
| 80 |
+
export function markInvalid(id) {
|
| 81 |
+
const tokens = loadTokens();
|
| 82 |
+
const idx = tokens.findIndex(t => t.id === id);
|
| 83 |
+
if (idx !== -1) {
|
| 84 |
+
tokens[idx].invalid = true;
|
| 85 |
+
saveTokens(tokens);
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
export function markValid(id, newToken) {
|
| 90 |
+
const tokens = loadTokens();
|
| 91 |
+
const idx = tokens.findIndex(t => t.id === id);
|
| 92 |
+
if (idx !== -1) {
|
| 93 |
+
tokens[idx].invalid = false;
|
| 94 |
+
tokens[idx].resetAt = null;
|
| 95 |
+
if (newToken) tokens[idx].token = newToken;
|
| 96 |
+
saveTokens(tokens);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export function listTokens() {
|
| 101 |
+
return loadTokens();
|
| 102 |
+
}
|
src/browser/auth.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// auth.js - Модуль для авторизации и проверки авторизации
|
| 2 |
+
import { saveSession } from './session.js';
|
| 3 |
+
import { setAuthenticationStatus, getAuthenticationStatus, restartBrowserInHeadlessMode } from './browser.js';
|
| 4 |
+
import { extractAuthToken } from '../api/chat.js';
|
| 5 |
+
|
| 6 |
+
const AUTH_URL = 'https://chat.qwen.ai/';
|
| 7 |
+
const AUTH_SIGNIN_URL = 'https://chat.qwen.ai/auth?action=signin';
|
| 8 |
+
|
| 9 |
+
const VERIFICATION_TIMEOUT = 300000;
|
| 10 |
+
|
| 11 |
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
| 12 |
+
|
| 13 |
+
async function getPage(context) {
|
| 14 |
+
if (context && typeof context.goto === 'function') {
|
| 15 |
+
return context;
|
| 16 |
+
} else if (context && typeof context.newPage === 'function') {
|
| 17 |
+
return await context.newPage();
|
| 18 |
+
} else {
|
| 19 |
+
throw new Error('Неверный контекст: не страница Puppeteer, не контекст Playwright');
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function isPlaywright(context) {
|
| 24 |
+
return context && typeof context.newPage === 'function';
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async function promptUser(question) {
|
| 28 |
+
return new Promise(resolve => {
|
| 29 |
+
process.stdout.write(question);
|
| 30 |
+
|
| 31 |
+
const onData = (data) => {
|
| 32 |
+
const input = data.toString().trim();
|
| 33 |
+
process.stdin.removeListener('data', onData);
|
| 34 |
+
process.stdin.pause();
|
| 35 |
+
resolve(input);
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
process.stdin.resume();
|
| 39 |
+
process.stdin.once('data', onData);
|
| 40 |
+
});
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export async function checkAuthentication(context) {
|
| 44 |
+
try {
|
| 45 |
+
if (getAuthenticationStatus()) {
|
| 46 |
+
return true;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const page = await getPage(context);
|
| 50 |
+
const isPW = isPlaywright(context);
|
| 51 |
+
|
| 52 |
+
console.log('Проверка авторизации...');
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
await page.goto(AUTH_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 56 |
+
|
| 57 |
+
if (isPW) {
|
| 58 |
+
await page.waitForLoadState('domcontentloaded');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
await delay(2000);
|
| 62 |
+
|
| 63 |
+
const pageTitle = await page.title();
|
| 64 |
+
const hasVerification = pageTitle.includes('Verification');
|
| 65 |
+
|
| 66 |
+
if (hasVerification) {
|
| 67 |
+
console.log('Обнаружена страница верификации. Пожалуйста, пройдите верификацию вручную.');
|
| 68 |
+
await promptUser('После прохождения верификации нажмите ENTER для продолжения...');
|
| 69 |
+
console.log('Верификация подтверждена пользователем.');
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
let loginContainerCount = 0;
|
| 73 |
+
if (isPW) {
|
| 74 |
+
loginContainerCount = await page.locator('.login-container').count();
|
| 75 |
+
} else {
|
| 76 |
+
const loginElements = await page.$$('.login-container');
|
| 77 |
+
loginContainerCount = loginElements.length;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if (loginContainerCount === 0) {
|
| 81 |
+
console.log('======================================================');
|
| 82 |
+
console.log(' АВТОРИЗАЦИЯ ОБНАРУЖЕНА ');
|
| 83 |
+
console.log('======================================================');
|
| 84 |
+
|
| 85 |
+
setAuthenticationStatus(true);
|
| 86 |
+
|
| 87 |
+
try {
|
| 88 |
+
await extractAuthToken(context, true);
|
| 89 |
+
await saveSession(context);
|
| 90 |
+
console.log('Сессия обновлена');
|
| 91 |
+
} catch (e) {
|
| 92 |
+
console.error('Не удалось обновить сессию:', e);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if (isPW) {
|
| 96 |
+
await page.close();
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return true;
|
| 100 |
+
} else {
|
| 101 |
+
console.log('------------------------------------------------------');
|
| 102 |
+
console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ ');
|
| 103 |
+
console.log('------------------------------------------------------');
|
| 104 |
+
console.log('Пожалуйста, выполните следующие действия:');
|
| 105 |
+
console.log('1. Войдите в систему через GitHub или другой способ в открытом браузере');
|
| 106 |
+
console.log('2. Дождитесь завершения процесса авторизации');
|
| 107 |
+
console.log('3. Нажмите ENTER в этой консоли');
|
| 108 |
+
console.log('------------------------------------------------------');
|
| 109 |
+
|
| 110 |
+
await promptUser('После успешной авторизации нажмите ENTER для продолжения...');
|
| 111 |
+
console.log('Пользователь подтвердил завершение авторизации.');
|
| 112 |
+
|
| 113 |
+
await page.reload({ waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 114 |
+
await delay(3000);
|
| 115 |
+
|
| 116 |
+
let loginElements = 0;
|
| 117 |
+
if (isPW) {
|
| 118 |
+
loginElements = await page.locator('.login-container').count();
|
| 119 |
+
} else {
|
| 120 |
+
const elements = await page.$$('.login-container');
|
| 121 |
+
loginElements = elements.length;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
if (loginElements === 0) {
|
| 125 |
+
console.log('Авторизация подтверждена.');
|
| 126 |
+
setAuthenticationStatus(true);
|
| 127 |
+
|
| 128 |
+
await saveSession(context);
|
| 129 |
+
await extractAuthToken(context, true);
|
| 130 |
+
|
| 131 |
+
if (isPW) {
|
| 132 |
+
await page.close();
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return true;
|
| 136 |
+
} else {
|
| 137 |
+
console.log('Предупреждение: Авторизация не обнаружена.');
|
| 138 |
+
setAuthenticationStatus(false);
|
| 139 |
+
return false;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
} catch (error) {
|
| 143 |
+
if (isPW) {
|
| 144 |
+
await page.close().catch(() => {});
|
| 145 |
+
}
|
| 146 |
+
throw error;
|
| 147 |
+
}
|
| 148 |
+
} catch (error) {
|
| 149 |
+
console.error('Ошибка при проверке авторизации:', error);
|
| 150 |
+
setAuthenticationStatus(false);
|
| 151 |
+
return false;
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
export async function startManualAuthentication(context, skipRestart = false) {
|
| 156 |
+
try {
|
| 157 |
+
const page = await getPage(context);
|
| 158 |
+
const isPW = isPlaywright(context);
|
| 159 |
+
|
| 160 |
+
console.log('Открытие страницы для ручной авторизации...');
|
| 161 |
+
|
| 162 |
+
try {
|
| 163 |
+
await page.goto(AUTH_SIGNIN_URL, { waitUntil: 'load', timeout: 120000 });
|
| 164 |
+
|
| 165 |
+
console.log('------------------------------------------------------');
|
| 166 |
+
console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ ');
|
| 167 |
+
console.log('------------------------------------------------------');
|
| 168 |
+
console.log('Пожалуйста, выполните следующие действия:');
|
| 169 |
+
console.log('1. Войдите в систему в открытом браузере');
|
| 170 |
+
console.log('2. Дождитесь завершения процесса авторизации');
|
| 171 |
+
console.log('3. Нажмите ENTER в этой консоли');
|
| 172 |
+
console.log('------------------------------------------------------');
|
| 173 |
+
|
| 174 |
+
await promptUser('После успешной авторизации нажмите ENTER для продолжения...');
|
| 175 |
+
|
| 176 |
+
await page.goto(AUTH_URL, { waitUntil: 'domcontentloaded', timeout: 120000 });
|
| 177 |
+
await delay(2000);
|
| 178 |
+
|
| 179 |
+
let loginElements = 0;
|
| 180 |
+
if (isPW) {
|
| 181 |
+
loginElements = await page.locator('.login-container').count();
|
| 182 |
+
} else {
|
| 183 |
+
const elements = await page.$$('.login-container');
|
| 184 |
+
loginElements = elements.length;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (loginElements === 0) {
|
| 188 |
+
console.log('Авторизация подтверждена.');
|
| 189 |
+
setAuthenticationStatus(true);
|
| 190 |
+
|
| 191 |
+
await saveSession(context);
|
| 192 |
+
await extractAuthToken(context, true);
|
| 193 |
+
|
| 194 |
+
console.log('Сессия сохранена успешно!');
|
| 195 |
+
|
| 196 |
+
if (isPW) {
|
| 197 |
+
await page.close();
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
if (!skipRestart) {
|
| 201 |
+
await restartBrowserInHeadlessMode();
|
| 202 |
+
}
|
| 203 |
+
return true;
|
| 204 |
+
} else {
|
| 205 |
+
console.log('Авторизация не удалась.');
|
| 206 |
+
setAuthenticationStatus(false);
|
| 207 |
+
return false;
|
| 208 |
+
}
|
| 209 |
+
} catch (error) {
|
| 210 |
+
if (isPW) {
|
| 211 |
+
await page.close().catch(() => {});
|
| 212 |
+
}
|
| 213 |
+
throw error;
|
| 214 |
+
}
|
| 215 |
+
} catch (error) {
|
| 216 |
+
console.error('Ошибка при ручной авторизации:', error);
|
| 217 |
+
setAuthenticationStatus(false);
|
| 218 |
+
return false;
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
export async function checkVerification(page) {
|
| 223 |
+
try {
|
| 224 |
+
const pageTitle = await page.title();
|
| 225 |
+
if (pageTitle.includes('Verification')) {
|
| 226 |
+
console.log('Обнаружена страница верификации');
|
| 227 |
+
await promptUser('Пройдите верификацию и нажмите ENTER...');
|
| 228 |
+
return true;
|
| 229 |
+
}
|
| 230 |
+
return false;
|
| 231 |
+
} catch (error) {
|
| 232 |
+
return false;
|
| 233 |
+
}
|
| 234 |
+
}
|
src/browser/browser.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import puppeteer from 'puppeteer-extra';
|
| 2 |
+
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
| 3 |
+
import { saveSession, loadSession, saveAuthToken } from './session.js';
|
| 4 |
+
import { checkAuthentication, startManualAuthentication } from './auth.js';
|
| 5 |
+
import { clearPagePool, getAuthToken } from '../api/chat.js';
|
| 6 |
+
import fs from 'fs';
|
| 7 |
+
import path from 'path';
|
| 8 |
+
|
| 9 |
+
puppeteer.use(StealthPlugin());
|
| 10 |
+
|
| 11 |
+
let browserInstance = null;
|
| 12 |
+
let browserContext = null;
|
| 13 |
+
|
| 14 |
+
export let isAuthenticated = false;
|
| 15 |
+
|
| 16 |
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
| 17 |
+
|
| 18 |
+
export async function initBrowser(visibleMode = true, skipManualRestart = false) {
|
| 19 |
+
if (!browserInstance) {
|
| 20 |
+
console.log('Инициализация браузера с Puppeteer Stealth...');
|
| 21 |
+
try {
|
| 22 |
+
browserInstance = await puppeteer.launch({
|
| 23 |
+
headless: !visibleMode,
|
| 24 |
+
slowMo: visibleMode ? 30 : 0,
|
| 25 |
+
executablePath: process.env.CHROME_PATH || undefined,
|
| 26 |
+
args: [
|
| 27 |
+
'--no-sandbox',
|
| 28 |
+
'--disable-setuid-sandbox',
|
| 29 |
+
'--disable-blink-features=AutomationControlled',
|
| 30 |
+
'--disable-dev-shm-usage',
|
| 31 |
+
'--disable-web-security',
|
| 32 |
+
'--disable-features=IsolateOrigins,site-per-process',
|
| 33 |
+
'--window-size=1920,1080',
|
| 34 |
+
'--start-maximized',
|
| 35 |
+
'--disable-infobars',
|
| 36 |
+
'--disable-extensions',
|
| 37 |
+
'--disable-gpu',
|
| 38 |
+
'--no-first-run',
|
| 39 |
+
'--no-default-browser-check',
|
| 40 |
+
'--ignore-certificate-errors',
|
| 41 |
+
'--ignore-certificate-errors-spki-list'
|
| 42 |
+
],
|
| 43 |
+
defaultViewport: {
|
| 44 |
+
width: 1920,
|
| 45 |
+
height: 1080
|
| 46 |
+
},
|
| 47 |
+
ignoreHTTPSErrors: true
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
const pages = await browserInstance.pages();
|
| 51 |
+
const page = pages.length > 0 ? pages[0] : await browserInstance.newPage();
|
| 52 |
+
|
| 53 |
+
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36');
|
| 54 |
+
|
| 55 |
+
await page.setViewport({
|
| 56 |
+
width: 1920,
|
| 57 |
+
height: 1080,
|
| 58 |
+
deviceScaleFactor: 1
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
await page.setExtraHTTPHeaders({
|
| 62 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 63 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 64 |
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
| 65 |
+
'Connection': 'keep-alive',
|
| 66 |
+
'Upgrade-Insecure-Requests': '1'
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
await page.evaluateOnNewDocument(() => {
|
| 70 |
+
Object.defineProperty(navigator, 'platform', {
|
| 71 |
+
get: () => 'Win32'
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
| 75 |
+
get: () => 8
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
Object.defineProperty(navigator, 'deviceMemory', {
|
| 79 |
+
get: () => 8
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
Object.defineProperty(navigator, 'plugins', {
|
| 83 |
+
get: () => [
|
| 84 |
+
{
|
| 85 |
+
0: { type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format' },
|
| 86 |
+
description: 'Portable Document Format',
|
| 87 |
+
filename: 'internal-pdf-viewer',
|
| 88 |
+
length: 1,
|
| 89 |
+
name: 'Chrome PDF Plugin'
|
| 90 |
+
}
|
| 91 |
+
]
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
Object.defineProperty(navigator, 'connection', {
|
| 95 |
+
get: () => ({
|
| 96 |
+
effectiveType: '4g',
|
| 97 |
+
rtt: 50,
|
| 98 |
+
downlink: 10,
|
| 99 |
+
saveData: false
|
| 100 |
+
})
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
if (!navigator.getBattery) {
|
| 104 |
+
navigator.getBattery = () => Promise.resolve({
|
| 105 |
+
charging: true,
|
| 106 |
+
chargingTime: 0,
|
| 107 |
+
dischargingTime: Infinity,
|
| 108 |
+
level: 1
|
| 109 |
+
});
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
| 113 |
+
EventTarget.prototype.addEventListener = function(type, listener, options) {
|
| 114 |
+
if (type === 'mousemove' || type === 'mousedown' || type === 'mouseup') {
|
| 115 |
+
const wrappedListener = function(event) {
|
| 116 |
+
const delay = Math.random() * 3;
|
| 117 |
+
setTimeout(() => {
|
| 118 |
+
listener.call(this, event);
|
| 119 |
+
}, delay);
|
| 120 |
+
};
|
| 121 |
+
return originalAddEventListener.call(this, type, wrappedListener, options);
|
| 122 |
+
}
|
| 123 |
+
return originalAddEventListener.call(this, type, listener, options);
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
| 127 |
+
HTMLCanvasElement.prototype.toDataURL = function(type) {
|
| 128 |
+
const context = this.getContext('2d');
|
| 129 |
+
if (context) {
|
| 130 |
+
const imageData = context.getImageData(0, 0, this.width, this.height);
|
| 131 |
+
const data = imageData.data;
|
| 132 |
+
for (let i = 0; i < data.length; i += 4) {
|
| 133 |
+
const noise = Math.floor(Math.random() * 5) - 2;
|
| 134 |
+
data[i] = Math.max(0, Math.min(255, data[i] + noise));
|
| 135 |
+
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
|
| 136 |
+
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
|
| 137 |
+
}
|
| 138 |
+
context.putImageData(imageData, 0, 0);
|
| 139 |
+
}
|
| 140 |
+
return originalToDataURL.apply(this, arguments);
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
console.log('Puppeteer Stealth активирован');
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
browserContext = page;
|
| 147 |
+
|
| 148 |
+
console.log('Браузер инициализирован с максимальной защитой от обнаружения');
|
| 149 |
+
|
| 150 |
+
if (visibleMode) {
|
| 151 |
+
await startManualAuthenticationPuppeteer(page, skipManualRestart);
|
| 152 |
+
} else {
|
| 153 |
+
const sessionLoaded = await loadSessionPuppeteer(page);
|
| 154 |
+
if (sessionLoaded) {
|
| 155 |
+
setAuthenticationStatus(true);
|
| 156 |
+
console.log('Сессия успешно загружена');
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
return true;
|
| 161 |
+
} catch (error) {
|
| 162 |
+
console.error('Ошибка при инициализации браузера:', error);
|
| 163 |
+
return false;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
return true;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
async function saveSessionPuppeteer(page) {
|
| 170 |
+
try {
|
| 171 |
+
const cookies = await page.cookies();
|
| 172 |
+
|
| 173 |
+
const sessionDir = path.join(process.cwd(), 'session', 'accounts');
|
| 174 |
+
if (!fs.existsSync(sessionDir)) {
|
| 175 |
+
fs.mkdirSync(sessionDir, { recursive: true });
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
const accountId = `acc_${Date.now()}`;
|
| 179 |
+
const accountDir = path.join(sessionDir, accountId);
|
| 180 |
+
|
| 181 |
+
if (!fs.existsSync(accountDir)) {
|
| 182 |
+
fs.mkdirSync(accountDir, { recursive: true });
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
fs.writeFileSync(
|
| 186 |
+
path.join(accountDir, 'cookies.json'),
|
| 187 |
+
JSON.stringify(cookies, null, 2)
|
| 188 |
+
);
|
| 189 |
+
|
| 190 |
+
console.log(`Cookies сохранены для аккаунта ${accountId}`);
|
| 191 |
+
return accountId;
|
| 192 |
+
|
| 193 |
+
} catch (error) {
|
| 194 |
+
console.error('Ошибка при сохранении сессии:', error);
|
| 195 |
+
return null;
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
async function startManualAuthenticationPuppeteer(page, skipManualRestart) {
|
| 200 |
+
try {
|
| 201 |
+
console.log('Открытие страницы для ручной авторизации...');
|
| 202 |
+
|
| 203 |
+
await page.goto('https://chat.qwen.ai/', {
|
| 204 |
+
waitUntil: 'networkidle2',
|
| 205 |
+
timeout: 60000
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
await delay(5000);
|
| 209 |
+
|
| 210 |
+
console.log('------------------------------------------------------');
|
| 211 |
+
console.log(' НЕОБХОДИМА АВТОРИЗАЦИЯ');
|
| 212 |
+
console.log('------------------------------------------------------');
|
| 213 |
+
console.log('Пожалуйста, выполните следующие действия:');
|
| 214 |
+
console.log('1. Войдите в систему в открытом браузере');
|
| 215 |
+
console.log('2. ВАЖНО: Двигайте мышью естественно, не спешите');
|
| 216 |
+
console.log('3. Если появится слайдер капчи - решите её медленно');
|
| 217 |
+
console.log('4. Дождитесь полной загрузки главной страницы');
|
| 218 |
+
console.log('5. После успешной авторизации нажмите ENTER в консоли');
|
| 219 |
+
console.log('------------------------------------------------------');
|
| 220 |
+
console.log('После успешной авторизации нажмите ENTER для продолжения...');
|
| 221 |
+
|
| 222 |
+
await new Promise((resolve) => {
|
| 223 |
+
if (process.stdin.isTTY) {
|
| 224 |
+
process.stdin.setRawMode(false);
|
| 225 |
+
}
|
| 226 |
+
process.stdin.resume();
|
| 227 |
+
process.stdin.setEncoding('utf8');
|
| 228 |
+
|
| 229 |
+
const onData = (key) => {
|
| 230 |
+
if (key === '\n' || key === '\r' || key.charCodeAt(0) === 13) {
|
| 231 |
+
process.stdin.pause();
|
| 232 |
+
process.stdin.removeListener('data', onData);
|
| 233 |
+
console.log('\nПолучено подтверждение, продолжаем...');
|
| 234 |
+
resolve();
|
| 235 |
+
}
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
process.stdin.on('data', onData);
|
| 239 |
+
});
|
| 240 |
+
|
| 241 |
+
const cookies = await page.cookies();
|
| 242 |
+
console.log(`Сохранено ${cookies.length} cookies`);
|
| 243 |
+
|
| 244 |
+
const token = await page.evaluate(() => {
|
| 245 |
+
return localStorage.getItem('token') ||
|
| 246 |
+
localStorage.getItem('auth_token') ||
|
| 247 |
+
localStorage.getItem('access_token') ||
|
| 248 |
+
sessionStorage.getItem('token') ||
|
| 249 |
+
sessionStorage.getItem('auth_token') ||
|
| 250 |
+
null;
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
if (token) {
|
| 254 |
+
console.log('Токен найден и будет сохранен');
|
| 255 |
+
saveAuthToken(token);
|
| 256 |
+
} else {
|
| 257 |
+
console.log('Токен не найден в localStorage/sessionStorage');
|
| 258 |
+
console.log('Попытка извлечь токен из cookies...');
|
| 259 |
+
|
| 260 |
+
const tokenCookie = cookies.find(c =>
|
| 261 |
+
c.name.toLowerCase().includes('token') ||
|
| 262 |
+
c.name.toLowerCase().includes('auth')
|
| 263 |
+
);
|
| 264 |
+
|
| 265 |
+
if (tokenCookie) {
|
| 266 |
+
console.log(`Токен найден в cookie: ${tokenCookie.name}`);
|
| 267 |
+
saveAuthToken(tokenCookie.value);
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
const accountId = await saveSessionPuppeteer(page);
|
| 272 |
+
if (accountId) {
|
| 273 |
+
console.log(`Сессия сохранена с ID: ${accountId}`);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
setAuthenticationStatus(true);
|
| 277 |
+
console.log('Авторизация завершена успешно');
|
| 278 |
+
|
| 279 |
+
if (!skipManualRestart) {
|
| 280 |
+
await restartBrowserInHeadlessMode();
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
} catch (error) {
|
| 284 |
+
console.error('Ошибка при ручной авторизации:', error);
|
| 285 |
+
throw error;
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
async function loadSessionPuppeteer(page) {
|
| 290 |
+
try {
|
| 291 |
+
return false;
|
| 292 |
+
} catch (error) {
|
| 293 |
+
console.error('Ошибка при загрузке сессии:', error);
|
| 294 |
+
return false;
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
export async function restartBrowserInHeadlessMode() {
|
| 299 |
+
console.log('Перезапуск браузера в фоновом режиме...');
|
| 300 |
+
|
| 301 |
+
const token = getAuthToken();
|
| 302 |
+
if (token) {
|
| 303 |
+
console.log('Сохранение токена...');
|
| 304 |
+
saveAuthToken(token);
|
| 305 |
+
await delay(1000);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
await shutdownBrowser();
|
| 309 |
+
await delay(2000);
|
| 310 |
+
|
| 311 |
+
const success = await initBrowser(false);
|
| 312 |
+
|
| 313 |
+
if (success) {
|
| 314 |
+
console.log('Браузер перезапущен в фоновом режиме');
|
| 315 |
+
} else {
|
| 316 |
+
console.error('Ошибка при перезапуске браузера');
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
export async function shutdownBrowser() {
|
| 321 |
+
try {
|
| 322 |
+
// Сначала очищаем пул страниц
|
| 323 |
+
try {
|
| 324 |
+
await clearPagePool();
|
| 325 |
+
} catch (e) {
|
| 326 |
+
console.error('Ошибка при очистке пула страниц:', e);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// Закрываем контекст браузера
|
| 330 |
+
if (browserInstance) {
|
| 331 |
+
try {
|
| 332 |
+
const pages = await browserInstance.pages();
|
| 333 |
+
for (const page of pages) {
|
| 334 |
+
await page.close().catch(() => {});
|
| 335 |
+
}
|
| 336 |
+
await browserInstance.close();
|
| 337 |
+
} catch (e) {
|
| 338 |
+
// Игнорируем ошибку, если контекст уже закрыт
|
| 339 |
+
console.error('Ошибка при закрытии браузера:', e);
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Сбрасываем переменные
|
| 344 |
+
browserContext = null;
|
| 345 |
+
browserInstance = null;
|
| 346 |
+
|
| 347 |
+
console.log('Браузер закрыт');
|
| 348 |
+
} catch (error) {
|
| 349 |
+
console.error('Ошибка при завершении работы браузера:', error);
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
export function getBrowserContext() {
|
| 354 |
+
return browserContext;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Установить статус авторизации
|
| 358 |
+
export function setAuthenticationStatus(status) {
|
| 359 |
+
isAuthenticated = status;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
// Получить статус авторизации
|
| 363 |
+
export function getAuthenticationStatus() {
|
| 364 |
+
return isAuthenticated;
|
| 365 |
+
}
|
src/browser/session.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
+
|
| 5 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
+
const __dirname = path.dirname(__filename);
|
| 7 |
+
|
| 8 |
+
const SESSION_DIR = path.join(__dirname, '..', '..', 'session');
|
| 9 |
+
const TOKEN_FILE = path.join(SESSION_DIR, 'auth_token.txt');
|
| 10 |
+
|
| 11 |
+
export function initSessionDirectory() {
|
| 12 |
+
if (!fs.existsSync(SESSION_DIR)) {
|
| 13 |
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
| 14 |
+
console.log(`Создана директория для сессий: ${SESSION_DIR}`);
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export async function saveSession(context, accountId = null) {
|
| 19 |
+
try {
|
| 20 |
+
initSessionDirectory();
|
| 21 |
+
|
| 22 |
+
const isPuppeteer = context && typeof context.goto === 'function';
|
| 23 |
+
const isPlaywright = context && typeof context.storageState === 'function';
|
| 24 |
+
|
| 25 |
+
if (isPuppeteer) {
|
| 26 |
+
const cookies = await context.cookies();
|
| 27 |
+
|
| 28 |
+
const sessionPath = accountId
|
| 29 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'cookies.json')
|
| 30 |
+
: path.join(SESSION_DIR, 'cookies.json');
|
| 31 |
+
|
| 32 |
+
const sessionDir = path.dirname(sessionPath);
|
| 33 |
+
if (!fs.existsSync(sessionDir)) {
|
| 34 |
+
fs.mkdirSync(sessionDir, { recursive: true });
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
fs.writeFileSync(sessionPath, JSON.stringify(cookies, null, 2));
|
| 38 |
+
|
| 39 |
+
console.log('Сессия Puppeteer сохранена');
|
| 40 |
+
return true;
|
| 41 |
+
|
| 42 |
+
} else if (isPlaywright && context.browser()) {
|
| 43 |
+
const sessionPath = accountId
|
| 44 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'state.json')
|
| 45 |
+
: path.join(SESSION_DIR, 'state.json');
|
| 46 |
+
|
| 47 |
+
const sessionDir = path.dirname(sessionPath);
|
| 48 |
+
if (!fs.existsSync(sessionDir)) {
|
| 49 |
+
fs.mkdirSync(sessionDir, { recursive: true });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
await context.storageState({ path: sessionPath });
|
| 53 |
+
console.log('Сессия Playwright сохранена');
|
| 54 |
+
return true;
|
| 55 |
+
} else {
|
| 56 |
+
console.error('Неизвестный тип контекста браузера');
|
| 57 |
+
return false;
|
| 58 |
+
}
|
| 59 |
+
} catch (error) {
|
| 60 |
+
console.error('Ошибка при сохранении сессии:', error);
|
| 61 |
+
return false;
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export async function loadSession(context, accountId = null) {
|
| 66 |
+
try {
|
| 67 |
+
const isPuppeteer = context && typeof context.goto === 'function';
|
| 68 |
+
const isPlaywright = context && typeof context.storageState === 'function';
|
| 69 |
+
|
| 70 |
+
if (isPuppeteer) {
|
| 71 |
+
const sessionPath = accountId
|
| 72 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'cookies.json')
|
| 73 |
+
: path.join(SESSION_DIR, 'cookies.json');
|
| 74 |
+
|
| 75 |
+
if (fs.existsSync(sessionPath)) {
|
| 76 |
+
const cookies = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
| 77 |
+
await context.setCookie(...cookies);
|
| 78 |
+
console.log('Сессия Puppeteer загружена');
|
| 79 |
+
return true;
|
| 80 |
+
}
|
| 81 |
+
} else if (isPlaywright) {
|
| 82 |
+
const sessionPath = accountId
|
| 83 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'state.json')
|
| 84 |
+
: path.join(SESSION_DIR, 'state.json');
|
| 85 |
+
|
| 86 |
+
if (fs.existsSync(sessionPath)) {
|
| 87 |
+
await context.storageState({ path: sessionPath });
|
| 88 |
+
console.log('Сессия Playwright загружена');
|
| 89 |
+
return true;
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
} catch (error) {
|
| 93 |
+
console.error('Ошибка при загрузке сессии:', error);
|
| 94 |
+
}
|
| 95 |
+
return false;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
export function clearSession(accountId = null) {
|
| 99 |
+
try {
|
| 100 |
+
const sessionPaths = [
|
| 101 |
+
accountId
|
| 102 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'state.json')
|
| 103 |
+
: path.join(SESSION_DIR, 'state.json'),
|
| 104 |
+
accountId
|
| 105 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'cookies.json')
|
| 106 |
+
: path.join(SESSION_DIR, 'cookies.json')
|
| 107 |
+
];
|
| 108 |
+
|
| 109 |
+
let cleared = false;
|
| 110 |
+
for (const sessionPath of sessionPaths) {
|
| 111 |
+
if (fs.existsSync(sessionPath)) {
|
| 112 |
+
fs.unlinkSync(sessionPath);
|
| 113 |
+
cleared = true;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
if (cleared) {
|
| 118 |
+
console.log('Сессия очищена');
|
| 119 |
+
return true;
|
| 120 |
+
}
|
| 121 |
+
} catch (error) {
|
| 122 |
+
console.error('Ошибка при очистке сессии:', error);
|
| 123 |
+
}
|
| 124 |
+
return false;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export function hasSession(accountId = null) {
|
| 128 |
+
const sessionPaths = [
|
| 129 |
+
accountId
|
| 130 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'state.json')
|
| 131 |
+
: path.join(SESSION_DIR, 'state.json'),
|
| 132 |
+
accountId
|
| 133 |
+
? path.join(SESSION_DIR, 'accounts', accountId, 'cookies.json')
|
| 134 |
+
: path.join(SESSION_DIR, 'cookies.json')
|
| 135 |
+
];
|
| 136 |
+
|
| 137 |
+
return sessionPaths.some(path => fs.existsSync(path));
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
export function saveAuthToken(token) {
|
| 141 |
+
try {
|
| 142 |
+
initSessionDirectory();
|
| 143 |
+
|
| 144 |
+
if (token) {
|
| 145 |
+
fs.writeFileSync(TOKEN_FILE, token, 'utf8');
|
| 146 |
+
console.log('Токен авторизации сохранен');
|
| 147 |
+
return true;
|
| 148 |
+
}
|
| 149 |
+
} catch (error) {
|
| 150 |
+
console.error('Ошибка при сохранении токена авторизации:', error);
|
| 151 |
+
}
|
| 152 |
+
return false;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
export function loadAuthToken() {
|
| 156 |
+
try {
|
| 157 |
+
if (fs.existsSync(TOKEN_FILE)) {
|
| 158 |
+
const token = fs.readFileSync(TOKEN_FILE, 'utf8');
|
| 159 |
+
console.log('Токен авторизации загружен');
|
| 160 |
+
return token;
|
| 161 |
+
}
|
| 162 |
+
} catch (error) {
|
| 163 |
+
console.error('Ошибка при загрузке токена авторизации:', error);
|
| 164 |
+
}
|
| 165 |
+
return null;
|
| 166 |
+
}
|
src/logger/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import winston from 'winston';
|
| 2 |
+
import morgan from 'morgan';
|
| 3 |
+
import fs from 'fs';
|
| 4 |
+
import path from 'path';
|
| 5 |
+
import { fileURLToPath } from 'url';
|
| 6 |
+
|
| 7 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 8 |
+
const __dirname = path.dirname(__filename);
|
| 9 |
+
|
| 10 |
+
// Создаем директорию для логов, если она не существует
|
| 11 |
+
const LOG_DIR = path.join(__dirname, '..', '..', 'logs');
|
| 12 |
+
if (!fs.existsSync(LOG_DIR)) {
|
| 13 |
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Настройки форматирования логов
|
| 17 |
+
const { combine, timestamp, printf, colorize } = winston.format;
|
| 18 |
+
|
| 19 |
+
// Формат для консоли (цветной)
|
| 20 |
+
const consoleFormat = combine(
|
| 21 |
+
colorize({ all: true }),
|
| 22 |
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
| 23 |
+
printf(({ level, message, timestamp }) => {
|
| 24 |
+
return `${timestamp} [${level}]: ${message}`;
|
| 25 |
+
})
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
// Формат для файла (без цветов)
|
| 29 |
+
const fileFormat = combine(
|
| 30 |
+
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
| 31 |
+
printf(({ level, message, timestamp }) => {
|
| 32 |
+
return `${timestamp} [${level}]: ${message}`;
|
| 33 |
+
})
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
const customLevels = {
|
| 38 |
+
levels: {
|
| 39 |
+
error: 0,
|
| 40 |
+
warn: 1,
|
| 41 |
+
info: 2,
|
| 42 |
+
http: 3,
|
| 43 |
+
debug: 4,
|
| 44 |
+
raw: 5
|
| 45 |
+
},
|
| 46 |
+
colors: {
|
| 47 |
+
error: 'red',
|
| 48 |
+
warn: 'yellow',
|
| 49 |
+
info: 'green',
|
| 50 |
+
http: 'cyan',
|
| 51 |
+
debug: 'blue',
|
| 52 |
+
raw: 'magenta'
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
// Определяем уровень логирования на основе окружения
|
| 57 |
+
const level = 'raw';
|
| 58 |
+
|
| 59 |
+
// Создаем инстанс логгера
|
| 60 |
+
const logger = winston.createLogger({
|
| 61 |
+
levels: customLevels.levels,
|
| 62 |
+
level,
|
| 63 |
+
format: fileFormat,
|
| 64 |
+
transports: [
|
| 65 |
+
// Лог всех сообщений уровня info и выше в combined.log
|
| 66 |
+
new winston.transports.File({
|
| 67 |
+
filename: path.join(LOG_DIR, 'combined.log'),
|
| 68 |
+
maxsize: 5242880, // 5MB
|
| 69 |
+
maxFiles: 5
|
| 70 |
+
}),
|
| 71 |
+
// Отдельный файл для HTTP-запросов
|
| 72 |
+
new winston.transports.File({
|
| 73 |
+
filename: path.join(LOG_DIR, 'http.log'),
|
| 74 |
+
level: 'http',
|
| 75 |
+
maxsize: 5242880, // 5MB
|
| 76 |
+
maxFiles: 5
|
| 77 |
+
}),
|
| 78 |
+
// Лог всех ошибок в error.log
|
| 79 |
+
new winston.transports.File({
|
| 80 |
+
filename: path.join(LOG_DIR, 'error.log'),
|
| 81 |
+
level: 'error',
|
| 82 |
+
maxsize: 5242880, // 5MB
|
| 83 |
+
maxFiles: 5
|
| 84 |
+
}),
|
| 85 |
+
// Файл для сырых ответов нейросети
|
| 86 |
+
new winston.transports.File({
|
| 87 |
+
filename: path.join(LOG_DIR, 'raw-responses.log'),
|
| 88 |
+
level: 'raw',
|
| 89 |
+
maxsize: 5242880, // 5MB
|
| 90 |
+
maxFiles: 5
|
| 91 |
+
}),
|
| 92 |
+
// Вывод в консоль
|
| 93 |
+
new winston.transports.Console({
|
| 94 |
+
format: consoleFormat
|
| 95 |
+
})
|
| 96 |
+
]
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// Добавляем цвета для уровней логирования
|
| 100 |
+
winston.addColors(customLevels.colors);
|
| 101 |
+
|
| 102 |
+
// Создаем stream для morgan, который будет писать в winston
|
| 103 |
+
const morganStream = {
|
| 104 |
+
write: (message) => {
|
| 105 |
+
// Убираем символ новой строки и отправляем в http-логи
|
| 106 |
+
logger.http(message.trim());
|
| 107 |
+
}
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
// Настраиваем формат morgan с дополнительной информацией
|
| 111 |
+
const morganFormat = ':remote-addr :method :url :status :res[content-length] - :response-time ms';
|
| 112 |
+
|
| 113 |
+
// Создаем middleware для express с использованием morgan
|
| 114 |
+
const httpLogger = morgan(morganFormat, { stream: morganStream });
|
| 115 |
+
|
| 116 |
+
// Отдельная функция для логирования HTTP-запросов (используется morgan)
|
| 117 |
+
export const logHttpRequest = httpLogger;
|
| 118 |
+
|
| 119 |
+
// Экспортируем функции для разных уровней логирования
|
| 120 |
+
export const logInfo = (message) => logger.info(message);
|
| 121 |
+
export const logError = (message, error) => {
|
| 122 |
+
if (error) {
|
| 123 |
+
logger.error(`${message}: ${error.message}`);
|
| 124 |
+
logger.error(error.stack);
|
| 125 |
+
} else {
|
| 126 |
+
logger.error(message);
|
| 127 |
+
}
|
| 128 |
+
};
|
| 129 |
+
export const logWarn = (message) => logger.warn(message);
|
| 130 |
+
export const logDebug = (message) => logger.debug(message);
|
| 131 |
+
export const logRaw = (message) => logger.raw(message);
|
| 132 |
+
export const logHttp = (message) => logger.http(message);
|
| 133 |
+
|
| 134 |
+
export default {
|
| 135 |
+
logHttpRequest,
|
| 136 |
+
logInfo,
|
| 137 |
+
logError,
|
| 138 |
+
logWarn,
|
| 139 |
+
logDebug,
|
| 140 |
+
logRaw,
|
| 141 |
+
logHttp
|
| 142 |
+
};
|
src/utils/accountSetup.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import readline from 'readline';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
|
| 6 |
+
import { initBrowser, shutdownBrowser, getBrowserContext } from '../browser/browser.js';
|
| 7 |
+
import { extractAuthToken } from '../api/chat.js';
|
| 8 |
+
import { loadTokens, saveTokens, markValid, removeToken } from '../api/tokenManager.js';
|
| 9 |
+
import { loadAuthToken } from '../browser/session.js';
|
| 10 |
+
|
| 11 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 12 |
+
const __dirname = path.dirname(__filename);
|
| 13 |
+
|
| 14 |
+
function prompt(question) {
|
| 15 |
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
| 16 |
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function ensureAccountDir(id) {
|
| 20 |
+
const accountDir = path.join(__dirname, '..', '..', 'session', 'accounts', id);
|
| 21 |
+
if (!fs.existsSync(accountDir)) {
|
| 22 |
+
fs.mkdirSync(accountDir, { recursive: true });
|
| 23 |
+
}
|
| 24 |
+
return accountDir;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export async function addAccountInteractive() {
|
| 28 |
+
console.log('======================================================');
|
| 29 |
+
console.log('Добавление нового аккаунта Qwen');
|
| 30 |
+
console.log('Браузер откроется, войдите в систему, затем вернитесь к консоли.');
|
| 31 |
+
console.log('======================================================');
|
| 32 |
+
|
| 33 |
+
const ok = await initBrowser(true, true);
|
| 34 |
+
if (!ok) {
|
| 35 |
+
console.error('Не удалось запустить браузер.');
|
| 36 |
+
return null;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
const ctx = getBrowserContext();
|
| 42 |
+
let token = await extractAuthToken(ctx, true);
|
| 43 |
+
|
| 44 |
+
if (!token) {
|
| 45 |
+
token = loadAuthToken();
|
| 46 |
+
if (token) {
|
| 47 |
+
console.log('Токен получен из сохранённого файла.');
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (!token) {
|
| 52 |
+
console.error('Токен не был получен. Аккаунт не добавлен.');
|
| 53 |
+
await shutdownBrowser();
|
| 54 |
+
return null;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
await shutdownBrowser();
|
| 58 |
+
// ---
|
| 59 |
+
|
| 60 |
+
const id = 'acc_' + Date.now();
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
ensureAccountDir(id);
|
| 64 |
+
fs.writeFileSync(path.join(__dirname, '..', '..', 'session', 'accounts', id, 'token.txt'), token, 'utf8');
|
| 65 |
+
|
| 66 |
+
const list = loadTokens();
|
| 67 |
+
list.push({ id, token, resetAt: null });
|
| 68 |
+
saveTokens(list);
|
| 69 |
+
|
| 70 |
+
console.log(`Аккаунт '${id}' добавлен. Всего аккаунтов: ${list.length}`);
|
| 71 |
+
console.log('======================================================');
|
| 72 |
+
return id;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export async function interactiveAccountMenu() {
|
| 76 |
+
while (true) {
|
| 77 |
+
console.log('\n=== Меню управления аккаунтами ===');
|
| 78 |
+
console.log('1 - Добавить новый аккаунт');
|
| 79 |
+
console.log('2 - Завершить');
|
| 80 |
+
const choice = await prompt('Ваш выбор (1/2): ');
|
| 81 |
+
if (choice === '1') {
|
| 82 |
+
await addAccountInteractive();
|
| 83 |
+
} else if (choice === '2') {
|
| 84 |
+
break;
|
| 85 |
+
} else {
|
| 86 |
+
console.log('Неверный выбор.');
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export async function reloginAccountInteractive() {
|
| 92 |
+
const tokens = loadTokens();
|
| 93 |
+
const invalids = tokens.filter(t => t.invalid);
|
| 94 |
+
if (!invalids.length) {
|
| 95 |
+
console.log('Нет аккаунтов, требующих повторного входа.');
|
| 96 |
+
await prompt('Нажмите ENTER чтобы вернуться в меню...');
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
console.log('\nАккаунты с истекшим токеном:');
|
| 101 |
+
invalids.forEach((t, idx) => console.log(`${idx + 1} - ${t.id}`));
|
| 102 |
+
const choice = await prompt('Выберите номер аккаунта для повторного входа: ');
|
| 103 |
+
const num = parseInt(choice, 10);
|
| 104 |
+
if (isNaN(num) || num < 1 || num > invalids.length) {
|
| 105 |
+
console.log('Неверный выбор.');
|
| 106 |
+
return;
|
| 107 |
+
}
|
| 108 |
+
const account = invalids[num - 1];
|
| 109 |
+
|
| 110 |
+
console.log(`\nПовторная авторизация для ${account.id}`);
|
| 111 |
+
const ok = await initBrowser(true, true);
|
| 112 |
+
if (!ok) {
|
| 113 |
+
console.error('Не удалось запустить браузер.');
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
const token = await extractAuthToken(getBrowserContext(), true);
|
| 118 |
+
await shutdownBrowser();
|
| 119 |
+
|
| 120 |
+
if (!token) {
|
| 121 |
+
console.error('Не удалось извлечь токен.');
|
| 122 |
+
return;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// сохраняем новый токен и снимаем invalid
|
| 126 |
+
markValid(account.id, token);
|
| 127 |
+
fs.writeFileSync(path.join(__dirname, '..', '..', 'session', 'accounts', account.id, 'token.txt'), token, 'utf8');
|
| 128 |
+
|
| 129 |
+
console.log(`Токен обновлён для ${account.id}`);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export async function removeAccountInteractive() {
|
| 133 |
+
const tokens = loadTokens();
|
| 134 |
+
if (!tokens.length) {
|
| 135 |
+
console.log('Нет сохранённых аккаунтов.');
|
| 136 |
+
await prompt('ENTER чтобы вернуться...');
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
console.log('\nДоступные аккаунты:');
|
| 141 |
+
tokens.forEach((t, idx) => console.log(`${idx + 1} - ${t.id}`));
|
| 142 |
+
const choice = await prompt('Номер аккаунта, который нужно удалить (или ENTER для отмены): ');
|
| 143 |
+
if (!choice) return;
|
| 144 |
+
const num = parseInt(choice, 10);
|
| 145 |
+
if (isNaN(num) || num < 1 || num > tokens.length) {
|
| 146 |
+
console.log('Неверный выбор.');
|
| 147 |
+
await prompt('ENTER чтобы вернуться...');
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
const acc = tokens[num - 1];
|
| 151 |
+
const confirm = await prompt(`Точно удалить ${acc.id}? (y/N): `);
|
| 152 |
+
if (confirm.toLowerCase() !== 'y') return;
|
| 153 |
+
|
| 154 |
+
removeToken(acc.id);
|
| 155 |
+
|
| 156 |
+
// удалить директорию аккаунта
|
| 157 |
+
const dir = path.join(__dirname, '..', '..', 'session', 'accounts', acc.id);
|
| 158 |
+
if (fs.existsSync(dir)) {
|
| 159 |
+
fs.rmSync(dir, { recursive: true, force: true });
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
console.log(`Аккаунт ${acc.id} удалён.`);
|
| 163 |
+
await prompt('ENTER чтобы вернуться...');
|
| 164 |
+
}
|
start.bat
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
chcp 65001 >nul
|
| 3 |
+
title Запуск Qwen API сервера
|
| 4 |
+
|
| 5 |
+
echo Проверка наличия Node.js...
|
| 6 |
+
where node >nul 2>nul
|
| 7 |
+
if %ERRORLEVEL% neq 0 (
|
| 8 |
+
echo [ОШИБКА] Node.js не установлен!
|
| 9 |
+
echo Пожалуйста, установите Node.js с сайта https://nodejs.org/
|
| 10 |
+
pause
|
| 11 |
+
exit /b 1
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
echo Проверка наличия npm...
|
| 15 |
+
where npm >nul 2>nul
|
| 16 |
+
if %ERRORLEVEL% neq 0 (
|
| 17 |
+
echo [ОШИБКА] npm не установлен!
|
| 18 |
+
echo Пожалуйста, переустановите Node.js с сайта https://nodejs.org/
|
| 19 |
+
pause
|
| 20 |
+
exit /b 1
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
echo Установка зависимостей...
|
| 24 |
+
call npm install
|
| 25 |
+
|
| 26 |
+
if %ERRORLEVEL% neq 0 (
|
| 27 |
+
echo [ОШИБКА] Не удалось установить зависимости!
|
| 28 |
+
pause
|
| 29 |
+
exit /b 1
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
echo.
|
| 33 |
+
echo Запуск приложения...
|
| 34 |
+
echo.
|
| 35 |
+
|
| 36 |
+
:: Запуск Node.js приложения
|
| 37 |
+
node index.js
|
| 38 |
+
|
| 39 |
+
pause
|