imseldrith commited on
Commit
9de864e
·
verified ·
1 Parent(s): feb457d

Initial upload from Google Colab

Browse files
.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