add: rotation status
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip } from '@mui/material';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton } from '@mui/material';
|
||||
import api from '../api';
|
||||
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Schedule, Update } from '@mui/icons-material';
|
||||
|
||||
const ROTATION_PRESETS = [
|
||||
{ label: 'Сутки', value: 1440 },
|
||||
@@ -14,6 +15,8 @@ export default function SettingsPage() {
|
||||
xui_login: '',
|
||||
xui_password: '',
|
||||
rotation_interval: '30',
|
||||
rotation_status: 'active',
|
||||
last_rotation_timestamp: '',
|
||||
});
|
||||
|
||||
const [adminProfile, setAdminProfile] = useState({
|
||||
@@ -125,20 +128,20 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceRotate = async () => {
|
||||
const handleForceRotate = async () => {
|
||||
if (confirm('ВНИМАНИЕ: Это немедленно обновит конфиги в подписках.\n\nИнтервал автоматической ротации НЕ будет сброшен.\n\nПродолжить?')) {
|
||||
try {
|
||||
setLoadingRotate(true);
|
||||
const res = await api.post('/rotation/rotate-all');
|
||||
|
||||
|
||||
setLoadingRotate(false);
|
||||
if (res.data && res.data.success) {
|
||||
setMsg({ open: true, type: 'success', text: res.data.message || 'Ротация успешно выполнена!' });
|
||||
} else {
|
||||
setMsg({
|
||||
open: true,
|
||||
type: 'error',
|
||||
text: res.data?.message || 'Ошибка выполнения ротации'
|
||||
setMsg({
|
||||
open: true,
|
||||
type: 'error',
|
||||
text: res.data?.message || 'Ошибка выполнения ротации'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -148,12 +151,105 @@ const handleForceRotate = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const togglePause = async () => {
|
||||
const newStatus = settings.rotation_status === 'active' ? 'stopped' : 'active';
|
||||
const updatedSettings = { ...settings, rotation_status: newStatus };
|
||||
|
||||
setSettings(updatedSettings);
|
||||
|
||||
try {
|
||||
await api.post('/settings', updatedSettings);
|
||||
|
||||
} catch (e) {
|
||||
setSettings((prev: any) => ({ ...prev, rotation_status: settings.rotation_status }));
|
||||
setMsg({ open: true, type: 'error', text: 'Не удалось изменить статус' });
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (isoString: string) => {
|
||||
if (!isoString) return 'Нет данных';
|
||||
return new Date(+isoString).toLocaleString('ru-RU', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const getNextRotationDate = () => {
|
||||
if (settings.rotation_status === 'stopped') return 'Пауза';
|
||||
if (!settings.last_rotation_timestamp) return 'Ожидание...';
|
||||
|
||||
const last = new Date(+settings.last_rotation_timestamp);
|
||||
const intervalMinutes = parseInt(settings.rotation_interval) || 60;
|
||||
const next = new Date(last.getTime() + intervalMinutes * 60000);
|
||||
|
||||
return next.toLocaleString('ru-RU', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const isPaused = settings.rotation_status === 'stopped';
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Настройки утилиты</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
|
||||
Статус сервиса
|
||||
</Typography>
|
||||
{isPaused ?
|
||||
<Chip icon={<PauseCircleFilled />} label="Остановлен" color="warning" size="small" variant="outlined" /> :
|
||||
<Chip icon={<CheckCircle />} label="Активен" color="success" size="small" variant="outlined" />
|
||||
}
|
||||
|
||||
<Tooltip title={isPaused ? "Возобновить ротацию" : "Поставить на паузу"}>
|
||||
<IconButton
|
||||
onClick={togglePause}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
'&:hover': { bgcolor: 'background.paper' },
|
||||
ml: 1
|
||||
}}
|
||||
>
|
||||
{isPaused ? <PlayCircleFilled fontSize="large" /> : <PauseCircleFilled fontSize="large" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
{/* Последняя генерация */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Последняя генерация
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
|
||||
{formatDate(settings.last_rotation_timestamp)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Следующая генерация */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Следующая генерация
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
|
||||
{getNextRotationDate()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 3, height: '100%' }}>
|
||||
<Typography variant="h6" gutterBottom>Панель 3x-ui</Typography>
|
||||
|
||||
@@ -191,12 +191,18 @@ export default function SubscriptionsPage() {
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} disableRestoreFocus>
|
||||
<DialogTitle>Новая подписка</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus margin="dense" label="Имя пользователя" fullWidth
|
||||
value={name} onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -215,7 +221,7 @@ export default function SubscriptionsPage() {
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать всё</Button>
|
||||
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать все</Button>
|
||||
<Button onClick={() => setLinksOpen(false)}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -35,7 +35,7 @@ export class InboundBuilderService {
|
||||
},
|
||||
tcpSettings: { acceptProxyProtocol: false, header: { type: 'none' } }
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
sniffing: JSON.stringify({ enabled: false, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class InboundBuilderService {
|
||||
protocol: 'vless',
|
||||
remark: `vless-xhttp-reality`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: 'xtls-rprx-vision', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
fallbacks: []
|
||||
@@ -66,9 +66,23 @@ export class InboundBuilderService {
|
||||
shortIds: [crypto.randomBytes(4).toString('hex'), crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
},
|
||||
xhttpSettings: { path: '/', mode: 'auto' }
|
||||
xhttpSettings: {
|
||||
host: domain,
|
||||
path: "/",
|
||||
mode: "auto",
|
||||
noSSEHeader: false,
|
||||
scMaxBufferedPosts: 30,
|
||||
scMaxEachPostBytes: "1000000",
|
||||
scStreamUpServerSecs: "20-80",
|
||||
xPaddingBytes: "100-1000"
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,17 +91,28 @@ export class InboundBuilderService {
|
||||
return {
|
||||
enable: true,
|
||||
port,
|
||||
protocol: 'vless',
|
||||
remark: `vless-grpc-reality`,
|
||||
protocol: "vless",
|
||||
remark: "vless-reality-grpc",
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
decryption: "none",
|
||||
encryption: "none",
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'grpc',
|
||||
security: 'reality',
|
||||
network: "grpc",
|
||||
security: "reality",
|
||||
externalProxy: [],
|
||||
realitySettings: {
|
||||
show: false,
|
||||
@@ -99,9 +124,18 @@ export class InboundBuilderService {
|
||||
shortIds: [crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
},
|
||||
grpcSettings: { serviceName: 'grpc', multiMode: false }
|
||||
grpcSettings: {
|
||||
serviceName: "myservice",
|
||||
authority: domain,
|
||||
multiMode: false,
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,18 +147,39 @@ export class InboundBuilderService {
|
||||
protocol: 'vless',
|
||||
remark: `vless-ws`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
decryption: "none",
|
||||
encryption: "none",
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'ws',
|
||||
security: 'none',
|
||||
network: "ws",
|
||||
security: "none",
|
||||
externalProxy: [],
|
||||
wsSettings: { path: '/', headers: { Host: domain } }
|
||||
wsSettings: {
|
||||
host: domain,
|
||||
path: "/",
|
||||
acceptProxyProtocol: false,
|
||||
heartbeatPeriod: 0,
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,29 +191,77 @@ export class InboundBuilderService {
|
||||
protocol: 'vmess',
|
||||
remark: 'vmess-tcp',
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, alterId: 0, email: uuid, limitIp: 0, totalGB: 0, expiryTime: 0, enable: true, tgId: '', subId: '', reset: 0 }],
|
||||
disableInsecureEncryption: false
|
||||
clients: [{
|
||||
id: uuid,
|
||||
flow: "",
|
||||
email: uuid,
|
||||
enable: true,
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "0",
|
||||
alterId: "0",
|
||||
reset: 0
|
||||
}],
|
||||
}),
|
||||
streamSettings: JSON.stringify({ network: 'tcp', security: 'none', tcpSettings: { header: { type: 'http', request: { method: 'GET', path: ['/'], headers: { Host: [] } } } } }),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
streamSettings: JSON.stringify({
|
||||
network: "tcp",
|
||||
security: "none",
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
buildShadowsocksTcp(params: { port: number; uuid: string }) {
|
||||
const { port, uuid } = params;
|
||||
const { port, uuid } = params;
|
||||
return {
|
||||
enable: true,
|
||||
port,
|
||||
protocol: 'shadowsocks',
|
||||
remark: 'shadowsocks-tcp',
|
||||
settings: JSON.stringify({
|
||||
method: 'aes-256-gcm',
|
||||
password: uuid,
|
||||
network: 'tcp,udp',
|
||||
clients: []
|
||||
clients: [{
|
||||
id: "",
|
||||
flow: "",
|
||||
email: uuid,
|
||||
password: crypto.randomBytes(32).toString("base64"),
|
||||
enable: true,
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
ivCheck: false,
|
||||
method: "2022-blake3-aes-256-gcm",
|
||||
network: "tcp",
|
||||
password: crypto.randomBytes(32).toString("base64")
|
||||
}),
|
||||
streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
streamSettings: JSON.stringify({
|
||||
network: "tcp",
|
||||
security: "none",
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,12 +273,24 @@ export class InboundBuilderService {
|
||||
protocol: 'trojan',
|
||||
remark: `trojan-tcp-reality`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ password: uuid, email: uuid, limitIp: 0, totalGB: 0, expiryTime: 0, enable: true, tgId: '', subId: '', reset: 0 }],
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
password: crypto.randomBytes(8).toString("hex"),
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
network: "tcp",
|
||||
security: "reality",
|
||||
externalProxy: [],
|
||||
realitySettings: {
|
||||
show: false,
|
||||
@@ -184,11 +299,34 @@ export class InboundBuilderService {
|
||||
dest: `${domain}:443`,
|
||||
serverNames: [domain],
|
||||
privateKey: privateKey,
|
||||
shortIds: [crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
shortIds: [
|
||||
crypto.randomBytes(4).toString("hex"),
|
||||
crypto.randomBytes(3).toString("hex"),
|
||||
crypto.randomBytes(8).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(4).toString("hex")
|
||||
],
|
||||
settings: {
|
||||
publicKey: publicKey,
|
||||
fingerprint: "random",
|
||||
serverName: "",
|
||||
spiderX: "/"
|
||||
}
|
||||
},
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,7 +418,7 @@ export class InboundBuilderService {
|
||||
|
||||
const vmessObj = {
|
||||
add: domain,
|
||||
aid: '',
|
||||
aid: '0',
|
||||
alpn: "",
|
||||
fp: "",
|
||||
host: "",
|
||||
@@ -308,9 +446,9 @@ export class InboundBuilderService {
|
||||
|
||||
const method = settings.method;
|
||||
const serverPassword = settings.password;
|
||||
const finalPass = serverPassword || idOrPass;
|
||||
const clientPassword = settings.clients[0].password;
|
||||
|
||||
const userInfo = `${method}:${finalPass}`;
|
||||
const userInfo = `${method}:${serverPassword}:${clientPassword}`;
|
||||
|
||||
const base64 = Buffer
|
||||
.from(userInfo, "utf8")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -13,7 +13,7 @@ import { InboundBuilderService } from '../inbounds/inbound-builder.service';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class RotationService {
|
||||
export class RotationService implements OnModuleInit {
|
||||
private readonly logger = new Logger(RotationService.name);
|
||||
|
||||
constructor(
|
||||
@@ -25,6 +25,26 @@ export class RotationService {
|
||||
private inboundBuilder: InboundBuilderService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.initDefaultSettings();
|
||||
}
|
||||
|
||||
private async initDefaultSettings() {
|
||||
const key = 'rotation_status';
|
||||
const existing = await this.settingRepo.findOne({ where: { key } });
|
||||
|
||||
if (!existing) {
|
||||
this.logger.log(`Инициализация настройки: ${key} = active`);
|
||||
const newSetting = this.settingRepo.create({
|
||||
key: key,
|
||||
value: 'active',
|
||||
});
|
||||
await this.settingRepo.save(newSetting);
|
||||
} else {
|
||||
this.logger.log(`Текущий статус ротации: ${existing.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleTicker() {
|
||||
const intervalSetting = await this.settingRepo.findOne({ where: { key: 'rotation_interval' } });
|
||||
@@ -35,8 +55,10 @@ export class RotationService {
|
||||
|
||||
const now = Date.now();
|
||||
const diffMinutes = (now - lastRun) / 1000 / 60;
|
||||
const statusSetting = await this.settingRepo.findOne({ where: { key: 'rotation_status' } });
|
||||
const isStopped = statusSetting?.value === 'stopped';
|
||||
|
||||
if (diffMinutes < intervalMinutes) {
|
||||
if (diffMinutes < intervalMinutes || isStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +74,7 @@ export class RotationService {
|
||||
await this.settingRepo.save(s);
|
||||
}
|
||||
|
||||
async performRotation() {
|
||||
async performRotation() {
|
||||
this.logger.log('Запуск плановой ротации...');
|
||||
|
||||
const isLoginSuccess = await this.xuiService.login();
|
||||
|
||||
@@ -17,17 +17,10 @@ need_root() {
|
||||
[[ $EUID -eq 0 ]] || die "Запускать только от root"
|
||||
}
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "❌ curl не установлен. Установите curl и повторите попытку"
|
||||
echo " apt install -y curl"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#################################
|
||||
# CONFIG
|
||||
#################################
|
||||
PROJECT_DIR="/opt/3dp-manager"
|
||||
REPO_RAW="https://raw.githubusercontent.com/denpiligrim/3dp-manager/main"
|
||||
|
||||
#################################
|
||||
# START
|
||||
@@ -46,53 +39,21 @@ cd "$PROJECT_DIR"
|
||||
command -v docker >/dev/null 2>&1 || die "Docker не установлен"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose v2 недоступен"
|
||||
|
||||
#################################
|
||||
# DOWNLOAD FILES
|
||||
#################################
|
||||
log "Загружаем обновлённые файлы из репозитория"
|
||||
|
||||
mkdir -p app
|
||||
|
||||
curl -fsSL "$REPO_RAW/app/Dockerfile" -o app/Dockerfile
|
||||
curl -fsSL "$REPO_RAW/app/package.json" -o app/package.json
|
||||
curl -fsSL "$REPO_RAW/app/index.js" -o app/index.js
|
||||
curl -fsSL "$REPO_RAW/app/rotate.js" -o app/rotate.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityTcp.js" -o app/builders/buildVlessRealityTcp.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityXhttp.js" -o app/builders/buildVlessRealityXhttp.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildTrojanRealityTcp.js" -o app/builders/buildTrojanRealityTcp.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildShadowsocksTcp.js" -o app/builders/buildShadowsocksTcp.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildVmessTcp.js" -o app/builders/buildVmessTcp.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityGrpc.js" -o app/builders/buildVlessRealityGrpc.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildVlessWs.js" -o app/builders/buildVlessWs.js
|
||||
curl -fsSL "$REPO_RAW/app/builders/buildInboundLink.js" -o app/builders/buildInboundLink.js
|
||||
curl -fsSL "$REPO_RAW/whitelist.txt" -o whitelist.txt
|
||||
|
||||
log "Файлы обновлены"
|
||||
|
||||
#################################
|
||||
# REBUILD BACKEND
|
||||
#################################
|
||||
log "Пересобираем backend"
|
||||
docker compose build node
|
||||
|
||||
#################################
|
||||
# RESTART CONTAINERS
|
||||
#################################
|
||||
log "Перезапускаем контейнеры"
|
||||
docker compose up -d
|
||||
|
||||
if [ -f "app/my_whitelist.txt" ]; then
|
||||
log "✔ Копируем my_whitelist.txt в контейнер..."
|
||||
docker cp app/my_whitelist.txt node:/app/my_whitelist.txt
|
||||
log "Скачивание последних версий Docker-образов..."
|
||||
if docker compose pull; then
|
||||
log "Образы успешно загружены."
|
||||
else
|
||||
error "Ошибка при скачивании образов. Проверьте подключение к интернету или доступность GitHub Container Registry."
|
||||
fi
|
||||
|
||||
#################################
|
||||
# HEALTH CHECK
|
||||
#################################
|
||||
sleep 2
|
||||
log "Пересоздание контейнеров..."
|
||||
docker compose up -d
|
||||
|
||||
docker compose ps | grep node >/dev/null || die "Backend не запущен"
|
||||
docker compose ps | grep nginx >/dev/null || die "Nginx не запущен"
|
||||
log "Очистка старых Docker-образов (освобождение места)..."
|
||||
docker image prune -f
|
||||
|
||||
#################################
|
||||
# DONE
|
||||
|
||||
Reference in New Issue
Block a user