feat:自动更换ip+流量监控

This commit is contained in:
2026-01-07 17:19:53 +08:00
commit 035da64084
27 changed files with 6182 additions and 0 deletions

430
templates/machines.html Normal file
View File

@@ -0,0 +1,430 @@
{% extends "base.html" %}
{% block title %}节点管理 - ProxyAuto Pro{% endblock %}
{% block content %}
<div class="page-header">
<h1>节点管理</h1>
</div>
<section class="section">
<div class="collapsible" id="add-machine-section">
<button class="collapsible-header" onclick="toggleCollapsible('add-machine-section')">
<svg class="collapsible-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span>添加新节点</span>
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="collapsible-content" {% if machines|length > 0 %}style="display: none;"{% endif %}>
<form id="add-machine-form" class="form-card inline-form">
<h3 class="form-section-title">基本信息</h3>
<div class="form-grid">
<div class="form-group">
<label for="new_name">节点名称</label>
<input type="text" id="new_name" name="name" placeholder="例如: US-East-Proxy" required>
</div>
<div class="form-group">
<label for="new_aws_service">AWS 服务</label>
<select id="new_aws_service" name="aws_service">
<option value="ec2">EC2</option>
<option value="lightsail">Lightsail</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="new_aws_region">AWS 区域 (Region)</label>
<input type="text" id="new_aws_region" name="aws_region" value="us-east-1" placeholder="例如: us-west-2" required>
</div>
<div class="form-group">
<label for="new_instance_id" id="new_instance_id_label">EC2 Instance ID</label>
<input type="text" id="new_instance_id" name="instance_id" placeholder="例如: i-0123456789abcdef0" required>
</div>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">域名绑定 (可选)</h3>
<div class="form-grid">
<div class="form-group">
<label for="new_cf_zone_id">Cloudflare Zone ID</label>
<input type="text" id="new_cf_zone_id" name="cf_zone_id" placeholder="留空则不绑定域名">
</div>
<div class="form-group">
<label for="new_cf_record_name">DNS 记录名称</label>
<input type="text" id="new_cf_record_name" name="cf_record_name" placeholder="例如: api.example.com">
</div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="new_cf_proxied" name="cf_proxied">
<span class="checkbox-text">启用 Cloudflare 代理 (橙色云朵)</span>
</label>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">自动更换设置</h3>
<div class="form-grid">
<div class="form-group">
<label for="new_change_interval">更换间隔 (分钟)</label>
<input type="number" id="new_change_interval" name="change_interval_minutes" value="60" min="1">
</div>
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
<label class="checkbox-label">
<input type="checkbox" id="new_auto_enabled" name="auto_enabled">
<span class="checkbox-text">启用自动更换</span>
</label>
</div>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">流量预警 (可选)</h3>
<div class="form-grid">
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
<label class="checkbox-label">
<input type="checkbox" id="new_traffic_alert_enabled" name="traffic_alert_enabled" onchange="toggleTrafficAlertFields('new')">
<span class="checkbox-text">启用流量预警</span>
</label>
</div>
<div class="form-group" id="new_traffic_limit_group" style="display: none;">
<label for="new_traffic_alert_limit_gb" id="new_traffic_limit_label">流量限制 (GB)</label>
<input type="number" id="new_traffic_alert_limit_gb" name="traffic_alert_limit_gb" step="0.1" min="0" placeholder="例如: 100">
<small class="form-hint" id="new_traffic_limit_hint">EC2: 上传流量预警 / Lightsail: 总流量预警</small>
</div>
</div>
<div class="form-divider"></div>
<div class="form-group">
<label for="new_note">备注 (可选)</label>
<input type="text" id="new_note" name="note" placeholder="例如: 生产环境主节点">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="new_enabled" name="enabled" checked>
<span class="checkbox-text">立即启用</span>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">添加节点</button>
</div>
</form>
</div>
</div>
</section>
<div class="divider"></div>
<section class="section">
{% if not machines %}
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/>
<line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>
<h3>暂无节点</h3>
<p>请点击上方"添加新节点"开始配置</p>
</div>
{% else %}
<div class="machine-list">
{% for machine in machines %}
<div class="machine-card" data-id="{{ machine.id }}">
<div class="machine-header">
<div class="machine-info">
<span class="status-dot {% if machine.enabled %}active{% else %}inactive{% endif %}"></span>
<span class="machine-name">{{ machine.name }}</span>
{% if machine.auto_enabled %}
<span class="badge badge-auto">自动</span>
{% endif %}
</div>
<div class="machine-meta">
<span class="meta-item">{{ machine.aws_service|upper }}</span>
<span class="meta-item">{{ machine.aws_region }}</span>
{% if machine.current_ip %}
<span class="meta-item ip-badge">{{ machine.current_ip }}</span>
{% endif %}
{% if machine.cf_record_name %}
<span class="meta-item domain-badge">{{ machine.cf_record_name }}</span>
{% endif %}
</div>
<div class="machine-actions">
<button class="btn btn-sm btn-ghost" onclick="toggleEdit({{ machine.id }})">编辑</button>
<button class="btn btn-sm btn-danger-ghost" onclick="deleteMachine({{ machine.id }}, '{{ machine.name }}')">删除</button>
</div>
</div>
{% if machine.note %}
<div class="machine-note-row">{{ machine.note }}</div>
{% endif %}
<div class="machine-edit" id="edit-{{ machine.id }}" style="display: none;">
<form class="edit-form" onsubmit="updateMachine(event, {{ machine.id }})">
<h3 class="form-section-title">基本信息</h3>
<div class="form-grid">
<div class="form-group">
<label>名称</label>
<input type="text" name="name" value="{{ machine.name }}" required>
</div>
<div class="form-group">
<label>服务类型</label>
<select name="aws_service">
<option value="ec2" {% if machine.aws_service == 'ec2' %}selected{% endif %}>EC2</option>
<option value="lightsail" {% if machine.aws_service == 'lightsail' %}selected{% endif %}>Lightsail</option>
</select>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label>区域</label>
<input type="text" name="aws_region" value="{{ machine.aws_region }}" required>
</div>
<div class="form-group">
<label>实例 ID</label>
<input type="text" name="instance_id" value="{{ machine.aws_instance_id }}" required>
</div>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">域名绑定</h3>
<div class="form-grid">
<div class="form-group">
<label>Cloudflare Zone ID</label>
<input type="text" name="cf_zone_id" value="{{ machine.cf_zone_id or '' }}" placeholder="留空则不绑定域名">
</div>
<div class="form-group">
<label>DNS 记录名称</label>
<input type="text" name="cf_record_name" value="{{ machine.cf_record_name or '' }}" placeholder="例如: api.example.com">
</div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="cf_proxied" {% if machine.cf_proxied %}checked{% endif %}>
<span class="checkbox-text">启用 Cloudflare 代理</span>
</label>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">自动更换设置</h3>
<div class="form-grid">
<div class="form-group">
<label>更换间隔 (分钟)</label>
<input type="number" name="change_interval_minutes" value="{{ machine.change_interval_seconds // 60 }}" min="1">
</div>
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
<label class="checkbox-label">
<input type="checkbox" name="auto_enabled" {% if machine.auto_enabled %}checked{% endif %}>
<span class="checkbox-text">启用自动更换</span>
</label>
</div>
</div>
<div class="form-divider"></div>
<h3 class="form-section-title">流量预警</h3>
<div class="form-grid">
<div class="form-group" style="display: flex; align-items: center; padding-top: 1.5rem;">
<label class="checkbox-label">
<input type="checkbox" name="traffic_alert_enabled" {% if machine.traffic_alert_enabled %}checked{% endif %} onchange="toggleTrafficAlertFields('edit-{{ machine.id }}')">
<span class="checkbox-text">启用流量预警</span>
</label>
{% if machine.traffic_alert_triggered %}
<span class="badge" style="background: var(--error-soft); color: var(--error); margin-left: 0.5rem;">已触发预警</span>
{% endif %}
</div>
<div class="form-group traffic-limit-group" {% if not machine.traffic_alert_enabled %}style="display: none;"{% endif %}>
<label>流量限制 (GB)</label>
<input type="number" name="traffic_alert_limit_gb" value="{{ machine.traffic_alert_limit_gb or '' }}" step="0.1" min="0" placeholder="例如: 100">
<small class="form-hint">{{ 'Lightsail: 总流量预警' if machine.aws_service == 'lightsail' else 'EC2: 上传流量预警' }}</small>
</div>
</div>
{% if machine.traffic_alert_triggered %}
<div class="form-group" style="margin-top: 0.5rem;">
<button type="button" class="btn btn-sm btn-secondary" onclick="resetTrafficAlert({{ machine.id }})">重置预警状态</button>
</div>
{% endif %}
<div class="form-divider"></div>
<div class="form-group">
<label>备注</label>
<input type="text" name="note" value="{{ machine.note or '' }}">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="enabled" {% if machine.enabled %}checked{% endif %}>
<span class="checkbox-text">启用</span>
</label>
</div>
<div class="form-actions inline">
<button type="submit" class="btn btn-primary btn-sm">保存修改</button>
<button type="button" class="btn btn-ghost btn-sm" onclick="toggleEdit({{ machine.id }})">取消</button>
</div>
</form>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</section>
{% endblock %}
{% block scripts %}
<script>
// 切换服务类型标签
document.getElementById('new_aws_service').addEventListener('change', function() {
const label = document.getElementById('new_instance_id_label');
const input = document.getElementById('new_instance_id');
const trafficHint = document.getElementById('new_traffic_limit_hint');
if (this.value === 'lightsail') {
label.textContent = 'Lightsail 实例名';
input.placeholder = '例如: my-lightsail-instance';
if (trafficHint) trafficHint.textContent = 'Lightsail: 总流量 (上传+下载) 预警';
} else {
label.textContent = 'EC2 Instance ID';
input.placeholder = '例如: i-0123456789abcdef0';
if (trafficHint) trafficHint.textContent = 'EC2: 上传流量预警';
}
});
// 切换流量预警字段显示
function toggleTrafficAlertFields(prefix) {
if (prefix === 'new') {
const checkbox = document.getElementById('new_traffic_alert_enabled');
const group = document.getElementById('new_traffic_limit_group');
group.style.display = checkbox.checked ? 'block' : 'none';
} else {
const editPanel = document.getElementById(prefix);
const checkbox = editPanel.querySelector('[name="traffic_alert_enabled"]');
const group = editPanel.querySelector('.traffic-limit-group');
if (group) group.style.display = checkbox.checked ? 'block' : 'none';
}
}
// 重置流量预警
async function resetTrafficAlert(machineId) {
try {
const response = await fetch(`/api/machines/${machineId}/reset-alert`, { method: 'POST' });
const result = await response.json();
if (result.ok) {
showToast('success', result.message);
setTimeout(() => location.reload(), 1000);
} else {
showToast('error', result.message);
}
} catch (error) {
showToast('error', `重置失败: ${error.message}`);
}
}
// 添加节点
document.getElementById('add-machine-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
name: document.getElementById('new_name').value,
aws_service: document.getElementById('new_aws_service').value,
aws_region: document.getElementById('new_aws_region').value,
instance_id: document.getElementById('new_instance_id').value,
note: document.getElementById('new_note').value,
enabled: document.getElementById('new_enabled').checked,
cf_zone_id: document.getElementById('new_cf_zone_id').value,
cf_record_name: document.getElementById('new_cf_record_name').value,
cf_proxied: document.getElementById('new_cf_proxied').checked,
change_interval_minutes: parseInt(document.getElementById('new_change_interval').value) || 60,
auto_enabled: document.getElementById('new_auto_enabled').checked,
traffic_alert_enabled: document.getElementById('new_traffic_alert_enabled').checked,
traffic_alert_limit_gb: parseFloat(document.getElementById('new_traffic_alert_limit_gb').value) || null,
};
try {
const response = await fetch('/api/machines', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.ok) {
showToast('success', '节点添加成功!');
setTimeout(() => location.reload(), 1000);
} else {
const msg = result.errors ? result.errors.join(', ') : result.message;
showToast('error', msg);
}
} catch (error) {
showToast('error', `添加失败: ${error.message}`);
}
});
function toggleCollapsible(id) {
const section = document.getElementById(id);
const content = section.querySelector('.collapsible-content');
const isOpen = content.style.display !== 'none';
content.style.display = isOpen ? 'none' : 'block';
section.classList.toggle('open', !isOpen);
}
function toggleEdit(id) {
const editPanel = document.getElementById(`edit-${id}`);
editPanel.style.display = editPanel.style.display === 'none' ? 'block' : 'none';
}
async function updateMachine(e, id) {
e.preventDefault();
const form = e.target;
const formData = {
name: form.querySelector('[name="name"]').value,
aws_service: form.querySelector('[name="aws_service"]').value,
aws_region: form.querySelector('[name="aws_region"]').value,
instance_id: form.querySelector('[name="instance_id"]').value,
note: form.querySelector('[name="note"]').value,
enabled: form.querySelector('[name="enabled"]').checked,
cf_zone_id: form.querySelector('[name="cf_zone_id"]').value,
cf_record_name: form.querySelector('[name="cf_record_name"]').value,
cf_proxied: form.querySelector('[name="cf_proxied"]').checked,
change_interval_minutes: parseInt(form.querySelector('[name="change_interval_minutes"]').value) || 60,
auto_enabled: form.querySelector('[name="auto_enabled"]').checked,
traffic_alert_enabled: form.querySelector('[name="traffic_alert_enabled"]').checked,
traffic_alert_limit_gb: parseFloat(form.querySelector('[name="traffic_alert_limit_gb"]').value) || null,
};
try {
const response = await fetch(`/api/machines/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.ok) {
showToast('success', '节点已更新');
setTimeout(() => location.reload(), 1000);
} else {
showToast('error', result.message);
}
} catch (error) {
showToast('error', `更新失败: ${error.message}`);
}
}
async function deleteMachine(id, name) {
if (!confirm(`确定要删除节点 "${name}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/machines/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.ok) {
showToast('success', '节点已删除');
setTimeout(() => location.reload(), 1000);
} else {
showToast('error', result.message);
}
} catch (error) {
showToast('error', `删除失败: ${error.message}`);
}
}
</script>
{% endblock %}