Compare commits

...

4 Commits

Author SHA1 Message Date
yyhuni
f63e40fbba 优化:agent自动更新逻辑 2025-12-19 20:07:55 +08:00
yyhuni
54573e210a fix: agent更新逻辑 2025-12-19 20:00:36 +08:00
github-actions[bot]
6179dd2ed3 chore: bump version to v1.0.22 2025-12-19 11:56:56 +00:00
github-actions[bot]
34ac706fbc chore: bump version to v1.0.21 2025-12-19 11:49:51 +00:00
6 changed files with 71 additions and 4 deletions

View File

@@ -1 +1 @@
v1.0.20
v1.0.22

View File

@@ -10,6 +10,8 @@ class WorkerNode(models.Model):
('deploying', '部署中'),
('online', '在线'),
('offline', '离线'),
('updating', '更新中'),
('outdated', '版本过低'),
]
name = models.CharField(max_length=100, help_text='节点名称')

View File

@@ -134,6 +134,17 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
"need_update": true/false,
"server_version": "v1.0.19"
}
状态流转:
┌─────────────────────────────────────────────────────────────────────┐
│ 场景 │ 状态变化 │
├─────────────────────────────┼───────────────────────────────────────┤
│ 首次心跳 │ pending/deploying → online │
│ 远程 Worker 版本不匹配 │ online → updating → (更新成功) online │
│ 远程 Worker 更新失败 │ updating → outdated │
│ 本地 Worker 版本不匹配 │ online → outdated (需手动 update.sh) │
│ 版本匹配 │ updating/outdated → online │
└─────────────────────────────┴───────────────────────────────────────┘
"""
from apps.engine.services.worker_load_service import worker_load_service
from django.conf import settings
@@ -165,9 +176,19 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
)
# 远程 Worker服务端主动通过 SSH 触发更新
# 旧版 agent 不会解析 need_update所以需要服务端主动推送
if not worker.is_local and worker.ip_address:
self._trigger_remote_agent_update(worker, server_version)
else:
# 本地 Worker 版本不匹配:标记为 outdated
# 需要用户手动执行 update.sh 更新
if worker.status != 'outdated':
worker.status = 'outdated'
worker.save(update_fields=['status'])
else:
# 版本匹配,确保状态为 online
if worker.status in ('updating', 'outdated'):
worker.status = 'online'
worker.save(update_fields=['status'])
return Response({
'status': 'ok',
@@ -184,7 +205,8 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
import redis
from django.conf import settings as django_settings
redis_client = redis.from_url(django_settings.REDIS_URL)
redis_url = f"redis://{django_settings.REDIS_HOST}:{django_settings.REDIS_PORT}/{django_settings.REDIS_DB}"
redis_client = redis.from_url(redis_url)
lock_key = f"agent_update_lock:{worker.id}"
# 尝试获取锁60秒过期防止重复触发
@@ -192,6 +214,9 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
logger.debug(f"Worker {worker.name} 更新已在进行中,跳过")
return
# 获取锁成功,设置状态为 updating
self._set_worker_status(worker.id, 'updating')
# 提取数据避免后台线程访问 ORM
worker_id = worker.id
worker_name = worker.name
@@ -230,11 +255,15 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
if success:
logger.info(f"Worker {worker_name} 远程更新成功")
# 更新成功后,新 agent 心跳会自动把状态改回 online
else:
logger.warning(f"Worker {worker_name} 远程更新失败: {message}")
# 更新失败,标记为 outdated
self._set_worker_status(worker_id, 'outdated')
except Exception as e:
logger.error(f"Worker {worker_name} 远程更新异常: {e}")
self._set_worker_status(worker_id, 'outdated')
finally:
# 释放锁
redis_client.delete(lock_key)
@@ -242,6 +271,14 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
# 后台执行,不阻塞心跳响应
threading.Thread(target=_async_update, daemon=True).start()
def _set_worker_status(self, worker_id: int, status: str):
"""更新 Worker 状态(用于后台线程)"""
try:
from apps.engine.models import WorkerNode
WorkerNode.objects.filter(id=worker_id).update(status=status)
except Exception as e:
logger.error(f"更新 Worker {worker_id} 状态失败: {e}")
@action(detail=False, methods=['post'])
def register(self, request):
"""

View File

@@ -297,6 +297,8 @@ export function DeployTerminalDialog({
{isConnected && currentStatus === 'deploying' && '正在部署中,点击查看进度'}
{isConnected && currentStatus === 'online' && '节点运行正常'}
{isConnected && currentStatus === 'offline' && '节点离线,可尝试重新部署'}
{isConnected && currentStatus === 'updating' && '正在自动更新 Agent...'}
{isConnected && currentStatus === 'outdated' && '版本过低,需要更新'}
</div>
{/* 右侧:操作按钮 */}
@@ -334,6 +336,28 @@ export function DeployTerminalDialog({
</button>
)}
{/* 更新中 -> 显示"查看进度" */}
{currentStatus === 'updating' && (
<button
onClick={handleAttach}
className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#e0af68] text-[#1a1b26] hover:bg-[#e0af68]/80 transition-colors"
>
<IconEye className="mr-1.5 h-4 w-4" />
</button>
)}
{/* 版本过低 -> 显示"重新部署" */}
{currentStatus === 'outdated' && (
<button
onClick={handleDeploy}
className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#f7768e] text-[#1a1b26] hover:bg-[#f7768e]/80 transition-colors"
>
<IconRocket className="mr-1.5 h-4 w-4" />
</button>
)}
{/* 已部署(online/offline) -> 显示"重新部署"和"卸载" */}
{(currentStatus === 'online' || currentStatus === 'offline') && (
<>

View File

@@ -51,6 +51,8 @@ const STATUS_MAP: Record<WorkerStatus, 'online' | 'offline' | 'maintenance' | 'd
offline: 'offline',
pending: 'maintenance',
deploying: 'degraded',
updating: 'degraded',
outdated: 'offline',
}
// 状态中文标签
@@ -59,6 +61,8 @@ const STATUS_LABEL: Record<WorkerStatus, string> = {
offline: '离线',
pending: '等待部署',
deploying: '部署中',
updating: '更新中',
outdated: '版本过低',
}
// 统计卡片组件

View File

@@ -3,7 +3,7 @@
*/
// Worker 状态枚举(前后端统一)
export type WorkerStatus = 'pending' | 'deploying' | 'online' | 'offline'
export type WorkerStatus = 'pending' | 'deploying' | 'online' | 'offline' | 'updating' | 'outdated'
// Worker 节点
export interface WorkerNode {