feat: 添加节点自动更新功能,支持手动触发更新

This commit is contained in:
ryan
2026-03-10 20:32:27 +08:00
parent 83a11ead2b
commit e72d658e2f
18 changed files with 227 additions and 90 deletions
+21 -11
View File
@@ -9,7 +9,14 @@ import NotFound from './pages/NotFound';
import Setting from './pages/Setting';
import EditUser from './pages/User/EditUser';
import AddUser from './pages/User/AddUser';
import { API, showError, showNotice } from './helpers';
import {
API,
getBuildVersion,
hasBuildVersion,
normalizeVersion,
showError,
showNotice,
} from './helpers';
import PasswordResetForm from './components/PasswordResetForm';
import GitHubOAuth from './components/GitHubOAuth';
import PasswordResetConfirm from './components/PasswordResetConfirm';
@@ -44,16 +51,19 @@ function App() {
console.log(`GitHub 仓库地址:https://github.com/Rain-kl/ATSFlare`);
localStorage.setItem('status', JSON.stringify(data));
statusDispatch({ type: 'set', payload: data });
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('home_page_link', data.home_page_link);
if (
data.version !== process.env.REACT_APP_VERSION &&
data.version !== 'v0.0.0' &&
process.env.REACT_APP_VERSION !== ''
) {
showNotice(
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('home_page_link', data.home_page_link);
const currentBuildVersion = getBuildVersion();
const latestServerVersion = normalizeVersion(data.version);
if (
hasBuildVersion() &&
latestServerVersion !== '' &&
latestServerVersion !== '0.0.0' &&
latestServerVersion !== currentBuildVersion
) {
showNotice(
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
);
}
} else {
@@ -17,7 +17,6 @@ const OperationSetting = () => {
AgentHeartbeatInterval: '30000',
AgentSyncInterval: '30000',
NodeOfflineThreshold: '120000',
AgentAutoUpdate: 'false',
AgentUpdateRepo: 'Rain-kl/ATSFlare',
ServerAddress: '',
AgentDiscoveryToken: '',
@@ -77,10 +76,6 @@ const OperationSetting = () => {
setInputs((prev) => ({ ...prev, [name]: value }));
};
const handleCheckboxChange = (e, { name, checked }) => {
setInputs((prev) => ({ ...prev, [name]: checked ? 'true' : 'false' }));
};
const submitAgentIntervals = async () => {
const hb = parseInt(inputs.AgentHeartbeatInterval, 10);
const sync = parseInt(inputs.AgentSyncInterval, 10);
@@ -102,8 +97,7 @@ const OperationSetting = () => {
await updateOption('NodeOfflineThreshold', String(offline));
};
const submitAutoUpdate = async () => {
await updateOption('AgentAutoUpdate', inputs.AgentAutoUpdate);
const submitUpdateRepo = async () => {
await updateOption('AgentUpdateRepo', inputs.AgentUpdateRepo);
};
@@ -178,15 +172,13 @@ const OperationSetting = () => {
</Form.Button>
<Divider />
<Header as='h3'>Agent 自动更新</Header>
<Form.Group>
<Form.Checkbox
label='启用 Agent 自动更新'
checked={inputs.AgentAutoUpdate === 'true'}
name='AgentAutoUpdate'
onChange={handleCheckboxChange}
/>
</Form.Group>
<Header as='h3'>Agent 更新源</Header>
<Message info size='small'>
<p>
自动更新开关和手动更新由节点页单独控制,这里只配置 Agent
更新仓库。
</p>
</Message>
<Form.Group widths='equal'>
<Form.Input
label='更新仓库'
@@ -196,7 +188,7 @@ const OperationSetting = () => {
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitAutoUpdate} primary>
<Form.Button onClick={submitUpdateRepo} primary>
保存更新设置
</Form.Button>
+20 -10
View File
@@ -1,6 +1,13 @@
import React, { useEffect, useState } from 'react';
import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import {
API,
getBuildVersion,
hasBuildVersion,
normalizeVersion,
showError,
showSuccess,
} from '../helpers';
import { marked } from 'marked';
const OtherSetting = () => {
@@ -81,15 +88,18 @@ const OtherSetting = () => {
window.location = 'https://github.com/Rain-kl/ATSFlare/releases/latest';
};
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/Rain-kl/ATSFlare/releases/latest'
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/Rain-kl/ATSFlare/releases/latest'
);
const { tag_name, body } = res.data;
if (
hasBuildVersion() &&
normalizeVersion(tag_name) === getBuildVersion()
) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
tag_name: tag_name,
content: marked.parse(body),
});
+13
View File
@@ -25,6 +25,19 @@ export function getFooterHTML() {
return localStorage.getItem('footer_html');
}
export function normalizeVersion(value) {
if (!value) return '';
return String(value).trim().replace(/^v/i, '');
}
export function getBuildVersion() {
return normalizeVersion(process.env.REACT_APP_VERSION);
}
export function hasBuildVersion() {
return getBuildVersion() !== '';
}
export async function copy(text, label) {
let okay = true;
try {
+42
View File
@@ -4,6 +4,7 @@ import { API, showError, showSuccess, timeAgo } from '../../helpers';
const initialForm = {
name: '',
auto_update_enabled: false,
};
const renderStatus = (status) => {
@@ -26,6 +27,16 @@ const renderApply = (result) => {
return <Label>暂无</Label>;
};
const renderUpdateMode = (node) => {
if (node.update_requested) {
return <Label color='orange'>等待更新</Label>;
}
if (node.auto_update_enabled) {
return <Label color='green'>自动更新</Label>;
}
return <Label>手动更新</Label>;
};
const Node = () => {
const [nodes, setNodes] = useState([]);
const [bootstrap, setBootstrap] = useState({ discovery_token: '' });
@@ -91,6 +102,7 @@ const Node = () => {
setSubmitting(true);
const payload = {
name: form.name.trim(),
auto_update_enabled: form.auto_update_enabled,
};
const res = editingId
? await API.put(`/api/nodes/${editingId}`, payload)
@@ -110,9 +122,21 @@ const Node = () => {
setEditingId(node.id);
setForm({
name: node.name || '',
auto_update_enabled: !!node.auto_update_enabled,
});
};
const requestAgentUpdate = async (node) => {
const res = await API.post(`/api/nodes/${node.id}/agent-update`);
const { success, message } = res.data;
if (success) {
showSuccess(`已向节点 ${node.name} 下发更新指令`);
await loadNodes(false);
} else {
showError(message);
}
};
const deleteNode = async (node) => {
if (
!window.confirm(
@@ -163,6 +187,14 @@ const Node = () => {
value={form.name}
onChange={(e, { value }) => setForm({ ...form, name: value })}
/>
<Form.Checkbox
label='启用该节点自动更新'
checked={form.auto_update_enabled}
onChange={(e, { checked }) =>
setForm({ ...form, auto_update_enabled: !!checked })
}
style={{ alignSelf: 'flex-end', marginBottom: '0.5rem' }}
/>
</Form.Group>
<Button primary type='submit' loading={submitting}>
{editingId ? '保存修改' : '新增节点'}
@@ -182,6 +214,7 @@ const Node = () => {
<Table.HeaderCell>Auth Token</Table.HeaderCell>
<Table.HeaderCell>IP</Table.HeaderCell>
<Table.HeaderCell>状态</Table.HeaderCell>
<Table.HeaderCell>Agent 更新</Table.HeaderCell>
<Table.HeaderCell>Agent / Nginx</Table.HeaderCell>
<Table.HeaderCell>当前版本</Table.HeaderCell>
<Table.HeaderCell>最近应用</Table.HeaderCell>
@@ -216,6 +249,7 @@ const Node = () => {
</Table.Cell>
<Table.Cell>{node.ip}</Table.Cell>
<Table.Cell>{renderStatus(node.status)}</Table.Cell>
<Table.Cell>{renderUpdateMode(node)}</Table.Cell>
<Table.Cell>
{node.agent_version} / {node.nginx_version || 'unknown'}
</Table.Cell>
@@ -231,6 +265,14 @@ const Node = () => {
</Table.Cell>
<Table.Cell>{node.last_error || '无'}</Table.Cell>
<Table.Cell>
<Button
size='small'
primary
disabled={node.update_requested}
onClick={() => requestAgentUpdate(node)}
>
立即更新
</Button>
<Button size='small' onClick={() => beginEdit(node)}>
编辑
</Button>