avg-nexus 1.0.10 → 1.0.12
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/bin/avg-nexus.js +756 -119
- package/package.json +26 -26
package/bin/avg-nexus.js
CHANGED
|
@@ -1,63 +1,259 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════
|
|
5
|
+
// AVG-NEXUS v4.0 — NexusAI Local Proxy + API Key Manager
|
|
6
|
+
// • Ollama → OpenRouter proxy (port 11435)
|
|
7
|
+
// • Device ID → API key tizimi
|
|
8
|
+
// • 10 ta OpenRouter key rotatsiya
|
|
9
|
+
// • Admin panel API (port 11436)
|
|
10
|
+
// • NexusAI saytni avtomatik ishga tushirish
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
12
|
|
|
13
|
+
const { execSync, spawn } = require('child_process');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
|
|
21
|
+
// ── Ranglar ──────────────────────────────────────────────────
|
|
9
22
|
const C = {
|
|
10
|
-
reset:
|
|
11
|
-
green:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cyan: '\x1b[36m',
|
|
15
|
-
bold: '\x1b[1m',
|
|
16
|
-
dim: '\x1b[2m',
|
|
23
|
+
reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m',
|
|
24
|
+
red:'\x1b[31m', green:'\x1b[32m', yellow:'\x1b[33m',
|
|
25
|
+
cyan:'\x1b[36m', purple:'\x1b[35m', blue:'\x1b[34m',
|
|
26
|
+
gray:'\x1b[90m', white:'\x1b[37m',
|
|
17
27
|
};
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
const log = (icon, msg, color = C.reset) =>
|
|
20
30
|
console.log(`${color}${icon} ${msg}${C.reset}`);
|
|
31
|
+
|
|
32
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
33
|
+
|
|
34
|
+
// ── Portlar ───────────────────────────────────────────────────
|
|
35
|
+
const PROXY_PORT = 11435; // nexusai-team .env.local ga mos
|
|
36
|
+
const ADMIN_PORT = 11436; // avg-ai CLI boshqaruv
|
|
37
|
+
const OLLAMA_PORT = 11434; // Ollama default
|
|
38
|
+
|
|
39
|
+
// ── Ma'lumot papkasi ──────────────────────────────────────────
|
|
40
|
+
const DATA_DIR = path.join(os.homedir(), '.avg-nexus');
|
|
41
|
+
const KEYS_FILE = path.join(DATA_DIR, 'keys.json');
|
|
42
|
+
const DEVICES_FILE = path.join(DATA_DIR, 'devices.json');
|
|
43
|
+
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
44
|
+
|
|
45
|
+
function ensureDataDir() {
|
|
46
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ══════════════════════════════════════════════════════════════
|
|
50
|
+
// KEY POOL — 10 ta OpenRouter kalit boshqaruvi
|
|
51
|
+
// ══════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
function loadKeys() {
|
|
54
|
+
ensureDataDir();
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(KEYS_FILE)) {
|
|
57
|
+
return JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
return { keys: [], currentIndex: 0 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function saveKeys(data) {
|
|
64
|
+
ensureDataDir();
|
|
65
|
+
fs.writeFileSync(KEYS_FILE, JSON.stringify(data, null, 2));
|
|
21
66
|
}
|
|
22
67
|
|
|
23
|
-
function
|
|
24
|
-
|
|
68
|
+
function addApiKey(key) {
|
|
69
|
+
const data = loadKeys();
|
|
70
|
+
if (!data.keys.includes(key)) {
|
|
71
|
+
data.keys.push(key);
|
|
72
|
+
saveKeys(data);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function removeApiKey(key) {
|
|
79
|
+
const data = loadKeys();
|
|
80
|
+
const before = data.keys.length;
|
|
81
|
+
data.keys = data.keys.filter(k => k !== key);
|
|
82
|
+
if (data.currentIndex >= data.keys.length) data.currentIndex = 0;
|
|
83
|
+
saveKeys(data);
|
|
84
|
+
return data.keys.length < before;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getNextKey() {
|
|
88
|
+
const data = loadKeys();
|
|
89
|
+
if (data.keys.length === 0) return null;
|
|
90
|
+
const key = data.keys[data.currentIndex % data.keys.length];
|
|
91
|
+
data.currentIndex = (data.currentIndex + 1) % data.keys.length;
|
|
92
|
+
saveKeys(data);
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getAllKeys() {
|
|
97
|
+
return loadKeys().keys;
|
|
25
98
|
}
|
|
26
99
|
|
|
27
|
-
//
|
|
100
|
+
// Key holati tracking
|
|
101
|
+
const keyStats = new Map();
|
|
102
|
+
|
|
103
|
+
function getKeyStats(key) {
|
|
104
|
+
if (!keyStats.has(key)) {
|
|
105
|
+
keyStats.set(key, {
|
|
106
|
+
calls: 0, success: 0, fail: 0,
|
|
107
|
+
lastUsed: null, lastError: null,
|
|
108
|
+
cooldownUntil: 0,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return keyStats.get(key);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function markSuccess(key) {
|
|
115
|
+
const s = getKeyStats(key);
|
|
116
|
+
s.calls++; s.success++; s.lastUsed = Date.now(); s.cooldownUntil = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function markFail(key, err) {
|
|
120
|
+
const s = getKeyStats(key);
|
|
121
|
+
s.calls++; s.fail++; s.lastError = err;
|
|
122
|
+
// Exponential backoff: 5s, 10s, 20s, max 60s
|
|
123
|
+
const backoff = Math.min(5000 * Math.pow(2, s.fail - 1), 60000);
|
|
124
|
+
s.cooldownUntil = Date.now() + backoff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isAvailable(key) {
|
|
128
|
+
const s = keyStats.get(key);
|
|
129
|
+
if (!s) return true;
|
|
130
|
+
return Date.now() > s.cooldownUntil;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getBestKey(keys) {
|
|
134
|
+
const available = keys.filter(isAvailable);
|
|
135
|
+
if (available.length === 0) return keys[Math.floor(Math.random() * keys.length)];
|
|
136
|
+
// Eng kam ishlatilgani
|
|
137
|
+
available.sort((a, b) => {
|
|
138
|
+
const sa = getKeyStats(a), sb = getKeyStats(b);
|
|
139
|
+
return (sa.lastUsed || 0) - (sb.lastUsed || 0);
|
|
140
|
+
});
|
|
141
|
+
return available[0];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ══════════════════════════════════════════════════════════════
|
|
145
|
+
// DEVICE MANAGER — har bir qurilma uchun API key
|
|
146
|
+
// ══════════════════════════════════════════════════════════════
|
|
147
|
+
|
|
148
|
+
function loadDevices() {
|
|
149
|
+
ensureDataDir();
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(DEVICES_FILE)) {
|
|
152
|
+
return JSON.parse(fs.readFileSync(DEVICES_FILE, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function saveDevices(data) {
|
|
159
|
+
ensureDataDir();
|
|
160
|
+
fs.writeFileSync(DEVICES_FILE, JSON.stringify(data, null, 2));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function registerDevice(deviceId, info = {}) {
|
|
164
|
+
const devices = loadDevices();
|
|
165
|
+
if (!devices[deviceId]) {
|
|
166
|
+
devices[deviceId] = {
|
|
167
|
+
id: deviceId,
|
|
168
|
+
registeredAt: Date.now(),
|
|
169
|
+
lastSeen: Date.now(),
|
|
170
|
+
info,
|
|
171
|
+
apiKey: null,
|
|
172
|
+
calls: 0,
|
|
173
|
+
};
|
|
174
|
+
saveDevices(devices);
|
|
175
|
+
log('📱', `Yangi qurilma: ${deviceId}`, C.cyan);
|
|
176
|
+
} else {
|
|
177
|
+
devices[deviceId].lastSeen = Date.now();
|
|
178
|
+
if (info.hostname) devices[deviceId].info = { ...devices[deviceId].info, ...info };
|
|
179
|
+
saveDevices(devices);
|
|
180
|
+
}
|
|
181
|
+
return devices[deviceId];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function assignKeyToDevice(deviceId, apiKey) {
|
|
185
|
+
const devices = loadDevices();
|
|
186
|
+
if (!devices[deviceId]) return false;
|
|
187
|
+
devices[deviceId].apiKey = apiKey;
|
|
188
|
+
saveDevices(devices);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getDeviceKey(deviceId) {
|
|
193
|
+
if (!deviceId) return null;
|
|
194
|
+
const devices = loadDevices();
|
|
195
|
+
return devices[deviceId]?.apiKey || null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ══════════════════════════════════════════════════════════════
|
|
199
|
+
// OLLAMA TOPISH
|
|
200
|
+
// ══════════════════════════════════════════════════════════════
|
|
201
|
+
|
|
28
202
|
function findOllama() {
|
|
203
|
+
// 1. PATH
|
|
29
204
|
try {
|
|
30
|
-
const r = execSync('where ollama'
|
|
205
|
+
const r = execSync(os.platform() === 'win32' ? 'where ollama' : 'which ollama',
|
|
206
|
+
{ encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
31
207
|
if (r) return r.split('\n')[0].trim();
|
|
32
208
|
} catch {}
|
|
33
209
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
210
|
+
// 2. Windows disk scan
|
|
211
|
+
if (os.platform() === 'win32') {
|
|
212
|
+
// Registry
|
|
213
|
+
try {
|
|
214
|
+
const reg = execSync(
|
|
215
|
+
'reg query "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ollama.exe" /ve',
|
|
216
|
+
{ stdio: 'pipe' }).toString();
|
|
217
|
+
const m = reg.match(/REG_SZ\s+(.+)/);
|
|
218
|
+
if (m && fs.existsSync(m[1].trim())) return m[1].trim();
|
|
219
|
+
} catch {}
|
|
40
220
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
221
|
+
// Disk scan
|
|
222
|
+
const drives = ['C','D','E','F','G','H'];
|
|
223
|
+
const locs = [
|
|
224
|
+
`AppData\\Local\\Programs\\Ollama\\ollama.exe`,
|
|
225
|
+
`Users\\${process.env.USERNAME || ''}\\AppData\\Local\\Programs\\Ollama\\ollama.exe`,
|
|
226
|
+
];
|
|
227
|
+
for (const d of drives) {
|
|
228
|
+
for (const l of locs) {
|
|
229
|
+
const p = `${d}:\\${l}`;
|
|
230
|
+
try { if (fs.existsSync(p)) return p; } catch {}
|
|
231
|
+
}
|
|
232
|
+
// Direct locations
|
|
233
|
+
for (const name of ['Ollama\\ollama.exe', 'ollama\\ollama.exe']) {
|
|
234
|
+
const p = `${d}:\\${name}`;
|
|
235
|
+
try { if (fs.existsSync(p)) return p; } catch {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (process.env.LOCALAPPDATA) {
|
|
239
|
+
const p = path.join(process.env.LOCALAPPDATA, 'Programs', 'Ollama', 'ollama.exe');
|
|
240
|
+
if (fs.existsSync(p)) return p;
|
|
45
241
|
}
|
|
46
242
|
}
|
|
47
243
|
return null;
|
|
48
244
|
}
|
|
49
245
|
|
|
50
|
-
//
|
|
246
|
+
// ══════════════════════════════════════════════════════════════
|
|
247
|
+
// MODEL TOPISH
|
|
248
|
+
// ══════════════════════════════════════════════════════════════
|
|
249
|
+
|
|
51
250
|
function findBestModel() {
|
|
52
|
-
const preferred = ['gemma4',
|
|
251
|
+
const preferred = ['gemma4','gemma3','gemma2','qwen2.5','qwen','llama','mistral','deepseek','phi'];
|
|
53
252
|
try {
|
|
54
253
|
const result = execSync('ollama list', {
|
|
55
|
-
encoding: 'utf8',
|
|
56
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
-
timeout: 10000,
|
|
254
|
+
encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 15000,
|
|
58
255
|
});
|
|
59
|
-
const lines = result
|
|
60
|
-
.split('\n')
|
|
256
|
+
const lines = result.split('\n')
|
|
61
257
|
.filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
|
|
62
258
|
|
|
63
259
|
if (lines.length > 0) {
|
|
@@ -65,8 +261,7 @@ function findBestModel() {
|
|
|
65
261
|
const found = lines.find(l => l.toLowerCase().includes(pref));
|
|
66
262
|
if (found) {
|
|
67
263
|
const name = found.split(/\s+/)[0].trim();
|
|
68
|
-
log('✅', `Model
|
|
69
|
-
return name;
|
|
264
|
+
if (name) { log('✅', `Model: ${name}`, C.green); return name; }
|
|
70
265
|
}
|
|
71
266
|
}
|
|
72
267
|
const name = lines[0].split(/\s+/)[0].trim();
|
|
@@ -74,94 +269,434 @@ function findBestModel() {
|
|
|
74
269
|
return name;
|
|
75
270
|
}
|
|
76
271
|
} catch (e) {
|
|
77
|
-
log('⚠️', `ollama list
|
|
272
|
+
log('⚠️', `ollama list: ${e.message}`, C.yellow);
|
|
78
273
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
274
|
+
log('📥', 'Model topilmadi, gemma4:e4b yuklanmoqda...', C.yellow);
|
|
275
|
+
try {
|
|
276
|
+
execSync('ollama pull gemma4:e4b', { stdio: 'inherit' });
|
|
277
|
+
} catch {}
|
|
83
278
|
return 'gemma4:e4b';
|
|
84
279
|
}
|
|
85
280
|
|
|
86
|
-
//
|
|
87
|
-
|
|
281
|
+
// ══════════════════════════════════════════════════════════════
|
|
282
|
+
// OLLAMA SERVE
|
|
283
|
+
// ══════════════════════════════════════════════════════════════
|
|
284
|
+
|
|
285
|
+
async function startOllama(ollamaPath) {
|
|
286
|
+
// Allaqachon ishlayaptimi?
|
|
88
287
|
try {
|
|
89
|
-
|
|
288
|
+
await httpGet('http://localhost:11434/api/tags', 2000);
|
|
90
289
|
log('✅', 'Ollama allaqachon ishlamoqda', C.green);
|
|
91
290
|
return;
|
|
92
291
|
} catch {}
|
|
292
|
+
|
|
93
293
|
log('🚀', 'Ollama ishga tushirilmoqda...', C.cyan);
|
|
94
294
|
const proc = spawn(ollamaPath, ['serve'], {
|
|
95
|
-
detached: true,
|
|
96
|
-
stdio: 'ignore',
|
|
97
|
-
windowsHide: true,
|
|
295
|
+
detached: true, stdio: 'ignore', windowsHide: true,
|
|
98
296
|
});
|
|
99
297
|
proc.unref();
|
|
298
|
+
await sleep(3000);
|
|
100
299
|
log('✅', 'Ollama serve ishga tushdi', C.green);
|
|
101
300
|
}
|
|
102
301
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
302
|
+
// ══════════════════════════════════════════════════════════════
|
|
303
|
+
// HTTP HELPER
|
|
304
|
+
// ══════════════════════════════════════════════════════════════
|
|
305
|
+
|
|
306
|
+
function httpGet(url, timeout = 5000) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const req = http.get(url, { timeout }, (res) => {
|
|
309
|
+
let data = '';
|
|
310
|
+
res.on('data', c => data += c);
|
|
311
|
+
res.on('end', () => resolve(data));
|
|
312
|
+
});
|
|
313
|
+
req.on('error', reject);
|
|
314
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ══════════════════════════════════════════════════════════════
|
|
319
|
+
// PROXY SERVER — OpenRouter API format → Ollama
|
|
320
|
+
// ══════════════════════════════════════════════════════════════
|
|
321
|
+
|
|
322
|
+
function startProxy(localModel) {
|
|
106
323
|
const server = http.createServer(async (req, res) => {
|
|
324
|
+
// CORS
|
|
107
325
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
108
326
|
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
109
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST,
|
|
327
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
328
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
110
329
|
|
|
111
|
-
|
|
330
|
+
const url = req.url || '/';
|
|
112
331
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
332
|
+
// ── Models endpoint ──
|
|
333
|
+
if (url.includes('/models')) {
|
|
334
|
+
try {
|
|
335
|
+
const result = execSync('ollama list', {
|
|
336
|
+
encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 5000,
|
|
337
|
+
});
|
|
338
|
+
const lines = result.split('\n')
|
|
339
|
+
.filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
|
|
340
|
+
const models = lines.map(l => {
|
|
341
|
+
const name = l.split(/\s+/)[0].trim();
|
|
342
|
+
return {
|
|
343
|
+
id: name, object: 'model',
|
|
344
|
+
created: Math.floor(Date.now() / 1000),
|
|
345
|
+
owned_by: 'ollama', name,
|
|
346
|
+
context_length: 8192,
|
|
347
|
+
pricing: { prompt: '0', completion: '0' },
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
351
|
+
res.end(JSON.stringify({ object: 'list', data: models }));
|
|
352
|
+
} catch {
|
|
353
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
354
|
+
res.end(JSON.stringify({
|
|
355
|
+
object: 'list',
|
|
356
|
+
data: [{ id: localModel, name: localModel, object: 'model',
|
|
357
|
+
owned_by: 'ollama', context_length: 8192,
|
|
358
|
+
pricing: { prompt: '0', completion: '0' } }],
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
116
361
|
return;
|
|
117
362
|
}
|
|
118
363
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
364
|
+
// ── Chat completions ──
|
|
365
|
+
if (url.includes('/chat/completions')) {
|
|
366
|
+
let body = '';
|
|
367
|
+
req.on('data', c => body += c);
|
|
368
|
+
req.on('end', async () => {
|
|
369
|
+
try {
|
|
370
|
+
const d = JSON.parse(body || '{}');
|
|
371
|
+
const model = localModel;
|
|
372
|
+
const isStream = d.stream !== false;
|
|
373
|
+
|
|
374
|
+
// Device ID dan key olish (Authorization header dan)
|
|
375
|
+
const authHeader = req.headers['authorization'] || '';
|
|
376
|
+
const deviceId = authHeader.replace('Bearer ', '').trim();
|
|
377
|
+
const deviceApiKey = getDeviceKey(deviceId);
|
|
378
|
+
|
|
379
|
+
// Agar device o'ziga specific OpenRouter key bor bo'lsa — undan foydalanish
|
|
380
|
+
// Aks holda Ollama local
|
|
381
|
+
const allKeys = getAllKeys();
|
|
382
|
+
const useOnline = (deviceApiKey || allKeys.length > 0) &&
|
|
383
|
+
deviceId && deviceId.startsWith('sk-') === false &&
|
|
384
|
+
!['ollama-local','local','offline'].includes(deviceId);
|
|
385
|
+
|
|
386
|
+
if (deviceId && !deviceId.startsWith('sk-') && deviceId !== 'ollama-local') {
|
|
387
|
+
registerDevice(deviceId, {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Ollama ga yo'naltir
|
|
391
|
+
await proxyToOllama(req, res, d, model, isStream);
|
|
392
|
+
|
|
393
|
+
} catch (e) {
|
|
394
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
395
|
+
res.end(JSON.stringify({ error: { message: e.message, type: 'proxy_error' } }));
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── Boshqalar ──
|
|
402
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
403
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
server.listen(PROXY_PORT, '127.0.0.1', () => {
|
|
407
|
+
log('✅', `Proxy: http://localhost:${PROXY_PORT}`, C.green);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
server.on('error', err => {
|
|
411
|
+
if (err.code === 'EADDRINUSE') {
|
|
412
|
+
log('⚠️', `Port ${PROXY_PORT} band — proxy allaqachon ishlamoqda`, C.yellow);
|
|
413
|
+
} else {
|
|
414
|
+
log('❌', `Proxy xatosi: ${err.message}`, C.red);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return server;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function proxyToOllama(req, res, body, model, isStream) {
|
|
422
|
+
const ollamaBody = JSON.stringify({
|
|
423
|
+
model: model,
|
|
424
|
+
messages: body.messages || [],
|
|
425
|
+
stream: isStream,
|
|
426
|
+
options: {
|
|
427
|
+
temperature: body.temperature || 0.7,
|
|
428
|
+
num_predict: body.max_tokens || 2048,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const proxyReq = http.request({
|
|
434
|
+
hostname: '127.0.0.1', port: OLLAMA_PORT,
|
|
435
|
+
path: '/api/chat', method: 'POST',
|
|
436
|
+
headers: {
|
|
437
|
+
'Content-Type': 'application/json',
|
|
438
|
+
'Content-Length': Buffer.byteLength(ollamaBody),
|
|
439
|
+
},
|
|
440
|
+
}, (proxyRes) => {
|
|
441
|
+
if (isStream) {
|
|
442
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
|
|
443
|
+
const decoder = require('string_decoder').StringDecoder;
|
|
444
|
+
const dec = new (decoder)('utf8');
|
|
445
|
+
proxyRes.on('data', chunk => {
|
|
446
|
+
const text = dec.write(chunk);
|
|
447
|
+
for (const line of text.split('\n')) {
|
|
448
|
+
if (!line.trim()) continue;
|
|
137
449
|
try {
|
|
138
450
|
const j = JSON.parse(line);
|
|
451
|
+
const content = j.message?.content || '';
|
|
452
|
+
const done = j.done || false;
|
|
139
453
|
res.write('data: ' + JSON.stringify({
|
|
140
454
|
id: 'chatcmpl-' + Date.now(),
|
|
141
455
|
object: 'chat.completion.chunk',
|
|
142
|
-
choices: [{
|
|
456
|
+
choices: [{
|
|
457
|
+
delta: { content },
|
|
458
|
+
finish_reason: done ? 'stop' : null,
|
|
459
|
+
index: 0,
|
|
460
|
+
}],
|
|
143
461
|
}) + '\n\n');
|
|
144
462
|
} catch {}
|
|
145
463
|
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
464
|
+
});
|
|
465
|
+
proxyRes.on('end', () => {
|
|
466
|
+
res.write('data: [DONE]\n\n');
|
|
467
|
+
res.end();
|
|
468
|
+
resolve();
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
let data = '';
|
|
472
|
+
proxyRes.on('data', c => data += c);
|
|
473
|
+
proxyRes.on('end', () => {
|
|
474
|
+
try {
|
|
475
|
+
// Ollama javoblarini OpenRouter formatiga aylantirish
|
|
476
|
+
const lines = data.split('\n').filter(l => l.trim());
|
|
477
|
+
let fullContent = '';
|
|
478
|
+
for (const line of lines) {
|
|
479
|
+
try {
|
|
480
|
+
const j = JSON.parse(line);
|
|
481
|
+
fullContent += j.message?.content || '';
|
|
482
|
+
} catch {}
|
|
483
|
+
}
|
|
484
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
485
|
+
res.end(JSON.stringify({
|
|
486
|
+
id: 'chatcmpl-' + Date.now(),
|
|
487
|
+
object: 'chat.completion',
|
|
488
|
+
model: model,
|
|
489
|
+
choices: [{
|
|
490
|
+
message: { role: 'assistant', content: fullContent },
|
|
491
|
+
finish_reason: 'stop', index: 0,
|
|
492
|
+
}],
|
|
493
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
494
|
+
}));
|
|
495
|
+
} catch (e) {
|
|
496
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
497
|
+
res.end(JSON.stringify({ error: { message: e.message } }));
|
|
498
|
+
}
|
|
499
|
+
resolve();
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
proxyRes.on('error', reject);
|
|
150
503
|
});
|
|
504
|
+
proxyReq.on('error', reject);
|
|
505
|
+
proxyReq.write(ollamaBody);
|
|
506
|
+
proxyReq.end();
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ══════════════════════════════════════════════════════════════
|
|
511
|
+
// ADMIN SERVER — avg-ai CLI boshqaruvi (port 11436)
|
|
512
|
+
// ══════════════════════════════════════════════════════════════
|
|
513
|
+
|
|
514
|
+
function startAdminServer(localModel) {
|
|
515
|
+
const server = http.createServer((req, res) => {
|
|
516
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
517
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
518
|
+
res.setHeader('Content-Type', 'application/json');
|
|
519
|
+
|
|
520
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
521
|
+
|
|
522
|
+
const url = req.url || '/';
|
|
523
|
+
let body = '';
|
|
524
|
+
req.on('data', c => body += c);
|
|
525
|
+
req.on('end', () => {
|
|
526
|
+
try {
|
|
527
|
+
handleAdminRequest(req, res, url, body, localModel);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
res.writeHead(500);
|
|
530
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
server.listen(ADMIN_PORT, '127.0.0.1', () => {
|
|
536
|
+
log('✅', `Admin API: http://localhost:${ADMIN_PORT}`, C.green);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
server.on('error', err => {
|
|
540
|
+
if (err.code !== 'EADDRINUSE') {
|
|
541
|
+
log('❌', `Admin server: ${err.message}`, C.red);
|
|
542
|
+
}
|
|
151
543
|
});
|
|
152
|
-
server.listen(PORT, () => log('✅', `Proxy: http://localhost:${PORT}`, C.green));
|
|
153
|
-
return server;
|
|
154
544
|
}
|
|
155
545
|
|
|
156
|
-
|
|
546
|
+
function handleAdminRequest(req, res, url, body, localModel) {
|
|
547
|
+
let parsed = {};
|
|
548
|
+
try { parsed = JSON.parse(body); } catch {}
|
|
549
|
+
|
|
550
|
+
// ── Status ──
|
|
551
|
+
if (url === '/status' || url === '/') {
|
|
552
|
+
const keys = getAllKeys();
|
|
553
|
+
const devices = loadDevices();
|
|
554
|
+
const keyStatus = keys.map((k, i) => {
|
|
555
|
+
const s = getKeyStats(k);
|
|
556
|
+
return {
|
|
557
|
+
index: i + 1,
|
|
558
|
+
preview: `...${k.slice(-6)}`,
|
|
559
|
+
available: isAvailable(k),
|
|
560
|
+
calls: s.calls, success: s.success, fail: s.fail,
|
|
561
|
+
cooldownMs: Math.max(0, s.cooldownUntil - Date.now()),
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
res.writeHead(200);
|
|
565
|
+
res.end(JSON.stringify({
|
|
566
|
+
status: 'running',
|
|
567
|
+
model: localModel,
|
|
568
|
+
proxyPort: PROXY_PORT,
|
|
569
|
+
adminPort: ADMIN_PORT,
|
|
570
|
+
ollamaPort: OLLAMA_PORT,
|
|
571
|
+
keys: { total: keys.length, status: keyStatus },
|
|
572
|
+
devices: { total: Object.keys(devices).length, list: Object.values(devices).map(d => ({
|
|
573
|
+
id: d.id, lastSeen: d.lastSeen, hasKey: !!d.apiKey, calls: d.calls,
|
|
574
|
+
})) },
|
|
575
|
+
uptime: process.uptime(),
|
|
576
|
+
}));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── Keys: qo'shish ──
|
|
581
|
+
if (url === '/keys/add' && req.method === 'POST') {
|
|
582
|
+
const key = parsed.key?.trim();
|
|
583
|
+
if (!key || !key.startsWith('sk-')) {
|
|
584
|
+
res.writeHead(400); res.end(JSON.stringify({ error: 'sk- bilan boshlanishi kerak' }));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const added = addApiKey(key);
|
|
588
|
+
res.writeHead(200);
|
|
589
|
+
res.end(JSON.stringify({ success: true, added, total: getAllKeys().length }));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── Keys: o'chirish ──
|
|
594
|
+
if (url === '/keys/remove' && req.method === 'POST') {
|
|
595
|
+
const key = parsed.key?.trim();
|
|
596
|
+
const removed = removeApiKey(key);
|
|
597
|
+
res.writeHead(200);
|
|
598
|
+
res.end(JSON.stringify({ success: removed, total: getAllKeys().length }));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── Keys: ro'yxat ──
|
|
603
|
+
if (url === '/keys/list') {
|
|
604
|
+
const keys = getAllKeys();
|
|
605
|
+
res.writeHead(200);
|
|
606
|
+
res.end(JSON.stringify({
|
|
607
|
+
total: keys.length,
|
|
608
|
+
keys: keys.map((k, i) => {
|
|
609
|
+
const s = getKeyStats(k);
|
|
610
|
+
return {
|
|
611
|
+
index: i + 1, preview: `sk-or-...${k.slice(-8)}`,
|
|
612
|
+
available: isAvailable(k), calls: s.calls,
|
|
613
|
+
success: s.success, fail: s.fail,
|
|
614
|
+
};
|
|
615
|
+
}),
|
|
616
|
+
}));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Device: ro'yxat ──
|
|
621
|
+
if (url === '/devices') {
|
|
622
|
+
const devices = loadDevices();
|
|
623
|
+
res.writeHead(200);
|
|
624
|
+
res.end(JSON.stringify({ devices: Object.values(devices) }));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Device: key tayinlash ──
|
|
629
|
+
if (url === '/devices/assign' && req.method === 'POST') {
|
|
630
|
+
const { deviceId, key } = parsed;
|
|
631
|
+
if (!deviceId || !key) {
|
|
632
|
+
res.writeHead(400); res.end(JSON.stringify({ error: 'deviceId va key kerak' }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const ok = assignKeyToDevice(deviceId, key);
|
|
636
|
+
res.writeHead(200);
|
|
637
|
+
res.end(JSON.stringify({ success: ok }));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── Ollama: modellar ──
|
|
642
|
+
if (url === '/ollama/models') {
|
|
643
|
+
try {
|
|
644
|
+
const result = execSync('ollama list', {
|
|
645
|
+
encoding: 'utf8', stdio: ['pipe','pipe','pipe'], timeout: 10000,
|
|
646
|
+
});
|
|
647
|
+
const lines = result.split('\n').filter(l => l.trim() && !l.toLowerCase().startsWith('name'));
|
|
648
|
+
const models = lines.map(l => ({
|
|
649
|
+
name: l.split(/\s+/)[0].trim(),
|
|
650
|
+
size: l.split(/\s+/)[2] || '',
|
|
651
|
+
modified: l.split(/\s+/).slice(3).join(' ') || '',
|
|
652
|
+
})).filter(m => m.name);
|
|
653
|
+
res.writeHead(200);
|
|
654
|
+
res.end(JSON.stringify({ models, current: localModel }));
|
|
655
|
+
} catch (e) {
|
|
656
|
+
res.writeHead(500);
|
|
657
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── Env: .env.local yangilash ──
|
|
663
|
+
if (url === '/env/update' && req.method === 'POST') {
|
|
664
|
+
const { projectDir } = parsed;
|
|
665
|
+
if (!projectDir || !fs.existsSync(projectDir)) {
|
|
666
|
+
res.writeHead(400); res.end(JSON.stringify({ error: 'projectDir topilmadi' })); return;
|
|
667
|
+
}
|
|
668
|
+
const keys = getAllKeys();
|
|
669
|
+
const envLines = [
|
|
670
|
+
`OPENROUTER_BASE_URL=http://localhost:${PROXY_PORT}/api/v1`,
|
|
671
|
+
];
|
|
672
|
+
if (keys.length > 0) {
|
|
673
|
+
keys.slice(0, 10).forEach((k, i) => envLines.push(`OPENROUTER_API_KEY_${i+1}=${k}`));
|
|
674
|
+
} else {
|
|
675
|
+
for (let i = 1; i <= 10; i++) envLines.push(`OPENROUTER_API_KEY_${i}=ollama-local`);
|
|
676
|
+
}
|
|
677
|
+
const hfKey = process.env.HUGGINGFACE_API_KEY || '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';
|
|
678
|
+
envLines.push(`HUGGINGFACE_API_KEY=${hfKey}`);
|
|
679
|
+
fs.writeFileSync(path.join(projectDir, '.env.local'), envLines.join('\n'));
|
|
680
|
+
res.writeHead(200);
|
|
681
|
+
res.end(JSON.stringify({ success: true, keysCount: keys.length }));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
res.writeHead(404);
|
|
686
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ══════════════════════════════════════════════════════════════
|
|
690
|
+
// NEXUSAI LOYIHA TOPISH
|
|
691
|
+
// ══════════════════════════════════════════════════════════════
|
|
692
|
+
|
|
157
693
|
function findProject() {
|
|
158
|
-
const cfgs = ['next.config.js',
|
|
694
|
+
const cfgs = ['next.config.js','next.config.ts','next.config.mjs'];
|
|
159
695
|
const cwd = process.cwd();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
696
|
+
if (cfgs.some(c => fs.existsSync(path.join(cwd, c)))) return cwd;
|
|
697
|
+
|
|
163
698
|
const drives = ['E','D','C','F','G'];
|
|
164
|
-
const names = ['nexusai-team','NexusAi-team','NexusAI-team','nexus-team','nexusai'];
|
|
699
|
+
const names = ['nexusai-team','NexusAi-team','NexusAI-team','nexus-team','nexusai','NexusAI'];
|
|
165
700
|
for (const d of drives) {
|
|
166
701
|
for (const n of names) {
|
|
167
702
|
const p = `${d}:\\${n}`;
|
|
@@ -170,56 +705,118 @@ function findProject() {
|
|
|
170
705
|
} catch {}
|
|
171
706
|
}
|
|
172
707
|
}
|
|
708
|
+
|
|
709
|
+
// Install papkasini ham tekshir
|
|
710
|
+
const installDir = `C:\\NexusAI`;
|
|
711
|
+
if (fs.existsSync(installDir) && cfgs.some(c => fs.existsSync(path.join(installDir, c)))) {
|
|
712
|
+
return installDir;
|
|
713
|
+
}
|
|
714
|
+
|
|
173
715
|
return null;
|
|
174
716
|
}
|
|
175
717
|
|
|
176
|
-
//
|
|
718
|
+
// ══════════════════════════════════════════════════════════════
|
|
719
|
+
// .env.local SOZLASH
|
|
720
|
+
// ══════════════════════════════════════════════════════════════
|
|
721
|
+
|
|
177
722
|
function setupEnv(dir) {
|
|
178
723
|
const envPath = path.join(dir, '.env.local');
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const ex = fs.readFileSync(envPath, 'utf8');
|
|
187
|
-
if (ex.includes('ollama-local')) return;
|
|
188
|
-
fs.writeFileSync(envPath, content + '\n' + ex);
|
|
724
|
+
const keys = getAllKeys();
|
|
725
|
+
const lines = [
|
|
726
|
+
`OPENROUTER_BASE_URL=http://localhost:${PROXY_PORT}/api/v1`,
|
|
727
|
+
];
|
|
728
|
+
|
|
729
|
+
if (keys.length > 0) {
|
|
730
|
+
keys.slice(0, 10).forEach((k, i) => lines.push(`OPENROUTER_API_KEY_${i+1}=${k}`));
|
|
189
731
|
} else {
|
|
190
|
-
|
|
732
|
+
for (let i = 1; i <= 10; i++) lines.push(`OPENROUTER_API_KEY_${i}=ollama-local`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const hfKey = process.env.HUGGINGFACE_API_KEY || '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';
|
|
736
|
+
lines.push(`HUGGINGFACE_API_KEY=${hfKey}`);
|
|
737
|
+
|
|
738
|
+
// Mavjud .env.local dan boshqa env varlarni saqlab qolish
|
|
739
|
+
let extra = '';
|
|
740
|
+
if (fs.existsSync(envPath)) {
|
|
741
|
+
const existing = fs.readFileSync(envPath, 'utf8').split('\n');
|
|
742
|
+
const keep = existing.filter(l => {
|
|
743
|
+
const key = l.split('=')[0].trim();
|
|
744
|
+
return l.trim() && !l.startsWith('#') &&
|
|
745
|
+
!key.startsWith('OPENROUTER_') && key !== 'HUGGINGFACE_API_KEY';
|
|
746
|
+
});
|
|
747
|
+
extra = keep.join('\n');
|
|
191
748
|
}
|
|
192
|
-
|
|
749
|
+
|
|
750
|
+
const content = lines.join('\n') + (extra ? '\n' + extra : '');
|
|
751
|
+
fs.writeFileSync(envPath, content);
|
|
752
|
+
log('✅', `.env.local sozlandi (${keys.length} key)`, C.green);
|
|
193
753
|
}
|
|
194
754
|
|
|
195
|
-
//
|
|
755
|
+
// ══════════════════════════════════════════════════════════════
|
|
756
|
+
// NEXT.JS ISHGA TUSHIRISH
|
|
757
|
+
// ══════════════════════════════════════════════════════════════
|
|
758
|
+
|
|
196
759
|
function startNextJS(dir) {
|
|
197
760
|
return new Promise(resolve => {
|
|
198
761
|
setupEnv(dir);
|
|
762
|
+
|
|
199
763
|
if (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
200
764
|
log('📦', 'npm install...', C.yellow);
|
|
201
|
-
|
|
765
|
+
try {
|
|
766
|
+
execSync('npm install', { cwd: dir, stdio: 'inherit' });
|
|
767
|
+
} catch (e) {
|
|
768
|
+
log('❌', `npm install xatosi: ${e.message}`, C.red);
|
|
769
|
+
}
|
|
202
770
|
}
|
|
771
|
+
|
|
203
772
|
log('🌐', 'NexusAI ishga tushmoqda...', C.cyan);
|
|
773
|
+
const env = {
|
|
774
|
+
...process.env,
|
|
775
|
+
PORT: '3000',
|
|
776
|
+
OPENROUTER_BASE_URL: `http://localhost:${PROXY_PORT}/api/v1`,
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// API keylarni ham environment ga qo'shamiz
|
|
780
|
+
const keys = getAllKeys();
|
|
781
|
+
keys.slice(0, 10).forEach((k, i) => {
|
|
782
|
+
env[`OPENROUTER_API_KEY_${i+1}`] = k;
|
|
783
|
+
});
|
|
784
|
+
if (keys.length === 0) {
|
|
785
|
+
for (let i = 1; i <= 10; i++) env[`OPENROUTER_API_KEY_${i}`] = 'ollama-local';
|
|
786
|
+
}
|
|
787
|
+
|
|
204
788
|
const dev = spawn('npm', ['run', 'dev'], {
|
|
205
789
|
cwd: dir, shell: true,
|
|
206
790
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
207
|
-
env
|
|
791
|
+
env,
|
|
208
792
|
});
|
|
793
|
+
|
|
794
|
+
let started = false;
|
|
209
795
|
dev.stdout.on('data', d => {
|
|
210
796
|
const t = d.toString();
|
|
211
|
-
|
|
797
|
+
process.stdout.write(C.dim + t + C.reset);
|
|
798
|
+
if (!started && (t.includes('Ready') || t.includes('ready') || t.includes('localhost:3000'))) {
|
|
799
|
+
started = true;
|
|
212
800
|
log('✅', 'Sayt tayyor: http://localhost:3000', C.green);
|
|
213
801
|
setTimeout(resolve, 500);
|
|
214
802
|
}
|
|
215
803
|
});
|
|
216
|
-
dev.on('
|
|
217
|
-
|
|
804
|
+
dev.stderr.on('data', d => {
|
|
805
|
+
const t = d.toString();
|
|
806
|
+
if (t.includes('Error') || t.includes('error')) {
|
|
807
|
+
process.stdout.write(C.red + t + C.reset);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
dev.on('error', e => { log('❌', e.message, C.red); resolve(); });
|
|
811
|
+
setTimeout(() => { if (!started) resolve(); }, 90000);
|
|
218
812
|
});
|
|
219
813
|
}
|
|
220
814
|
|
|
221
|
-
//
|
|
222
|
-
|
|
815
|
+
// ══════════════════════════════════════════════════════════════
|
|
816
|
+
// BANNER
|
|
817
|
+
// ══════════════════════════════════════════════════════════════
|
|
818
|
+
|
|
819
|
+
function showBanner() {
|
|
223
820
|
console.log(`\n${C.bold}${C.cyan}
|
|
224
821
|
███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗ █████╗ ██╗
|
|
225
822
|
████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝██╔══██╗██║
|
|
@@ -227,39 +824,79 @@ async function main() {
|
|
|
227
824
|
██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║██╔══██║██║
|
|
228
825
|
██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝███████║██║ ██║██║
|
|
229
826
|
╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝
|
|
230
|
-
${C.reset}${C.
|
|
827
|
+
${C.reset}${C.yellow} AVG-NEXUS v4.0 — NexusAI Local Proxy${C.reset}\n`);
|
|
828
|
+
}
|
|
231
829
|
|
|
830
|
+
// ══════════════════════════════════════════════════════════════
|
|
831
|
+
// MAIN
|
|
832
|
+
// ══════════════════════════════════════════════════════════════
|
|
833
|
+
|
|
834
|
+
async function main() {
|
|
835
|
+
ensureDataDir();
|
|
836
|
+
showBanner();
|
|
837
|
+
|
|
838
|
+
// 1. Ollama topish
|
|
232
839
|
log('🔍', 'Ollama qidirilmoqda...', C.cyan);
|
|
233
840
|
const ollamaPath = findOllama();
|
|
234
841
|
if (!ollamaPath) {
|
|
235
842
|
log('❌', 'Ollama topilmadi! https://ollama.com/download', C.red);
|
|
843
|
+
log('💡', 'Ollama o\'rnatib qayta ishga tushiring', C.yellow);
|
|
236
844
|
process.exit(1);
|
|
237
845
|
}
|
|
238
846
|
log('✅', `Ollama: ${ollamaPath}`, C.green);
|
|
239
847
|
|
|
240
|
-
|
|
241
|
-
await
|
|
848
|
+
// 2. Ollama serve
|
|
849
|
+
await startOllama(ollamaPath);
|
|
242
850
|
|
|
851
|
+
// 3. Model topish
|
|
243
852
|
log('🔍', 'AI model qidirilmoqda...', C.cyan);
|
|
244
853
|
const model = findBestModel();
|
|
245
854
|
|
|
855
|
+
// 4. Proxy server
|
|
856
|
+
log('🔀', 'Proxy server ishga tushirilmoqda...', C.cyan);
|
|
246
857
|
startProxy(model);
|
|
247
858
|
|
|
859
|
+
// 5. Admin server
|
|
860
|
+
startAdminServer(model);
|
|
861
|
+
|
|
862
|
+
// 6. Keys holati
|
|
863
|
+
const keys = getAllKeys();
|
|
864
|
+
if (keys.length > 0) {
|
|
865
|
+
log('🔑', `${keys.length} ta OpenRouter key topildi`, C.green);
|
|
866
|
+
} else {
|
|
867
|
+
log('🦙', 'API key yo\'q — faqat Ollama (offline rejim)', C.yellow);
|
|
868
|
+
log('💡', 'Key qo\'shish: avg-ai yoqing va /keys add sk-or-... bering', C.dim);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// 7. NexusAI loyiha
|
|
248
872
|
log('🔍', 'NexusAI loyiha qidirilmoqda...', C.cyan);
|
|
249
873
|
const dir = findProject();
|
|
250
874
|
if (!dir) {
|
|
251
|
-
log('
|
|
252
|
-
log('💡', 'nexusai-team papkasida turib avg-nexus bering', C.
|
|
253
|
-
|
|
875
|
+
log('⚠️', 'NexusAI loyiha topilmadi — faqat proxy ishlaydi', C.yellow);
|
|
876
|
+
log('💡', 'nexusai-team papkasida turib avg-nexus bering', C.dim);
|
|
877
|
+
log('', `\n${C.green}${C.bold}✅ Proxy tayyor: http://localhost:${PROXY_PORT}${C.reset}`, '');
|
|
878
|
+
log('', `${C.green}Admin API: http://localhost:${ADMIN_PORT}${C.reset}\n`, '');
|
|
879
|
+
} else {
|
|
880
|
+
log('✅', `Loyiha: ${dir}`, C.green);
|
|
881
|
+
await startNextJS(dir);
|
|
882
|
+
log('', `\n${C.bold}${C.green}🎉 NexusAI tayyor! http://localhost:3000${C.reset}`, '');
|
|
883
|
+
log('', `${C.dim}Proxy: :${PROXY_PORT} Admin: :${ADMIN_PORT} Ctrl+C to'xtatish${C.reset}\n`, '');
|
|
254
884
|
}
|
|
255
|
-
log('✅', `Loyiha: ${dir}`, C.green);
|
|
256
885
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
886
|
+
// Chiqishni ushlab turish
|
|
887
|
+
process.on('SIGINT', () => {
|
|
888
|
+
log('👋', 'To\'xtatildi', C.yellow);
|
|
889
|
+
process.exit(0);
|
|
890
|
+
});
|
|
261
891
|
|
|
262
|
-
|
|
892
|
+
// Har 60 soniyada status
|
|
893
|
+
setInterval(() => {
|
|
894
|
+
const keys = getAllKeys();
|
|
895
|
+
const available = keys.filter(isAvailable).length;
|
|
896
|
+
if (keys.length > 0) {
|
|
897
|
+
log('📊', `Keys: ${available}/${keys.length} available | Uptime: ${Math.floor(process.uptime())}s`, C.dim);
|
|
898
|
+
}
|
|
899
|
+
}, 60000);
|
|
263
900
|
}
|
|
264
901
|
|
|
265
|
-
main().catch(e => { log('❌', e.message, C.red); process.exit(1); });
|
|
902
|
+
main().catch(e => { log('❌', e.message, C.red); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "avg-nexus",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "bin/avg-nexus.js",
|
|
6
|
-
"type": "commonjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node bin/avg-nexus.js"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"ollama",
|
|
15
|
-
"openrouter",
|
|
16
|
-
"proxy",
|
|
17
|
-
"nexusai",
|
|
18
|
-
"offline",
|
|
19
|
-
"ai"
|
|
20
|
-
],
|
|
21
|
-
"author": "avg1002",
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=18.0.0"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "avg-nexus",
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"description": "NexusAI Local Proxy — Ollama + OpenRouter + Device API Manager",
|
|
5
|
+
"main": "bin/avg-nexus.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"avg-nexus": "bin/avg-nexus.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/avg-nexus.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ollama",
|
|
15
|
+
"openrouter",
|
|
16
|
+
"proxy",
|
|
17
|
+
"nexusai",
|
|
18
|
+
"offline",
|
|
19
|
+
"ai"
|
|
20
|
+
],
|
|
21
|
+
"author": "avg1002",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|