[优化] 接口优化, 心跳请求返回规则摘要

This commit is contained in:
ryan
2026-03-13 11:22:37 +08:00
parent b33923d5f7
commit 3fb4cec99c
23 changed files with 300 additions and 177 deletions
+1 -2
View File
@@ -51,8 +51,7 @@ var AgentDiscoveryToken = ""
var NodeOfflineThreshold = 2 * time.Minute
// V3 operational settings (hot-reloadable via Option table)
var AgentHeartbeatInterval = 30000 // milliseconds
var AgentSyncInterval = 30000 // milliseconds
var AgentHeartbeatInterval = 10000 // milliseconds
var AgentUpdateRepo = "Rain-kl/ATSFlare"
// V5 OpenResty performance settings (hot-reloadable via Option table)
+1
View File
@@ -91,6 +91,7 @@ func AgentHeartbeat(c *gin.Context) {
"message": "",
"data": node.Node,
"agent_settings": node.AgentSettings,
"active_config": node.ActiveConfig,
})
}
-5
View File
@@ -52,7 +52,6 @@ func InitOptionMap() {
common.OptionMap["TurnstileSecretKey"] = ""
common.OptionMap["AgentDiscoveryToken"] = ""
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["AgentUpdateRepo"] = common.AgentUpdateRepo
common.OptionMap["OpenRestyWorkerProcesses"] = common.OpenRestyWorkerProcesses
@@ -199,10 +198,6 @@ func updateOptionMap(key string, value string) {
if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.AgentHeartbeatInterval = v
}
case "AgentSyncInterval":
if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.AgentSyncInterval = v
}
case "NodeOfflineThreshold":
if v, err := strconv.Atoi(value); err == nil && v > 0 {
common.NodeOfflineThreshold = time.Duration(v) * time.Millisecond
+7 -3
View File
@@ -284,9 +284,10 @@ func TestPhase2AgentLifecycle(t *testing.T) {
t.Fatalf("unexpected heartbeat status %d: %s", restartHeartbeatRecorder.Code, restartHeartbeatRecorder.Body.String())
}
var restartHeartbeatBody struct {
Success bool `json:"success"`
Message string `json:"message"`
AgentSettings service.AgentSettings `json:"agent_settings"`
Success bool `json:"success"`
Message string `json:"message"`
AgentSettings service.AgentSettings `json:"agent_settings"`
ActiveConfig *service.ActiveConfigMeta `json:"active_config"`
}
if err = json.Unmarshal(restartHeartbeatRecorder.Body.Bytes(), &restartHeartbeatBody); err != nil {
t.Fatalf("failed to decode heartbeat response: %v", err)
@@ -297,6 +298,9 @@ func TestPhase2AgentLifecycle(t *testing.T) {
if !restartHeartbeatBody.AgentSettings.RestartOpenrestyNow {
t.Fatal("expected heartbeat response to instruct openresty restart")
}
if restartHeartbeatBody.ActiveConfig == nil || restartHeartbeatBody.ActiveConfig.Version == "" || restartHeartbeatBody.ActiveConfig.Checksum == "" {
t.Fatal("expected heartbeat response to include active config summary")
}
logsResp := performJSONRequest(t, engine, adminToken, http.MethodGet, "/api/apply-logs/?node_id="+createdNode.NodeID, nil)
var logs []model.ApplyLog
+27 -4
View File
@@ -57,7 +57,6 @@ type AgentConfigResponse struct {
type AgentSettings struct {
HeartbeatInterval int `json:"heartbeat_interval"`
SyncInterval int `json:"sync_interval"`
AutoUpdate bool `json:"auto_update"`
UpdateRepo string `json:"update_repo"`
UpdateNow bool `json:"update_now"`
@@ -66,9 +65,15 @@ type AgentSettings struct {
RestartOpenrestyNow bool `json:"restart_openresty_now"`
}
type ActiveConfigMeta struct {
Version string `json:"version"`
Checksum string `json:"checksum"`
}
type HeartbeatResponse struct {
Node *model.Node `json:"node"`
AgentSettings *AgentSettings `json:"agent_settings"`
Node *model.Node `json:"node"`
AgentSettings *AgentSettings `json:"agent_settings"`
ActiveConfig *ActiveConfigMeta `json:"active_config"`
}
type NodeView struct {
@@ -124,11 +129,14 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon
if err := model.DB.Model(node).Select("ip", "agent_version", "nginx_version", "openresty_status", "openresty_message", "status", "current_version", "last_seen_at", "last_error", "update_requested", "update_channel", "update_tag", "restart_openresty_requested").Updates(node).Error; err != nil {
return nil, err
}
activeConfig, err := GetActiveConfigMetaForAgent()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
return &HeartbeatResponse{
Node: node,
AgentSettings: &AgentSettings{
HeartbeatInterval: common.AgentHeartbeatInterval,
SyncInterval: common.AgentSyncInterval,
AutoUpdate: node.AutoUpdateEnabled,
UpdateRepo: common.AgentUpdateRepo,
UpdateNow: updateNow,
@@ -136,6 +144,21 @@ func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatRespon
UpdateTag: updateTag,
RestartOpenrestyNow: restartOpenrestyNow,
},
ActiveConfig: activeConfig,
}, nil
}
func GetActiveConfigMetaForAgent() (*ActiveConfigMeta, error) {
version, err := model.GetActiveConfigVersion()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
return nil, err
}
return &ActiveConfigMeta{
Version: version.Version,
Checksum: version.Checksum,
}, nil
}
+17
View File
@@ -78,6 +78,17 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
if err := node.Insert(); err != nil {
t.Fatalf("failed to seed node: %v", err)
}
if err := model.DB.Create(&model.ConfigVersion{
Version: "20260313-001",
SnapshotJSON: "{}",
MainConfig: "worker_processes auto;",
RenderedConfig: "server { listen 80; }",
Checksum: "checksum-active-1",
IsActive: true,
CreatedBy: "root",
}).Error; err != nil {
t.Fatalf("failed to seed active config version: %v", err)
}
resp, err := HeartbeatNode(node, AgentNodePayload{
NodeID: node.NodeID,
@@ -94,6 +105,12 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
if resp.AgentSettings == nil {
t.Fatal("expected agent settings in heartbeat response")
}
if resp.ActiveConfig == nil {
t.Fatal("expected active config summary in heartbeat response")
}
if resp.ActiveConfig.Version == "" || resp.ActiveConfig.Checksum == "" {
t.Fatal("expected active config summary to include version and checksum")
}
if !resp.AgentSettings.UpdateNow {
t.Fatal("expected update_now to be true")
}
@@ -68,8 +68,7 @@ const defaultSystemFields = {
};
const defaultOperationFields = {
AgentHeartbeatInterval: '30000',
AgentSyncInterval: '30000',
AgentHeartbeatInterval: '10000',
NodeOfflineThreshold: '120000',
AgentUpdateRepo: 'Rain-kl/ATSFlare',
OpenRestyWorkerProcesses: 'auto',
@@ -335,8 +334,7 @@ export function SettingsPage() {
});
setOperationFields({
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '30000',
AgentSyncInterval: optionMap.AgentSyncInterval ?? '30000',
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '10000',
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
@@ -942,10 +940,6 @@ export function SettingsPage() {
operationFields.AgentHeartbeatInterval,
10,
);
const sync = Number.parseInt(
operationFields.AgentSyncInterval,
10,
);
const offline = Number.parseInt(
operationFields.NodeOfflineThreshold,
10,
@@ -954,9 +948,6 @@ export function SettingsPage() {
if (Number.isNaN(heartbeat) || heartbeat < 5000) {
throw new Error('心跳间隔不能小于 5000 毫秒。');
}
if (Number.isNaN(sync) || sync < 5000) {
throw new Error('同步间隔不能小于 5000 毫秒。');
}
if (Number.isNaN(offline) || offline < 10000) {
throw new Error('离线阈值不能小于 10000 毫秒。');
}
@@ -964,7 +955,6 @@ export function SettingsPage() {
await saveOptionEntries(
[
['AgentHeartbeatInterval', String(heartbeat)],
['AgentSyncInterval', String(sync)],
['NodeOfflineThreshold', String(offline)],
],
'Agent 运行参数已保存。',
@@ -980,7 +970,7 @@ export function SettingsPage() {
</div>
}
>
<div className="grid gap-5 md:grid-cols-3">
<div className="grid gap-5 md:grid-cols-2">
<ResourceField
label={`心跳间隔 (${formatDurationLabel(operationFields.AgentHeartbeatInterval)})`}
>
@@ -995,20 +985,6 @@ export function SettingsPage() {
}
/>
</ResourceField>
<ResourceField
label={`同步间隔 (${formatDurationLabel(operationFields.AgentSyncInterval)})`}
>
<ResourceInput
type="number"
value={operationFields.AgentSyncInterval}
onChange={(event) =>
setOperationFields((previous) => ({
...previous,
AgentSyncInterval: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField
label={`离线阈值 (${formatDurationLabel(operationFields.NodeOfflineThreshold)})`}
>
@@ -422,7 +422,7 @@ export function WebsitesPage() {
<div className="space-y-6">
<PageHeader
title="网站"
description="在同一页完成网站录入、默认证书绑定、证书导入和资产巡检,减少域名页与证书页之间的来回切换。"
description="管理域名和TLS证书。"
/>
{feedback ? (
@@ -432,7 +432,6 @@ export function WebsitesPage() {
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
<AppCard
title={editingDomainId ? '编辑网站' : '快速添加网站'}
description="录入域名后即可直接绑定默认证书。若证书尚未导入,可在右侧先导入再回到这里选择。"
action={
<div className="flex flex-wrap gap-2">
{editingDomainId ? (
@@ -517,8 +516,7 @@ export function WebsitesPage() {
</AppCard>
<AppCard
title="证书快捷入口"
description="先导入证书,再回到左侧立即绑定到网站。无需跳转到单独页面。"
title="证书"
action={
<div className="flex flex-wrap gap-2">
<SecondaryButton