crawlerkeeper-agent 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ """CrawlerKeeper Agent - 节点代理程序"""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,65 @@
1
+ """CrawlerKeeper Agent 命令行入口。
2
+
3
+ 提供 pip 安装后的 ``crawlerkeeper-agent`` 命令与 ``python -m crawlerkeeper`` 两种启动方式。
4
+ 本地源码运行走 ``src/main.py``(薄委托到本模块)。
5
+ """
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ from crawlerkeeper import __version__
12
+ from crawlerkeeper.agent import Agent
13
+
14
+
15
+ def main() -> None:
16
+ """主函数"""
17
+ # 版本查询:只打印版本号,不启动 Agent
18
+ if len(sys.argv) > 1 and sys.argv[1] in ("-V", "--version"):
19
+ print(f"crawlerkeeper-agent {__version__}")
20
+ return
21
+
22
+ print("正在启动 CrawlerKeeper Agent...", flush=True)
23
+
24
+ # 解析命令行参数
25
+ config_file: Optional[str] = None
26
+ extra_config: Dict[str, Any] = {"cleanup_temp_files": True} # 默认开启清理
27
+ i = 1
28
+ while i < len(sys.argv):
29
+ if sys.argv[i] in ['-c', '--config'] and i + 1 < len(sys.argv):
30
+ config_file = sys.argv[i + 1]
31
+ i += 2
32
+ elif sys.argv[i].startswith('--cleanup_temp_files='):
33
+ _, value = sys.argv[i].split('=', 1)
34
+ extra_config['cleanup_temp_files'] = value.lower() == 'on'
35
+ i += 1
36
+ elif sys.argv[i] == '--cleanup_temp_files':
37
+ # 单独出现默认为 on,或取下一个参数值
38
+ if i + 1 < len(sys.argv) and sys.argv[i + 1] in ('on', 'off'):
39
+ extra_config['cleanup_temp_files'] = sys.argv[i + 1].lower() == 'on'
40
+ i += 2
41
+ else:
42
+ extra_config['cleanup_temp_files'] = True
43
+ i += 1
44
+ else:
45
+ i += 1
46
+
47
+ # 如果没有指定配置文件,使用当前工作目录下的默认路径
48
+ if not config_file:
49
+ config_file = str(Path.cwd() / "config" / "config.yaml")
50
+
51
+ print(f"配置文件: {config_file}", flush=True)
52
+ print(f"额外配置: {extra_config}", flush=True)
53
+
54
+ # 创建并启动 Agent
55
+ agent = Agent(config_file if os.path.exists(config_file) else None, extra_config=extra_config)
56
+
57
+ try:
58
+ agent.start()
59
+ except Exception as e:
60
+ print(f"Agent 启动失败: {e}", flush=True)
61
+ sys.exit(1)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
crawlerkeeper/agent.py ADDED
@@ -0,0 +1,281 @@
1
+ """Agent 主程序模块"""
2
+ import os
3
+ import sys
4
+ import signal
5
+ import logging
6
+ import time
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any
10
+ from http.server import HTTPServer, BaseHTTPRequestHandler
11
+ from .config import Config
12
+ from .client import HttpClient
13
+ from .heartbeat import HeartbeatManager
14
+ from .task import TaskManager
15
+ from .executor import SpiderExecutor
16
+ from .deployment import DeploymentManager
17
+ from .logger import setup_logger
18
+ from .metrics import get_metrics_server
19
+
20
+ logger = logging.getLogger("crawlerkeeper-agent")
21
+
22
+
23
+ class Agent:
24
+ """CrawlerKeeper Agent 主类"""
25
+
26
+ def __init__(self, config_file: str = None, extra_config: Optional[Dict[str, Any]] = None):
27
+ self.config = Config(config_file)
28
+ self.extra_config = extra_config or {}
29
+ self.client: Optional[HttpClient] = None
30
+ self.heartbeat_manager: Optional[HeartbeatManager] = None
31
+ self.task_manager: Optional[TaskManager] = None
32
+ self.executor: Optional[SpiderExecutor] = None
33
+ self.deployment_manager: Optional[DeploymentManager] = None
34
+ self.running = False
35
+ self.metrics_server = None
36
+ self._http_server = None
37
+ self._http_thread = None
38
+
39
+ def _start_http_server(self):
40
+ """启动 HTTP 服务器用于 /metrics 端点"""
41
+ metrics_server = get_metrics_server()
42
+ metrics_server.set_thread_pool_size(self.task_manager.max_workers if self.task_manager else 4)
43
+
44
+ MetricsHandler = type('MetricsHandler', (BaseHTTPRequestHandler,), {
45
+ 'do_GET': lambda self: (
46
+ self.send_response(200),
47
+ self.send_header('Content-Type', metrics_server.get_content_type()),
48
+ self.end_headers(),
49
+ self.wfile.write(metrics_server.get_metrics())
50
+ )
51
+ })
52
+
53
+ server_address = ('', self.config.agent_port)
54
+ self._http_server = HTTPServer(server_address, MetricsHandler)
55
+ self._http_thread = threading.Thread(target=self._http_server.serve_forever, daemon=True)
56
+ self._http_thread.start()
57
+ logger.info(f"指标 HTTP 服务器已启动,端口: {self.config.agent_port}")
58
+
59
+ def start(self):
60
+ """启动 Agent"""
61
+ # 设置日志
62
+ setup_logger(
63
+ level=self.config.log_level,
64
+ log_file=self.config.log_file
65
+ )
66
+
67
+ logger.info("=" * 50)
68
+ logger.info("CrawlerKeeper Agent 启动")
69
+ logger.info("=" * 50)
70
+ logger.info(f"配置: {self.config}")
71
+
72
+ # 创建 HTTP 客户端
73
+ self.client = HttpClient(
74
+ base_url=self.config.server_url,
75
+ api_key=self.config.api_key,
76
+ timeout=self.config.heartbeat_timeout
77
+ )
78
+
79
+ # 注册节点
80
+ if not self._register_node():
81
+ logger.error("节点注册失败,Agent 退出")
82
+ return False
83
+
84
+ # 初始化组件
85
+ self._init_components()
86
+
87
+ # 初始化指标服务器
88
+ self.metrics_server = get_metrics_server()
89
+
90
+ # 启动 HTTP 服务器(用于 /metrics)
91
+ self._start_http_server()
92
+
93
+ # 注册信号处理
94
+ self._register_signal_handlers()
95
+
96
+ # 启动组件
97
+ self._start_components()
98
+
99
+ # 主循环
100
+ self._main_loop()
101
+
102
+ return True
103
+
104
+ def _register_node(self) -> bool:
105
+ """注册节点"""
106
+ # 检查是否已有节点 ID
107
+ if self.config.node_id and self.config.api_key:
108
+ logger.info(f"使用已有节点配置: node_id={self.config.node_id}")
109
+ return True
110
+
111
+ # 注册新节点
112
+ logger.info("开始注册节点...")
113
+ logger.info(f"注册配置: name={self.config.agent_name}, ip={self.config.agent_ip}, port={self.config.agent_port}")
114
+
115
+ url = "/api/nodes/register"
116
+ data = {
117
+ "name": self.config.agent_name,
118
+ "ip": self.config.agent_ip,
119
+ "port": self.config.agent_port,
120
+ "description": f"CrawlerKeeper Agent - {self.config.agent_name}"
121
+ }
122
+ logger.debug(f"注册请求数据: {data}")
123
+
124
+ response = self.client.post(url, data)
125
+
126
+ if not response:
127
+ logger.error("节点注册请求失败")
128
+ return False
129
+
130
+ if response.get("code") != 200:
131
+ logger.error(f"节点注册失败: {response.get('message')}")
132
+ return False
133
+
134
+ # 保存节点信息
135
+ data = response.get("data", {})
136
+ self.config.node_id = data.get("id")
137
+
138
+ if not self.config.node_id:
139
+ logger.error("节点注册响应数据不完整")
140
+ return False
141
+
142
+ logger.info(f"节点注册成功: id={self.config.node_id}")
143
+
144
+ # 保存配置
145
+ config_file = os.getenv("AGENT_CONFIG_FILE", "./config/config.yaml")
146
+ self.config.save_to_file(config_file)
147
+
148
+ return True
149
+
150
+ def _init_components(self):
151
+ """初始化组件"""
152
+ # 心跳管理器
153
+ self.heartbeat_manager = HeartbeatManager(
154
+ client=self.client,
155
+ node_id=self.config.node_id,
156
+ interval=self.config.heartbeat_interval,
157
+ timeout=self.config.heartbeat_timeout
158
+ )
159
+
160
+ # 任务执行器
161
+ self.executor = SpiderExecutor(
162
+ client=self.client,
163
+ work_dir=self.config.work_dir,
164
+ task_execute_timeout=self.config.task_execute_timeout
165
+ )
166
+
167
+ # 任务管理器
168
+ self.task_manager = TaskManager(
169
+ client=self.client,
170
+ node_id=self.config.node_id,
171
+ poll_interval=self.config.task_poll_interval,
172
+ work_dir=self.config.work_dir,
173
+ extra_config=self.extra_config
174
+ )
175
+ self.task_manager.set_executor(self.executor)
176
+ self.executor.set_task_manager(self.task_manager)
177
+
178
+ # 部署管理器
179
+ self.deployment_manager = DeploymentManager(
180
+ client=self.client,
181
+ node_id=self.config.node_id,
182
+ poll_interval=self.config.deploy_poll_interval,
183
+ work_dir=self.config.work_dir
184
+ )
185
+
186
+ logger.info("组件初始化完成")
187
+
188
+ def _start_components(self):
189
+ """启动组件"""
190
+ # 启动心跳
191
+ self.heartbeat_manager.start()
192
+
193
+ # 启动任务拉取
194
+ self.task_manager.start()
195
+
196
+ # 启动部署拉取
197
+ self.deployment_manager.start()
198
+
199
+ self.running = True
200
+ logger.info("所有组件已启动")
201
+
202
+ def _stop_components(self):
203
+ """停止组件"""
204
+ logger.info("正在停止组件...")
205
+
206
+ if self.task_manager:
207
+ self.task_manager.stop()
208
+
209
+ if self.heartbeat_manager:
210
+ self.heartbeat_manager.stop()
211
+
212
+ if self.deployment_manager:
213
+ self.deployment_manager.stop()
214
+
215
+ logger.info("所有组件已停止")
216
+
217
+ def _register_signal_handlers(self):
218
+ """注册信号处理器"""
219
+ def sigterm_handler(signum, frame):
220
+ """SIGTERM 信号处理器 - 优雅关闭"""
221
+ logger.info(f"收到 SIGTERM 信号: {signum},将进行优雅关闭")
222
+ self.stop(graceful=True)
223
+
224
+ def sigint_handler(signum, frame):
225
+ """SIGINT 信号处理器 - 强制关闭"""
226
+ logger.info(f"收到 SIGINT 信号: {signum},将强制关闭")
227
+ self.stop(graceful=False)
228
+
229
+ signal.signal(signal.SIGINT, sigint_handler)
230
+ signal.signal(signal.SIGTERM, sigterm_handler)
231
+
232
+ def _main_loop(self):
233
+ """主循环"""
234
+ logger.info("Agent 运行中,按 Ctrl+C 停止")
235
+
236
+ try:
237
+ while self.running:
238
+ # 检查心跳状态
239
+ if self.heartbeat_manager and not self.heartbeat_manager.is_alive():
240
+ logger.warning("心跳检测失败,尝试重新注册...")
241
+ if not self._register_node():
242
+ logger.error("重新注册失败")
243
+
244
+ time.sleep(10)
245
+
246
+ except KeyboardInterrupt:
247
+ logger.info("收到键盘中断")
248
+
249
+ def stop(self, graceful: bool = True):
250
+ """停止 Agent
251
+
252
+ Args:
253
+ graceful: True 则优雅关闭(等待任务完成),False 则强制关闭
254
+ """
255
+ logger.info(f"正在停止 Agent (graceful={graceful})...")
256
+ self.running = False
257
+
258
+ # 停止 HTTP 服务器
259
+ if self._http_server:
260
+ self._http_server.shutdown()
261
+ logger.info("指标 HTTP 服务器已关闭")
262
+
263
+ # 停止任务管理器(传入优雅关闭参数)
264
+ if self.task_manager:
265
+ self.task_manager.stop(graceful=graceful)
266
+
267
+ # 停止其他组件
268
+ if self.heartbeat_manager:
269
+ self.heartbeat_manager.stop()
270
+
271
+ if self.deployment_manager:
272
+ self.deployment_manager.stop()
273
+
274
+ logger.info("Agent 已停止")
275
+
276
+ def restart(self):
277
+ """重启 Agent"""
278
+ logger.info("正在重启 Agent...")
279
+ self.stop()
280
+ time.sleep(2)
281
+ self.start()
@@ -0,0 +1,112 @@
1
+ """HTTP 客户端模块"""
2
+ import logging
3
+ import requests
4
+ import time
5
+ from typing import Optional, Dict, Any
6
+
7
+ # 使用根 logger,确保能继承全局配置
8
+ logger = logging.getLogger("crawlerkeeper-agent")
9
+ logger.setLevel(logging.DEBUG)
10
+ logger.propagate = True
11
+
12
+
13
+ class HttpClient:
14
+ """HTTP 客户端封装"""
15
+
16
+ def __init__(self, base_url: str, api_key: str = None, timeout: int = 30):
17
+ self.base_url = base_url.rstrip('/')
18
+ self.api_key = api_key
19
+ self.timeout = timeout
20
+ self.session = requests.Session()
21
+
22
+ def _get_headers(self) -> Dict[str, str]:
23
+ """获取请求头"""
24
+ headers = {
25
+ "Content-Type": "application/json",
26
+ "Accept": "application/json"
27
+ }
28
+ if self.api_key:
29
+ headers["Authorization"] = f"ApiKey {self.api_key}"
30
+ return headers
31
+
32
+ def _build_url(self, path: str) -> str:
33
+ """构建完整 URL"""
34
+ if path.startswith('http'):
35
+ return path
36
+ return f"{self.base_url}{path}"
37
+
38
+ def get(self, path: str, params: Optional[Dict] = None, retry: int = 3) -> Optional[Dict]:
39
+ """发送 GET 请求"""
40
+ url = self._build_url(path)
41
+ headers = self._get_headers()
42
+
43
+ for attempt in range(retry):
44
+ try:
45
+ response = self.session.get(
46
+ url,
47
+ params=params,
48
+ headers=headers,
49
+ timeout=self.timeout
50
+ )
51
+ response.raise_for_status()
52
+ result = response.json()
53
+ return result
54
+ except requests.exceptions.RequestException as e:
55
+ logger.warning(f"GET 请求失败 (尝试 {attempt + 1}/{retry}): {url}, 错误: {e}")
56
+ if attempt < retry - 1:
57
+ time.sleep(2 ** attempt) # 指数退避
58
+ else:
59
+ logger.error(f"GET 请求最终失败: {url}")
60
+ return None
61
+ return None
62
+
63
+ def post(self, path: str, data: Optional[Dict] = None, retry: int = 3) -> Optional[Dict]:
64
+ """发送 POST 请求"""
65
+ url = self._build_url(path)
66
+ headers = self._get_headers()
67
+
68
+ for attempt in range(retry):
69
+ try:
70
+ response = self.session.post(
71
+ url,
72
+ json=data,
73
+ headers=headers,
74
+ timeout=self.timeout
75
+ )
76
+ response.raise_for_status()
77
+ result = response.json()
78
+ return result
79
+ except requests.exceptions.RequestException as e:
80
+ logger.warning(f"POST 请求失败 (尝试 {attempt + 1}/{retry}): {url}, 错误: {e}")
81
+ if attempt < retry - 1:
82
+ time.sleep(2 ** attempt)
83
+ else:
84
+ logger.error(f"POST 请求最终失败: {url}")
85
+ return None
86
+ return None
87
+
88
+ def get_raw(self, path: str, params: Optional[Dict] = None, retry: int = 3):
89
+ """发送 GET 请求并返回原始响应(用于下载文件)"""
90
+ url = self._build_url(path)
91
+ headers = self._get_headers()
92
+ # 下载文件时不需要 Accept: application/json
93
+ headers.pop("Accept", None)
94
+
95
+ for attempt in range(retry):
96
+ try:
97
+ response = self.session.get(
98
+ url,
99
+ params=params,
100
+ headers=headers,
101
+ timeout=self.timeout
102
+ )
103
+ response.raise_for_status()
104
+ return response
105
+ except requests.exceptions.RequestException as e:
106
+ logger.warning(f"GET raw 请求失败 (尝试 {attempt + 1}/{retry}): {url}, 错误: {e}")
107
+ if attempt < retry - 1:
108
+ time.sleep(2 ** attempt)
109
+ else:
110
+ logger.error(f"GET raw 请求最终失败: {url}")
111
+ return None
112
+ return None
@@ -0,0 +1,45 @@
1
+ """爬虫代码包(ZIP)下载校验工具。
2
+
3
+ 后端业务异常(如 Git 克隆失败、归属校验失败)会以 HTTP 200 + JSON 响应体返回,
4
+ Agent 若直接把响应体当 ZIP 解压,真实失败原因会被掩盖成「解压失败」。
5
+ 下载后先按 ZIP 魔数校验响应体,非 ZIP 时输出可定位的错误提示并按下载失败处理。
6
+ """
7
+ import logging
8
+ from typing import Optional
9
+
10
+ logger = logging.getLogger("crawlerkeeper-agent")
11
+
12
+ # ZIP 本地文件头魔数(PK\x03\x04)
13
+ ZIP_MAGIC = b"PK\x03\x04"
14
+
15
+ # 错误响应体日志摘录长度上限
16
+ SNIPPET_LIMIT = 200
17
+
18
+
19
+ def is_zip_content(content: Optional[bytes]) -> bool:
20
+ """判断响应体是否为 ZIP 包
21
+
22
+ Args:
23
+ content: 响应体字节
24
+
25
+ Returns:
26
+ True 表示响应体以 ZIP 魔数开头
27
+ """
28
+ if not content:
29
+ return False
30
+ return bool(content.startswith(ZIP_MAGIC))
31
+
32
+
33
+ def body_snippet(content: Optional[bytes]) -> str:
34
+ """提取响应体文本摘录(用于非 ZIP 响应时的错误定位)
35
+
36
+ Args:
37
+ content: 响应体字节
38
+
39
+ Returns:
40
+ 可读文本摘录(最多 200 字符),空响应体返回占位说明
41
+ """
42
+ if not content:
43
+ return "<空响应体>"
44
+ text = content.decode("utf-8", errors="replace").strip()
45
+ return text[:SNIPPET_LIMIT]
@@ -0,0 +1,175 @@
1
+ """配置管理模块"""
2
+ import os
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ import yaml
7
+ import socket
8
+ import logging
9
+
10
+ logger = logging.getLogger("crawlerkeeper-agent")
11
+
12
+
13
+ class Config:
14
+ """Agent 配置类"""
15
+
16
+ def __init__(self, config_file: str = None):
17
+ # 默认配置
18
+ self.server_url: str = os.getenv("AGENT_SERVER_URL", "http://localhost:8080")
19
+ self.api_key: str = os.getenv("AGENT_API_KEY", "")
20
+
21
+ self.agent_name: str = os.getenv("AGENT_NAME", socket.gethostname())
22
+ self.agent_ip: str = os.getenv("AGENT_IP", self._get_local_ip())
23
+ self.agent_port: int = int(os.getenv("AGENT_PORT", "9000"))
24
+
25
+ self.heartbeat_interval: int = int(os.getenv("AGENT_HEARTBEAT_INTERVAL", "30"))
26
+ self.heartbeat_timeout: int = int(os.getenv("AGENT_HEARTBEAT_TIMEOUT", "90"))
27
+
28
+ self.task_poll_interval: int = int(os.getenv("AGENT_TASK_POLL_INTERVAL", "10"))
29
+ self.deploy_poll_interval: int = int(os.getenv("AGENT_DEPLOY_POLL_INTERVAL", "30"))
30
+ # 任务执行超时(秒):0 表示不限时;负值视为配置错误,按 0 处理(避免立即超时误杀任务)
31
+ self.task_execute_timeout: int = int(os.getenv("AGENT_TASK_EXECUTE_TIMEOUT", "3600"))
32
+ if self.task_execute_timeout < 0:
33
+ logger.warning(f"AGENT_TASK_EXECUTE_TIMEOUT 配置为负数({self.task_execute_timeout}),按 0(不限时)处理")
34
+ self.task_execute_timeout = 0
35
+ self.work_dir: str = os.getenv("AGENT_WORK_DIR", "./data/spiders")
36
+ # 注意:最大并行任务数不通过配置文件设置,由 TaskManager 根据
37
+ # AGENT_MAX_WORKERS 环境变量或容器 CPU 限制自动计算
38
+
39
+ self.log_level: str = os.getenv("AGENT_LOG_LEVEL", "INFO")
40
+ self.log_file: str = os.getenv("AGENT_LOG_FILE", "./logs/agent.log")
41
+
42
+ self.node_id: Optional[int] = None
43
+ self.server_url_from_register: Optional[str] = None
44
+
45
+ # 加载配置文件
46
+ if config_file:
47
+ self._load_from_file(config_file)
48
+
49
+ def _get_local_ip(self) -> str:
50
+ """获取本机 IP 地址"""
51
+ try:
52
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
53
+ s.settimeout(3) # 3秒超时
54
+ s.connect(("8.8.8.8", 80))
55
+ ip = s.getsockname()[0]
56
+ s.close()
57
+ return ip
58
+ except Exception:
59
+ return "127.0.0.1"
60
+
61
+ def _load_from_file(self, config_file: str):
62
+ """从 YAML 配置文件加载配置"""
63
+ config_path = Path(config_file)
64
+ if not config_path.exists():
65
+ logger.warning(f"配置文件不存在: {config_file}")
66
+ return
67
+
68
+ try:
69
+ with open(config_path, 'r', encoding='utf-8') as f:
70
+ config = yaml.safe_load(f)
71
+
72
+ if not config:
73
+ return
74
+
75
+ # 主控系统配置
76
+ if 'server' in config:
77
+ server = config['server']
78
+ if 'url' in server and not os.getenv("AGENT_SERVER_URL"):
79
+ self.server_url = server['url']
80
+
81
+ # Agent 配置
82
+ if 'agent' in config:
83
+ agent = config['agent']
84
+ # 只有当环境变量未设置且配置文件值不为空时才使用配置文件的值
85
+ if 'name' in agent and agent['name'] and not os.getenv("AGENT_NAME"):
86
+ self.agent_name = agent['name']
87
+ if 'ip' in agent and agent.get('ip') and not os.getenv("AGENT_IP"):
88
+ self.agent_ip = agent.get('ip')
89
+ if 'port' in agent and agent.get('port') and not os.getenv("AGENT_PORT"):
90
+ try:
91
+ self.agent_port = int(agent['port'])
92
+ except (ValueError, TypeError):
93
+ pass # 保持默认值
94
+ if 'api_key' in agent and agent['api_key'] and not os.getenv("AGENT_API_KEY"):
95
+ self.api_key = agent['api_key']
96
+
97
+ # 心跳配置
98
+ if 'heartbeat' in config:
99
+ hb = config['heartbeat']
100
+ if 'interval' in hb and not os.getenv("AGENT_HEARTBEAT_INTERVAL"):
101
+ self.heartbeat_interval = hb['interval']
102
+ if 'timeout' in hb and not os.getenv("AGENT_HEARTBEAT_TIMEOUT"):
103
+ self.heartbeat_timeout = hb['timeout']
104
+
105
+ # 任务配置
106
+ if 'task' in config:
107
+ task = config['task']
108
+ if 'poll_interval' in task and not os.getenv("AGENT_TASK_POLL_INTERVAL"):
109
+ self.task_poll_interval = task['poll_interval']
110
+ if 'deploy_poll_interval' in task and not os.getenv("AGENT_DEPLOY_POLL_INTERVAL"):
111
+ self.deploy_poll_interval = task['deploy_poll_interval']
112
+ if 'work_dir' in task and not os.getenv("AGENT_WORK_DIR"):
113
+ self.work_dir = task['work_dir']
114
+
115
+ # 日志配置
116
+ if 'logging' in config:
117
+ log = config['logging']
118
+ if 'level' in log and not os.getenv("AGENT_LOG_LEVEL"):
119
+ self.log_level = log['level']
120
+ if 'file' in log and not os.getenv("AGENT_LOG_FILE"):
121
+ self.log_file = log['file']
122
+
123
+ if self.api_key and not os.getenv("AGENT_API_KEY"):
124
+ os.environ["AGENT_API_KEY"] = self.api_key
125
+
126
+ logger.info(f"已从配置文件加载配置: {config_file}")
127
+ except Exception as e:
128
+ logger.error(f"加载配置文件失败: {e}")
129
+
130
+ def save_to_file(self, config_file: str):
131
+ """保存配置到文件"""
132
+ config_path = Path(config_file)
133
+ config_path.parent.mkdir(parents=True, exist_ok=True)
134
+
135
+ config = {
136
+ 'server': {
137
+ 'url': self.server_url
138
+ },
139
+ 'agent': {
140
+ 'name': self.agent_name,
141
+ 'ip': self.agent_ip,
142
+ 'port': self.agent_port,
143
+ 'api_key': self.api_key
144
+ },
145
+ 'heartbeat': {
146
+ 'interval': self.heartbeat_interval,
147
+ 'timeout': self.heartbeat_timeout
148
+ },
149
+ 'task': {
150
+ 'poll_interval': self.task_poll_interval,
151
+ 'deploy_poll_interval': self.deploy_poll_interval,
152
+ 'work_dir': self.work_dir
153
+ },
154
+ 'logging': {
155
+ 'level': self.log_level,
156
+ 'file': self.log_file
157
+ }
158
+ }
159
+
160
+ try:
161
+ with open(config_path, 'w', encoding='utf-8') as f:
162
+ yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
163
+ logger.info(f"配置已保存到: {config_file}")
164
+ except Exception as e:
165
+ logger.error(f"保存配置文件失败: {e}")
166
+
167
+ def get_auth_headers(self) -> dict:
168
+ """获取认证 Header"""
169
+ if self.api_key:
170
+ return {"Authorization": f"ApiKey {self.api_key}"}
171
+ return {}
172
+
173
+ def __repr__(self):
174
+ return (f"Config(server_url={self.server_url}, agent_name={self.agent_name}, "
175
+ f"agent_ip={self.agent_ip}, node_id={self.node_id})")