qwen-api-proxy 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +2054 -0
- package/bin/qwen-api-proxy.js +414 -0
- package/index.js +444 -0
- package/package.json +85 -0
- package/src/Authorization.txt +17 -0
- package/src/AvailableModels.txt +26 -0
- package/src/api/chat.js +1392 -0
- package/src/api/chatHistory.js +344 -0
- package/src/api/fileUpload.js +182 -0
- package/src/api/imageGeneration.js +459 -0
- package/src/api/modelMapping.js +274 -0
- package/src/api/routes.js +2160 -0
- package/src/api/tokenManager.js +382 -0
- package/src/browser/auth.js +171 -0
- package/src/browser/browser.js +233 -0
- package/src/browser/session.js +134 -0
- package/src/config.js +116 -0
- package/src/logger/index.js +89 -0
- package/src/utils/accountSetup.js +153 -0
- package/src/utils/botSettings.js +231 -0
- package/src/utils/permissionChecker.js +205 -0
- package/src/utils/prompt.js +11 -0
- package/src/utils/proxy.js +255 -0
- package/src/utils/telegramBot.js +2977 -0
- package/src/utils/telegramNotifier.js +94 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
import { initBrowser, shutdownBrowser, getBrowserContext } from '../browser/browser.js';
|
|
6
|
+
import { extractAuthToken } from '../api/chat.js';
|
|
7
|
+
import { loadTokens, saveTokens, markValid, removeToken } from '../api/tokenManager.js';
|
|
8
|
+
import { loadAuthToken } from '../browser/session.js';
|
|
9
|
+
import { logInfo, logError, logWarn } from '../logger/index.js';
|
|
10
|
+
import { prompt } from './prompt.js';
|
|
11
|
+
import { SESSION_DIR, ACCOUNTS_DIR } from '../config.js';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
function ensureAccountDir(id) {
|
|
16
|
+
const accountDir = path.resolve(__dirname, '..', '..', SESSION_DIR, ACCOUNTS_DIR, id);
|
|
17
|
+
if (!fs.existsSync(accountDir)) fs.mkdirSync(accountDir, { recursive: true });
|
|
18
|
+
return accountDir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function addAccountInteractive() {
|
|
22
|
+
logInfo('======================================================');
|
|
23
|
+
logInfo('Добавление нового аккаунта Qwen');
|
|
24
|
+
logInfo('Браузер откроется, войдите в систему, затем вернитесь к консоли.');
|
|
25
|
+
logInfo('======================================================');
|
|
26
|
+
|
|
27
|
+
const ok = await initBrowser(true, true);
|
|
28
|
+
if (!ok) {
|
|
29
|
+
logError('Не удалось запустить браузер.');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ctx = getBrowserContext();
|
|
34
|
+
let token = await extractAuthToken(ctx, true);
|
|
35
|
+
|
|
36
|
+
if (!token) {
|
|
37
|
+
token = loadAuthToken();
|
|
38
|
+
if (token) logInfo('Токен получен из сохранённого файла.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!token) {
|
|
42
|
+
logError('Токен не был получен. Аккаунт не добавлен.');
|
|
43
|
+
await shutdownBrowser();
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await shutdownBrowser();
|
|
48
|
+
|
|
49
|
+
const id = 'acc_' + Date.now();
|
|
50
|
+
ensureAccountDir(id);
|
|
51
|
+
fs.writeFileSync(path.resolve(__dirname, '..', '..', SESSION_DIR, ACCOUNTS_DIR, id, 'token.txt'), token, 'utf8');
|
|
52
|
+
|
|
53
|
+
const list = loadTokens();
|
|
54
|
+
list.push({ id, token, resetAt: null });
|
|
55
|
+
saveTokens(list);
|
|
56
|
+
|
|
57
|
+
logInfo(`Аккаунт '${id}' добавлен. Всего аккаунтов: ${list.length}`);
|
|
58
|
+
logInfo('======================================================');
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function interactiveAccountMenu() {
|
|
63
|
+
while (true) {
|
|
64
|
+
console.log('\n=== Меню управления аккаунтами ===');
|
|
65
|
+
console.log('1 - Добавить новый аккаунт');
|
|
66
|
+
console.log('2 - Завершить');
|
|
67
|
+
const choice = await prompt('Ваш выбор (1/2): ');
|
|
68
|
+
if (choice === '1') await addAccountInteractive();
|
|
69
|
+
else if (choice === '2') break;
|
|
70
|
+
else console.log('Неверный выбор.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function reloginAccountInteractive() {
|
|
75
|
+
const tokens = loadTokens();
|
|
76
|
+
const invalids = tokens.filter(t => t.invalid);
|
|
77
|
+
if (!invalids.length) {
|
|
78
|
+
console.log('Нет аккаунтов, требующих повторного входа.');
|
|
79
|
+
await prompt('Нажмите ENTER чтобы вернуться в меню...');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('\nАккаунты с истекшим токеном:');
|
|
84
|
+
invalids.forEach((t, idx) => console.log(`${idx + 1} - ${t.id}`));
|
|
85
|
+
const choice = await prompt('Выберите номер аккаунта для повторного входа: ');
|
|
86
|
+
const num = parseInt(choice, 10);
|
|
87
|
+
if (isNaN(num) || num < 1 || num > invalids.length) {
|
|
88
|
+
console.log('Неверный выбор.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const account = invalids[num - 1];
|
|
92
|
+
|
|
93
|
+
logInfo(`Повторная авторизация для ${account.id}`);
|
|
94
|
+
const ok = await initBrowser(true, true);
|
|
95
|
+
if (!ok) { logError('Не удалось запустить браузер.'); return; }
|
|
96
|
+
|
|
97
|
+
const token = await extractAuthToken(getBrowserContext(), true);
|
|
98
|
+
await shutdownBrowser();
|
|
99
|
+
|
|
100
|
+
if (!token) { logError('Не удалось извлечь токен.'); return; }
|
|
101
|
+
|
|
102
|
+
markValid(account.id, token);
|
|
103
|
+
fs.writeFileSync(path.resolve(__dirname, '..', '..', SESSION_DIR, ACCOUNTS_DIR, account.id, 'token.txt'), token, 'utf8');
|
|
104
|
+
logInfo(`Токен обновлён для ${account.id}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function removeAccountInteractive() {
|
|
108
|
+
const tokens = loadTokens();
|
|
109
|
+
if (!tokens.length) {
|
|
110
|
+
console.log('Нет сохранённых аккаунтов.');
|
|
111
|
+
await prompt('ENTER чтобы вернуться...');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const validTokens = tokens.filter(t => {
|
|
117
|
+
if (t.invalid) return false;
|
|
118
|
+
if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
|
|
119
|
+
if (t.expiryTime && t.expiryTime <= now) return false;
|
|
120
|
+
// Проверяем наличие cookies.json
|
|
121
|
+
const cookiesPath = path.resolve(__dirname, '..', '..', SESSION_DIR, ACCOUNTS_DIR, t.id, 'cookies.json');
|
|
122
|
+
if (!fs.existsSync(cookiesPath)) return false;
|
|
123
|
+
return true;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (validTokens.length === 0) {
|
|
127
|
+
console.log('Нет действительных аккаунтов.');
|
|
128
|
+
await prompt('ENTER чтобы вернуться...');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('\nДоступные аккаунты:');
|
|
133
|
+
validTokens.forEach((t, idx) => console.log(`${idx + 1} - ${t.id}`));
|
|
134
|
+
const choice = await prompt('Номер аккаунта, который нужно удалить (или ENTER для отмены): ');
|
|
135
|
+
if (!choice) return;
|
|
136
|
+
const num = parseInt(choice, 10);
|
|
137
|
+
if (isNaN(num) || num < 1 || num > validTokens.length) {
|
|
138
|
+
console.log('Неверный выбор.');
|
|
139
|
+
await prompt('ENTER чтобы вернуться...');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const acc = validTokens[num - 1];
|
|
144
|
+
const confirm = await prompt(`Точно удалить ${acc.id}? (y/N): `);
|
|
145
|
+
if (confirm.toLowerCase() !== 'y') return;
|
|
146
|
+
|
|
147
|
+
removeToken(acc.id);
|
|
148
|
+
const dir = path.resolve(__dirname, '..', '..', SESSION_DIR, ACCOUNTS_DIR, acc.id);
|
|
149
|
+
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
150
|
+
|
|
151
|
+
logInfo(`Аккаунт ${acc.id} удалён.`);
|
|
152
|
+
await prompt('ENTER чтобы вернуться...');
|
|
153
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { logInfo, logError, logWarn } from '../logger/index.js';
|
|
5
|
+
import { SESSION_DIR, DEFAULT_MODEL } from '../config.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
// Пути к файлам настроек
|
|
11
|
+
const SETTINGS_FILE = path.join(process.cwd(), SESSION_DIR, 'bot_settings.json');
|
|
12
|
+
|
|
13
|
+
// Кэш настроек
|
|
14
|
+
let settingsCache = null;
|
|
15
|
+
let settingsCacheMTime = null; // Время последнего изменения файла
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Проверяет изменился ли файл с момента последнего чтения
|
|
19
|
+
*/
|
|
20
|
+
function hasFileChanged() {
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
23
|
+
return true; // Файл не существует, нужно перечитать
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const stats = fs.statSync(SETTINGS_FILE);
|
|
27
|
+
const currentMTime = stats.mtimeMs;
|
|
28
|
+
|
|
29
|
+
// Если кэш пуст или время изменения отличается - файл изменился
|
|
30
|
+
return !settingsCacheMTime || currentMTime !== settingsCacheMTime;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return true; // При ошибке перечитываем
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Загружает настройки бота из файла (с кэшированием)
|
|
38
|
+
*/
|
|
39
|
+
export function loadBotSettings() {
|
|
40
|
+
try {
|
|
41
|
+
// Проверяем есть ли файл
|
|
42
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
43
|
+
if (!settingsCache) {
|
|
44
|
+
logInfo('📝 Файл настроек бота не найден, используем значения по умолчанию');
|
|
45
|
+
}
|
|
46
|
+
const defaultSettings = {
|
|
47
|
+
activeModel: null,
|
|
48
|
+
llmChatEnabled: false,
|
|
49
|
+
lastUpdated: null
|
|
50
|
+
};
|
|
51
|
+
settingsCache = defaultSettings;
|
|
52
|
+
settingsCacheMTime = null;
|
|
53
|
+
return defaultSettings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Проверяем изменился ли файл
|
|
57
|
+
if (!hasFileChanged() && settingsCache) {
|
|
58
|
+
// Файл не изменился, возвращаем кэш
|
|
59
|
+
return settingsCache;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Файл изменился или кэш пуст - читаем файл
|
|
63
|
+
const stats = fs.statSync(SETTINGS_FILE);
|
|
64
|
+
settingsCacheMTime = stats.mtimeMs;
|
|
65
|
+
|
|
66
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
67
|
+
settingsCache = settings;
|
|
68
|
+
|
|
69
|
+
logInfo(`✅ Настройки бота загружены из файла: модель=${settings.activeModel || 'default'}, LLM=${settings.llmChatEnabled}`);
|
|
70
|
+
return settings;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logError('❌ Ошибка загрузки настроек бота', error);
|
|
73
|
+
// Возвращаем кэш если есть, иначе defaults
|
|
74
|
+
if (settingsCache) {
|
|
75
|
+
logWarn('⚠️ Используется кэш настроек из-за ошибки чтения');
|
|
76
|
+
return settingsCache;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
activeModel: null,
|
|
80
|
+
llmChatEnabled: false,
|
|
81
|
+
lastUpdated: null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Сохраняет настройки бота в файл и обновляет кэш
|
|
88
|
+
*/
|
|
89
|
+
export function saveBotSettings(settings) {
|
|
90
|
+
try {
|
|
91
|
+
// Создаем директорию session если не существует
|
|
92
|
+
const sessionPath = path.join(process.cwd(), SESSION_DIR);
|
|
93
|
+
if (!fs.existsSync(sessionPath)) {
|
|
94
|
+
fs.mkdirSync(sessionPath, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
settings.lastUpdated = new Date().toISOString();
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
|
|
100
|
+
|
|
101
|
+
// Обновляем кэш
|
|
102
|
+
settingsCache = settings;
|
|
103
|
+
// Обновляем время модификации
|
|
104
|
+
const stats = fs.statSync(SETTINGS_FILE);
|
|
105
|
+
settingsCacheMTime = stats.mtimeMs;
|
|
106
|
+
|
|
107
|
+
logInfo(`💾 Настройки бота сохранены: модель=${settings.activeModel || 'default'}, LLM=${settings.llmChatEnabled}`);
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logError('❌ Ошибка сохранения настроек бота', error);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Загружает модели для конкретных чатов (не используется, все используют activeModel)
|
|
117
|
+
*/
|
|
118
|
+
export function loadChatModels() {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Сохраняет модели для конкретных чатов (не используется, все используют activeModel)
|
|
124
|
+
*/
|
|
125
|
+
export function saveChatModels(chatModels) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Принудительно очищает кэш настроек
|
|
131
|
+
* Используйте если файл был изменен вручную
|
|
132
|
+
*/
|
|
133
|
+
export function clearSettingsCache() {
|
|
134
|
+
settingsCache = null;
|
|
135
|
+
settingsCacheMTime = null;
|
|
136
|
+
logInfo('🗑️ Кэш настроек очищен');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Получает активную модель из настроек бота
|
|
141
|
+
* Приоритет: activeModel из bot_settings.json > DEFAULT_MODEL из .env > первая модель из AvailableModels.txt
|
|
142
|
+
* @returns {string} название модели
|
|
143
|
+
*/
|
|
144
|
+
export function getActiveModel() {
|
|
145
|
+
try {
|
|
146
|
+
const settings = loadBotSettings();
|
|
147
|
+
|
|
148
|
+
// Если есть activeModel в настройках бота - используем его
|
|
149
|
+
if (settings && settings.activeModel) {
|
|
150
|
+
return settings.activeModel;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Иначе используем DEFAULT_MODEL из .env
|
|
154
|
+
if (DEFAULT_MODEL) {
|
|
155
|
+
return DEFAULT_MODEL;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fallback: первая модель из списка доступных
|
|
159
|
+
try {
|
|
160
|
+
const modelsFile = path.join(process.cwd(), 'src', 'AvailableModels.txt');
|
|
161
|
+
if (fs.existsSync(modelsFile)) {
|
|
162
|
+
const modelsContent = fs.readFileSync(modelsFile, 'utf8');
|
|
163
|
+
const models = modelsContent.split('\n').map(m => m.trim()).filter(m => m && !m.startsWith('#'));
|
|
164
|
+
if (models.length > 0) {
|
|
165
|
+
return models[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// Если не удалось загрузить список моделей
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Последний fallback
|
|
173
|
+
return 'qwen3.5-plus';
|
|
174
|
+
} catch (error) {
|
|
175
|
+
logError('❌ Ошибка получения активной модели', error);
|
|
176
|
+
return 'qwen3.5-plus';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Получает текущий статус кэша (для отладки)
|
|
182
|
+
*/
|
|
183
|
+
export function getCacheStatus() {
|
|
184
|
+
return {
|
|
185
|
+
hasCache: settingsCache !== null,
|
|
186
|
+
cacheMTime: settingsCacheMTime,
|
|
187
|
+
cachedModel: settingsCache?.activeModel || null,
|
|
188
|
+
cachedLLM: settingsCache?.llmChatEnabled || false
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Устанавливает модель для конкретного чата
|
|
194
|
+
*/
|
|
195
|
+
export function setChatModel(chatId, modelName) {
|
|
196
|
+
try {
|
|
197
|
+
const chatModels = loadChatModels();
|
|
198
|
+
chatModels[String(chatId)] = modelName;
|
|
199
|
+
return saveChatModels(chatModels);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logError(`❌ Ошибка установки модели для чата ${chatId}`, error);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Получает модель для конкретного чата
|
|
208
|
+
*/
|
|
209
|
+
export function getChatModel(chatId) {
|
|
210
|
+
try {
|
|
211
|
+
const chatModels = loadChatModels();
|
|
212
|
+
return chatModels[String(chatId)] || null;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
logError(`❌ Ошибка получения модели для чата ${chatId}`, error);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Удаляет модель для конкретного чата
|
|
221
|
+
*/
|
|
222
|
+
export function removeChatModel(chatId) {
|
|
223
|
+
try {
|
|
224
|
+
const chatModels = loadChatModels();
|
|
225
|
+
delete chatModels[String(chatId)];
|
|
226
|
+
return saveChatModels(chatModels);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logError(`❌ Ошибка удаления модели для чата ${chatId}`, error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Permission Checker
|
|
5
|
+
*
|
|
6
|
+
* Checks write permissions for all project directories and files at startup.
|
|
7
|
+
* Provides helpful commands to fix any permission issues.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { logInfo, logWarn, logError } from '../logger/index.js';
|
|
14
|
+
import { SESSION_DIR, UPLOADS_DIR, LOGS_DIR } from '../config.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT_DIR = path.resolve(__dirname, '../..');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Define all directories and files that need write access
|
|
21
|
+
*/
|
|
22
|
+
const PATHS_TO_CHECK = [
|
|
23
|
+
// Main directories
|
|
24
|
+
{ path: SESSION_DIR, type: 'directory', recursive: true },
|
|
25
|
+
{ path: path.join(SESSION_DIR, 'accounts'), type: 'directory', recursive: true },
|
|
26
|
+
{ path: path.join(SESSION_DIR, 'history'), type: 'directory', recursive: true },
|
|
27
|
+
{ path: UPLOADS_DIR, type: 'directory', recursive: false },
|
|
28
|
+
{ path: LOGS_DIR, type: 'directory', recursive: false },
|
|
29
|
+
{ path: 'temp', type: 'directory', recursive: false },
|
|
30
|
+
{ path: 'session_backup', type: 'directory', recursive: false },
|
|
31
|
+
|
|
32
|
+
// Important files
|
|
33
|
+
{ path: path.join(SESSION_DIR, 'tokens.json'), type: 'file', optional: true },
|
|
34
|
+
{ path: path.join(SESSION_DIR, 'auth_token.txt'), type: 'file', optional: true },
|
|
35
|
+
{ path: path.join(SESSION_DIR, 'bot_settings.json'), type: 'file', optional: true },
|
|
36
|
+
{ path: '.env', type: 'file', optional: true },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Test if a path is writable
|
|
41
|
+
*/
|
|
42
|
+
function testWritePermission(testPath, type) {
|
|
43
|
+
try {
|
|
44
|
+
if (type === 'directory') {
|
|
45
|
+
// Test directory writability by checking if we can create/delete a temp file
|
|
46
|
+
const tempFile = path.join(testPath, '.write_test_' + Date.now());
|
|
47
|
+
fs.writeFileSync(tempFile, 'test');
|
|
48
|
+
fs.unlinkSync(tempFile);
|
|
49
|
+
return { writable: true, error: null };
|
|
50
|
+
} else if (type === 'file') {
|
|
51
|
+
// For files, test if parent directory is writable
|
|
52
|
+
const parentDir = path.dirname(testPath);
|
|
53
|
+
if (!fs.existsSync(parentDir)) {
|
|
54
|
+
return { writable: false, error: `Parent directory does not exist: ${parentDir}` };
|
|
55
|
+
}
|
|
56
|
+
// Try to read if file exists
|
|
57
|
+
if (fs.existsSync(testPath)) {
|
|
58
|
+
fs.accessSync(testPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
59
|
+
}
|
|
60
|
+
return { writable: true, error: null };
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { writable: false, error: error.message };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate fix commands for a path
|
|
69
|
+
*/
|
|
70
|
+
function generateFixCommands(testPath, type) {
|
|
71
|
+
const absolutePath = path.isAbsolute(testPath) ? testPath : path.join(ROOT_DIR, testPath);
|
|
72
|
+
const commands = [];
|
|
73
|
+
|
|
74
|
+
if (type === 'directory' || (type === 'file' && fs.existsSync(testPath))) {
|
|
75
|
+
// Fix ownership
|
|
76
|
+
commands.push(`sudo chown -R $USER:$USER "${absolutePath}"`);
|
|
77
|
+
// Fix permissions
|
|
78
|
+
if (type === 'directory') {
|
|
79
|
+
commands.push(`sudo chmod -R 755 "${absolutePath}"`);
|
|
80
|
+
} else {
|
|
81
|
+
commands.push(`sudo chmod 644 "${absolutePath}"`);
|
|
82
|
+
}
|
|
83
|
+
} else if (type === 'file' && !fs.existsSync(testPath)) {
|
|
84
|
+
// File doesn't exist, fix parent directory
|
|
85
|
+
const parentDir = path.dirname(absolutePath);
|
|
86
|
+
commands.push(`sudo chown -R $USER:$USER "${parentDir}"`);
|
|
87
|
+
commands.push(`sudo chmod -R 755 "${parentDir}"`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return commands;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check all paths and report issues
|
|
95
|
+
*/
|
|
96
|
+
export async function checkPermissions() {
|
|
97
|
+
logInfo('🔍 Проверка прав доступа к директориям и файлам...');
|
|
98
|
+
|
|
99
|
+
const issues = [];
|
|
100
|
+
const allCommands = new Set();
|
|
101
|
+
|
|
102
|
+
for (const { path: relativePath, type, optional, recursive } of PATHS_TO_CHECK) {
|
|
103
|
+
const absolutePath = path.isAbsolute(relativePath) ? relativePath : path.join(ROOT_DIR, relativePath);
|
|
104
|
+
|
|
105
|
+
// Skip optional paths that don't exist
|
|
106
|
+
if (optional && !fs.existsSync(absolutePath)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// For directories, check if they exist
|
|
111
|
+
if (type === 'directory' && !fs.existsSync(absolutePath)) {
|
|
112
|
+
// Try to create it
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(absolutePath, { recursive: true });
|
|
115
|
+
logInfo(` ✅ Создана директория: ${relativePath}`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
issues.push({
|
|
118
|
+
path: relativePath,
|
|
119
|
+
absolutePath,
|
|
120
|
+
type,
|
|
121
|
+
error: `Cannot create directory: ${error.message}`,
|
|
122
|
+
commands: generateFixCommands(path.dirname(absolutePath), 'directory')
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Test write permission
|
|
129
|
+
const result = testWritePermission(absolutePath, type);
|
|
130
|
+
|
|
131
|
+
if (!result.writable) {
|
|
132
|
+
issues.push({
|
|
133
|
+
path: relativePath,
|
|
134
|
+
absolutePath,
|
|
135
|
+
type,
|
|
136
|
+
error: result.error,
|
|
137
|
+
commands: generateFixCommands(absolutePath, type)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Report results
|
|
143
|
+
if (issues.length === 0) {
|
|
144
|
+
logInfo('✅ Все директории и файлы доступны для записи');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Report issues
|
|
149
|
+
logError(`❌ Обнаружены проблемы с правами доступа (${issues.length}):`);
|
|
150
|
+
console.error('');
|
|
151
|
+
|
|
152
|
+
for (const issue of issues) {
|
|
153
|
+
logError(` 📁 ${issue.path} (${issue.type})`);
|
|
154
|
+
logError(` Ошибка: ${issue.error}`);
|
|
155
|
+
logError(` Решение:`);
|
|
156
|
+
for (const cmd of issue.commands) {
|
|
157
|
+
console.error(` ${cmd}`);
|
|
158
|
+
allCommands.add(cmd);
|
|
159
|
+
}
|
|
160
|
+
console.error('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Show combined fix command
|
|
164
|
+
if (allCommands.size > 0) {
|
|
165
|
+
console.error('═'.repeat(70));
|
|
166
|
+
console.error('🔧 Быстрое решение (скопируйте и выполните в терминале):');
|
|
167
|
+
console.error('═'.repeat(70));
|
|
168
|
+
console.error('');
|
|
169
|
+
console.error(' ⚡ ОДНОЙ СТРОКОЙ (рекомендуется):');
|
|
170
|
+
console.error(' ' + '─'.repeat(68));
|
|
171
|
+
|
|
172
|
+
// Group by parent directory for cleaner output
|
|
173
|
+
const dirsToFix = new Set();
|
|
174
|
+
for (const issue of issues) {
|
|
175
|
+
if (issue.type === 'directory') {
|
|
176
|
+
dirsToFix.add(issue.absolutePath);
|
|
177
|
+
} else {
|
|
178
|
+
dirsToFix.add(path.dirname(issue.absolutePath));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const allPaths = Array.from(dirsToFix).join(' ');
|
|
183
|
+
console.error('');
|
|
184
|
+
console.error(` sudo chown -R $USER:$USER ${allPaths}`);
|
|
185
|
+
console.error(` sudo chmod -R 755 ${allPaths}`);
|
|
186
|
+
console.error('');
|
|
187
|
+
|
|
188
|
+
console.error(' 📦 ИЛИ исправить все основные директории проекта:');
|
|
189
|
+
console.error(' ' + '─'.repeat(68));
|
|
190
|
+
const mainDirs = [
|
|
191
|
+
path.join(ROOT_DIR, SESSION_DIR),
|
|
192
|
+
path.join(ROOT_DIR, UPLOADS_DIR),
|
|
193
|
+
path.join(ROOT_DIR, LOGS_DIR),
|
|
194
|
+
path.join(ROOT_DIR, 'temp'),
|
|
195
|
+
path.join(ROOT_DIR, 'session_backup')
|
|
196
|
+
].join(' ');
|
|
197
|
+
console.error(` sudo chown -R $USER:$USER ${mainDirs}`);
|
|
198
|
+
console.error(` sudo chmod -R 755 ${mainDirs}`);
|
|
199
|
+
console.error('');
|
|
200
|
+
console.error('═'.repeat(70));
|
|
201
|
+
console.error('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Интерактивный ввод из stdin.
|
|
5
|
+
* @param {string} question — текст вопроса
|
|
6
|
+
* @returns {Promise<string>} — ответ пользователя (trimmed)
|
|
7
|
+
*/
|
|
8
|
+
export function prompt(question) {
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
11
|
+
}
|