From e72d658e2f5bfa0ff2f9b3f4ff75f9048d55e360 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 10 Mar 2026 20:32:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=8B=E5=8A=A8=E8=A7=A6=E5=8F=91=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atsf_agent/internal/agent/runner.go | 10 +++- atsf_agent/internal/protocol/agent_api.go | 1 + atsf_server/common/constants.go | 1 - atsf_server/controller/node.go | 27 ++++++++- atsf_server/model/node.go | 28 ++++----- atsf_server/model/option.go | 3 - atsf_server/router/api-router.go | 2 + atsf_server/service/agent.go | 10 +++- atsf_server/service/node.go | 58 ++++++++++++------- atsf_server/web/src/App.js | 32 ++++++---- .../web/src/components/OperationSetting.js | 26 +++------ .../web/src/components/OtherSetting.js | 30 ++++++---- atsf_server/web/src/helpers/utils.js | 13 +++++ atsf_server/web/src/pages/Node/index.js | 42 ++++++++++++++ docs/deployment.md | 17 +++++- docs/design.md | 11 ++-- docs/development-guidelines.md | 2 +- docs/development-plan.md | 4 +- 18 files changed, 227 insertions(+), 90 deletions(-) diff --git a/atsf_agent/internal/agent/runner.go b/atsf_agent/internal/agent/runner.go index f184b975..954ec6a7 100644 --- a/atsf_agent/internal/agent/runner.go +++ b/atsf_agent/internal/agent/runner.go @@ -35,6 +35,7 @@ type Runner struct { Updater Updater autoUpdate bool + updateNow bool updateRepo string } @@ -130,16 +131,19 @@ func (r *Runner) applySettings(settings *protocol.AgentSettings) bool { } } r.autoUpdate = settings.AutoUpdate - r.updateRepo = settings.UpdateRepo + r.updateNow = settings.UpdateNow + r.updateRepo = strings.TrimSpace(settings.UpdateRepo) return changed } func (r *Runner) tryAutoUpdate(ctx context.Context) { - if !r.autoUpdate || r.Updater == nil || r.updateRepo == "" { + shouldCheck := r.autoUpdate || r.updateNow + r.updateNow = false + if !shouldCheck || r.Updater == nil || r.updateRepo == "" { return } if err := r.Updater.CheckAndUpdate(ctx, r.updateRepo); err != nil { - log.Printf("agent auto-update check failed: %v", err) + log.Printf("agent update check failed: %v", err) } } diff --git a/atsf_agent/internal/protocol/agent_api.go b/atsf_agent/internal/protocol/agent_api.go index 22835c6e..8169872f 100644 --- a/atsf_agent/internal/protocol/agent_api.go +++ b/atsf_agent/internal/protocol/agent_api.go @@ -18,6 +18,7 @@ type AgentSettings struct { SyncInterval int `json:"sync_interval"` AutoUpdate bool `json:"auto_update"` UpdateRepo string `json:"update_repo"` + UpdateNow bool `json:"update_now"` } type NodePayload struct { diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go index 794d4f68..e10e3c16 100644 --- a/atsf_server/common/constants.go +++ b/atsf_server/common/constants.go @@ -53,7 +53,6 @@ var NodeOfflineThreshold = 2 * time.Minute // V3 operational settings (hot-reloadable via Option table) var AgentHeartbeatInterval = 30000 // milliseconds var AgentSyncInterval = 30000 // milliseconds -var AgentAutoUpdate = false var AgentUpdateRepo = "Rain-kl/ATSFlare" const ( diff --git a/atsf_server/controller/node.go b/atsf_server/controller/node.go index 39943667..61540f8a 100644 --- a/atsf_server/controller/node.go +++ b/atsf_server/controller/node.go @@ -3,9 +3,10 @@ package controller import ( "atsflare/service" "encoding/json" - "github.com/gin-gonic/gin" "net/http" "strconv" + + "github.com/gin-gonic/gin" ) func CreateNode(c *gin.Context) { @@ -117,3 +118,27 @@ func DeleteNode(c *gin.Context) { "message": "", }) } + +func RequestNodeAgentUpdate(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil || id == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "无效的参数", + }) + return + } + node, err := service.RequestNodeAgentUpdate(uint(id)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": node, + }) +} diff --git a/atsf_server/model/node.go b/atsf_server/model/node.go index b63a7e2b..b3b7b752 100644 --- a/atsf_server/model/node.go +++ b/atsf_server/model/node.go @@ -3,19 +3,21 @@ package model import "time" type Node struct { - ID uint `json:"id" gorm:"primaryKey"` - NodeID string `json:"node_id" gorm:"uniqueIndex;size:64;not null"` - Name string `json:"name" gorm:"size:128;not null"` - IP string `json:"ip" gorm:"size:64;not null"` - AgentToken string `json:"-" gorm:"size:128;index"` - AgentVersion string `json:"agent_version" gorm:"size:64;not null"` - NginxVersion string `json:"nginx_version" gorm:"size:64"` - Status string `json:"status" gorm:"size:16;not null;default:'offline'"` - CurrentVersion string `json:"current_version" gorm:"size:32"` - LastSeenAt time.Time `json:"last_seen_at"` - LastError string `json:"last_error" gorm:"size:1024"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id" gorm:"primaryKey"` + NodeID string `json:"node_id" gorm:"uniqueIndex;size:64;not null"` + Name string `json:"name" gorm:"size:128;not null"` + IP string `json:"ip" gorm:"size:64;not null"` + AgentToken string `json:"-" gorm:"size:128;index"` + AutoUpdateEnabled bool `json:"auto_update_enabled" gorm:"not null;default:false"` + UpdateRequested bool `json:"update_requested" gorm:"not null;default:false"` + AgentVersion string `json:"agent_version" gorm:"size:64;not null"` + NginxVersion string `json:"nginx_version" gorm:"size:64"` + Status string `json:"status" gorm:"size:16;not null;default:'offline'"` + CurrentVersion string `json:"current_version" gorm:"size:32"` + LastSeenAt time.Time `json:"last_seen_at"` + LastError string `json:"last_error" gorm:"size:1024"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func ListNodes() (nodes []*Node, err error) { diff --git a/atsf_server/model/option.go b/atsf_server/model/option.go index 9478e8fe..f1d903fa 100644 --- a/atsf_server/model/option.go +++ b/atsf_server/model/option.go @@ -54,7 +54,6 @@ func InitOptionMap() { common.OptionMap["AgentHeartbeatInterval"] = strconv.Itoa(common.AgentHeartbeatInterval) common.OptionMap["AgentSyncInterval"] = strconv.Itoa(common.AgentSyncInterval) common.OptionMap["NodeOfflineThreshold"] = strconv.Itoa(int(common.NodeOfflineThreshold.Milliseconds())) - common.OptionMap["AgentAutoUpdate"] = strconv.FormatBool(common.AgentAutoUpdate) common.OptionMap["AgentUpdateRepo"] = common.AgentUpdateRepo common.OptionMapRWMutex.Unlock() options, _ := AllOption() @@ -162,8 +161,6 @@ func updateOptionMap(key string, value string) { if v, err := strconv.Atoi(value); err == nil && v > 0 { common.NodeOfflineThreshold = time.Duration(v) * time.Millisecond } - case "AgentAutoUpdate": - common.AgentAutoUpdate = value == "true" case "AgentUpdateRepo": if value != "" { common.AgentUpdateRepo = value diff --git a/atsf_server/router/api-router.go b/atsf_server/router/api-router.go index 9022426f..4a8d4f11 100644 --- a/atsf_server/router/api-router.go +++ b/atsf_server/router/api-router.go @@ -3,6 +3,7 @@ package router import ( "atsflare/controller" "atsflare/middleware" + "github.com/gin-gonic/gin" ) @@ -104,6 +105,7 @@ func SetApiRouter(router *gin.Engine) { nodeRoute.POST("/bootstrap-token/rotate", controller.RotateNodeBootstrapToken) nodeRoute.GET("/", controller.GetNodes) nodeRoute.POST("/", controller.CreateNode) + nodeRoute.POST("/:id/agent-update", controller.RequestNodeAgentUpdate) nodeRoute.PUT("/:id", controller.UpdateNode) nodeRoute.DELETE("/:id", controller.DeleteNode) } diff --git a/atsf_server/service/agent.go b/atsf_server/service/agent.go index 4d2f44f1..f2e12fe2 100644 --- a/atsf_server/service/agent.go +++ b/atsf_server/service/agent.go @@ -49,6 +49,7 @@ type AgentSettings struct { SyncInterval int `json:"sync_interval"` AutoUpdate bool `json:"auto_update"` UpdateRepo string `json:"update_repo"` + UpdateNow bool `json:"update_now"` } type HeartbeatResponse struct { @@ -63,6 +64,8 @@ type NodeView struct { IP string `json:"ip"` AgentToken string `json:"agent_token"` Pending bool `json:"pending"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` + UpdateRequested bool `json:"update_requested"` AgentVersion string `json:"agent_version"` NginxVersion string `json:"nginx_version"` Status string `json:"status"` @@ -87,8 +90,10 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon if err := validateAgentNodePayload(payload); err != nil { return nil, err } + updateNow := node.UpdateRequested applyNodeRuntime(node, payload, true) - if err := model.DB.Model(node).Select("ip", "agent_version", "nginx_version", "status", "current_version", "last_seen_at", "last_error").Updates(node).Error; err != nil { + node.UpdateRequested = false + if err := model.DB.Model(node).Select("ip", "agent_version", "nginx_version", "status", "current_version", "last_seen_at", "last_error", "update_requested").Updates(node).Error; err != nil { return nil, err } return &HeartbeatResponse{ @@ -96,8 +101,9 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon AgentSettings: &AgentSettings{ HeartbeatInterval: common.AgentHeartbeatInterval, SyncInterval: common.AgentSyncInterval, - AutoUpdate: common.AgentAutoUpdate, + AutoUpdate: node.AutoUpdateEnabled, UpdateRepo: common.AgentUpdateRepo, + UpdateNow: updateNow, }, }, nil } diff --git a/atsf_server/service/node.go b/atsf_server/service/node.go index b6e8fc2e..77b59fac 100644 --- a/atsf_server/service/node.go +++ b/atsf_server/service/node.go @@ -11,7 +11,8 @@ import ( ) type NodeInput struct { - Name string `json:"name"` + Name string `json:"name"` + AutoUpdateEnabled bool `json:"auto_update_enabled"` } type NodeBootstrapView struct { @@ -30,11 +31,12 @@ func CreateNode(input NodeInput) (*NodeView, error) { return nil, errors.New("节点名不能为空") } node := &model.Node{ - Name: name, - IP: "", - AgentVersion: "", - NginxVersion: "", - Status: NodeStatusPending, + Name: name, + IP: "", + AgentVersion: "", + NginxVersion: "", + Status: NodeStatusPending, + AutoUpdateEnabled: input.AutoUpdateEnabled, } var err error node.NodeID, err = newServerNodeID() @@ -65,6 +67,7 @@ func UpdateNode(id uint, input NodeInput) (*NodeView, error) { return nil, err } node.Name = name + node.AutoUpdateEnabled = input.AutoUpdateEnabled if err = node.Update(); err != nil { return nil, err } @@ -81,6 +84,19 @@ func DeleteNode(id uint) error { return node.Delete() } +func RequestNodeAgentUpdate(id uint) (*NodeView, error) { + node, err := model.GetNodeByID(id) + if err != nil { + return nil, err + } + node.UpdateRequested = true + if err = model.DB.Model(node).Update("update_requested", true).Error; err != nil { + return nil, err + } + common.SysLog("agent manual update requested: node_id=" + node.NodeID + " name=" + node.Name) + return buildNodeView(node), nil +} + func AuthenticateAgentToken(token string) (*model.Node, error) { token = strings.TrimSpace(token) if token == "" { @@ -149,20 +165,22 @@ func RotateGlobalDiscoveryToken() (*NodeBootstrapView, error) { func buildNodeView(node *model.Node) *NodeView { status := computeNodeStatus(node) view := &NodeView{ - ID: node.ID, - NodeID: node.NodeID, - Name: node.Name, - IP: node.IP, - AgentToken: node.AgentToken, - AgentVersion: node.AgentVersion, - NginxVersion: node.NginxVersion, - Status: status, - CurrentVersion: node.CurrentVersion, - LastSeenAt: node.LastSeenAt, - LastError: node.LastError, - CreatedAt: node.CreatedAt, - UpdatedAt: node.UpdatedAt, - Pending: status == NodeStatusPending, + ID: node.ID, + NodeID: node.NodeID, + Name: node.Name, + IP: node.IP, + AgentToken: node.AgentToken, + AgentVersion: node.AgentVersion, + NginxVersion: node.NginxVersion, + Status: status, + CurrentVersion: node.CurrentVersion, + LastSeenAt: node.LastSeenAt, + LastError: node.LastError, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + Pending: status == NodeStatusPending, + AutoUpdateEnabled: node.AutoUpdateEnabled, + UpdateRequested: node.UpdateRequested, } return view } diff --git a/atsf_server/web/src/App.js b/atsf_server/web/src/App.js index c9836599..88ba6272 100644 --- a/atsf_server/web/src/App.js +++ b/atsf_server/web/src/App.js @@ -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 { diff --git a/atsf_server/web/src/components/OperationSetting.js b/atsf_server/web/src/components/OperationSetting.js index 2cfc1d27..f6c0ef43 100644 --- a/atsf_server/web/src/components/OperationSetting.js +++ b/atsf_server/web/src/components/OperationSetting.js @@ -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 = () => { -
Agent 自动更新
- - - +
Agent 更新源
+ +

+ 自动更新开关和手动更新由节点页单独控制,这里只配置 Agent + 更新仓库。 +

+
{ onChange={handleInputChange} /> - + 保存更新设置 diff --git a/atsf_server/web/src/components/OtherSetting.js b/atsf_server/web/src/components/OtherSetting.js index d2c2b020..7babb4e4 100644 --- a/atsf_server/web/src/components/OtherSetting.js +++ b/atsf_server/web/src/components/OtherSetting.js @@ -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), }); diff --git a/atsf_server/web/src/helpers/utils.js b/atsf_server/web/src/helpers/utils.js index 8cd4e347..11ba15a2 100644 --- a/atsf_server/web/src/helpers/utils.js +++ b/atsf_server/web/src/helpers/utils.js @@ -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 { diff --git a/atsf_server/web/src/pages/Node/index.js b/atsf_server/web/src/pages/Node/index.js index fa16d4df..c2b3be2d 100644 --- a/atsf_server/web/src/pages/Node/index.js +++ b/atsf_server/web/src/pages/Node/index.js @@ -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 ; }; +const renderUpdateMode = (node) => { + if (node.update_requested) { + return ; + } + if (node.auto_update_enabled) { + return ; + } + return ; +}; + 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 })} /> + + setForm({ ...form, auto_update_enabled: !!checked }) + } + style={{ alignSelf: 'flex-end', marginBottom: '0.5rem' }} + /> diff --git a/docs/deployment.md b/docs/deployment.md index 83a67351..19159234 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -229,7 +229,14 @@ curl -fsSL https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/insta ### 7.3 Agent 自动更新 -在管理端 **运维设置** 中启用「Agent 自动更新」后,Agent 会在每次心跳周期检查 GitHub Releases,发现新版本时自动下载并重启。 +Agent 自动更新默认为关闭。 + +在管理端 **节点管理** 页面中可以: + +* 为单个节点开启「自动更新」 +* 为单个节点点击「立即更新」,下发一次性更新指令 + +节点在收到对应心跳响应后,会检查 GitHub Releases,发现新版本时自动下载并重启。 --- @@ -252,9 +259,15 @@ GitHub Release 中的 Agent 二进制命名格式: | AgentHeartbeatInterval | 心跳间隔(毫秒) | heartbeat_interval | | AgentSyncInterval | 同步间隔(毫秒) | sync_interval | | NodeOfflineThreshold | 节点离线阈值(毫秒) | - | -| AgentAutoUpdate | 是否启用自动更新 | auto_update | | AgentUpdateRepo | 自动更新仓库 | update_repo | +节点管理页下发的字段: + +| 参数 | 说明 | Agent 字段 | +| ---------------------- | ------------------ | ----------- | +| Node.AutoUpdateEnabled | 节点是否自动更新 | auto_update | +| Node.UpdateRequested | 节点一次性更新请求 | update_now | + --- ## 10. 当前已知限制 diff --git a/docs/design.md b/docs/design.md index f3516d88..deeaf688 100644 --- a/docs/design.md +++ b/docs/design.md @@ -347,7 +347,6 @@ Agent 接口当前覆盖: * `AgentHeartbeatInterval`:Agent 心跳上报间隔(毫秒),默认 30000 * `AgentSyncInterval`:Agent 配置同步间隔(毫秒),默认 30000 * `NodeOfflineThreshold`:节点离线判定阈值(毫秒),默认 120000 - * `AgentAutoUpdate`:是否允许 Agent 自动更新(`true`/`false`),默认 `false` * `AgentUpdateRepo`:Agent 自动更新 GitHub 仓库地址,默认 `Rain-kl/ATSFlare` * 环境变量类设置(`SESSION_SECRET`、`SQLITE_PATH`、`PORT`)不迁移,保留原有方式 * 前端在设置页面新增「运维设置」Tab @@ -364,8 +363,9 @@ Agent 接口当前覆盖: * 心跳响应新增 `agent_settings` 字段,包含 Server 端可控的运行时参数: * `heartbeat_interval`(毫秒) * `sync_interval`(毫秒) - * `auto_update`(布尔值) + * `auto_update`(节点级布尔值) * `update_repo`(GitHub 仓库名) + * `update_now`(一次性手动更新指令) * Agent 收到心跳响应后,动态调整本地定时器间隔 * 当 Server 未返回 `agent_settings` 或字段为空时,Agent 保持本地值不变 * Agent 不持久化 Server 下发的间隔值,重启后以本地 `agent.json` 为准,再由下次心跳覆盖 @@ -378,7 +378,7 @@ Agent 接口当前覆盖: 第三版变更: -* Agent 在收到 `auto_update=true` 时: + * Agent 在收到 `auto_update=true` 或 `update_now=true` 时: * 通过 GitHub Releases API 查询 `update_repo` 的最新 Release * 比较本地 `agent_version` 与远端 tag * 若存在更新,下载对应平台的二进制文件 @@ -387,6 +387,8 @@ Agent 接口当前覆盖: * 更新过程中不中断当前同步任务 * 更新失败不影响正常心跳与同步 * Agent 二进制文件命名约定:`atsflare-agent-{os}-{arch}` + * `auto_update` 默认关闭,由节点管理页逐节点开启 + * `update_now` 由节点管理页手动触发,一次心跳消费一次 ### 11.5 Agent 一键部署 @@ -443,13 +445,14 @@ Agent 接口当前覆盖: * Agent 心跳间隔 * Agent 同步间隔 * 节点离线阈值 - * Agent 自动更新开关 * Agent 更新仓库 * 全局 Discovery Token 展示与重新生成 * Agent 一键部署命令展示(根据当前 ServerAddress 和 DiscoveryToken 动态生成 curl 命令) * 节点列表页优化: * 时间显示改为友好的相对时间格式 * 节点状态使用颜色标识 + * 支持逐节点开启自动更新 + * 支持逐节点手动触发一次 Agent 更新 --- diff --git a/docs/development-guidelines.md b/docs/development-guidelines.md index d619f4b1..672d56df 100644 --- a/docs/development-guidelines.md +++ b/docs/development-guidelines.md @@ -205,7 +205,7 @@ Agent 必须满足: V3 新增行为: * 心跳响应包含 `agent_settings` 时,动态调整定时器间隔 -* `auto_update=true` 时在每次心跳后检查 GitHub Releases 更新 +* `auto_update=true` 或 `update_now=true` 时在每次心跳后检查 GitHub Releases 更新 * 自我更新失败不影响心跳与同步 * Server 下发的间隔值不持久化到 `agent.json`,重启后以本地为准 * Agent 新增 `internal/updater` 模块处理自我更新逻辑 diff --git a/docs/development-plan.md b/docs/development-plan.md index 32749795..643f9023 100644 --- a/docs/development-plan.md +++ b/docs/development-plan.md @@ -53,7 +53,6 @@ * `AgentHeartbeatInterval`(默认 30000ms) * `AgentSyncInterval`(默认 30000ms) * `NodeOfflineThreshold`(默认 120000ms) - * `AgentAutoUpdate`(默认 false) * `AgentUpdateRepo`(默认 `Rain-kl/ATSFlare`) 2. 在 `model/option.go` 的 `InitOptionMap()` 注册新选项 3. 在 `model/option.go` 的 `updateOptionMap()` 增加对新选项的同步 @@ -101,13 +100,14 @@ * 下载对应平台二进制 * 替换自身并重启(exec syscall) 2. 在 `runner.go` 心跳循环中集成更新检查 -3. 更新触发条件:`auto_update=true` 且存在新版本 +3. 更新触发条件:节点 `auto_update=true` 或收到一次性 `update_now=true` 且存在新版本 验收标准: * Agent 能正确检测新版本 * 能下载并替换自身二进制 * 更新后自动重启并恢复心跳 + * 默认不自动更新,需由控制面板逐节点开启或手动触发 * 更新失败不影响正常运行 ### 3.4 阶段四:GitHub Actions 完善与 Agent 一键部署