44
55import argparse
66import asyncio
7+ import hashlib
78import json
89import os
910import re
2223
2324
2425REQUIRED_METADATA_FIELDS = ("name" , "desc" , "version" , "author" )
26+ DEFAULT_CLONE_TIMEOUT = 120
2527
2628
2729def build_result (
@@ -205,7 +207,7 @@ def combine_requested_names(
205207 plugin_names : list [str ] | None ,
206208 plugin_name_list : str | None ,
207209) -> list [str ]:
208- names = list ( plugin_names or [])
210+ names = [ name . strip () for name in ( plugin_names or [])]
209211 if plugin_name_list :
210212 names .extend (part .strip () for part in plugin_name_list .split ("," ))
211213 return [name for name in names if name ]
@@ -216,12 +218,47 @@ def sanitize_name(name: str) -> str:
216218 return sanitized or "plugin"
217219
218220
219- def clone_plugin_repo (repo_url : str , destination : Path ) -> None :
221+ def build_plugin_clone_dir (work_dir : Path , plugin : str ) -> Path :
222+ digest = hashlib .sha256 (plugin .encode ("utf-8" )).hexdigest ()[:8 ]
223+ return work_dir / f"{ sanitize_name (plugin )} -{ digest } "
224+
225+
226+ def _normalize_process_output (output : str | bytes | None ) -> str | None :
227+ if output is None :
228+ return None
229+ if isinstance (output , bytes ):
230+ output = output .decode ("utf-8" , errors = "replace" )
231+ normalized = output .strip ()
232+ return normalized or None
233+
234+
235+ def build_process_output_details (
236+ * ,
237+ stdout : str | bytes | None ,
238+ stderr : str | bytes | None ,
239+ ) -> dict | None :
240+ details = {}
241+ stdout_text = _normalize_process_output (stdout )
242+ stderr_text = _normalize_process_output (stderr )
243+ if stdout_text :
244+ details ["stdout" ] = stdout_text
245+ if stderr_text :
246+ details ["stderr" ] = stderr_text
247+ return details or None
248+
249+
250+ def clone_plugin_repo (
251+ repo_url : str ,
252+ destination : Path ,
253+ * ,
254+ timeout : int = DEFAULT_CLONE_TIMEOUT ,
255+ ) -> None :
220256 subprocess .run (
221257 ["git" , "clone" , "--depth" , "1" , repo_url , str (destination )],
222258 check = True ,
223259 capture_output = True ,
224260 text = True ,
261+ timeout = timeout ,
225262 )
226263
227264
@@ -267,6 +304,7 @@ def validate_plugin(
267304 astrbot_path : Path ,
268305 script_path : Path ,
269306 work_dir : Path ,
307+ clone_timeout : int ,
270308 load_timeout : int ,
271309) -> dict :
272310 repo_url = plugin_data .get ("repo" )
@@ -292,9 +330,13 @@ def validate_plugin(
292330 message = str (exc ),
293331 )
294332
295- plugin_clone_dir = work_dir / sanitize_name ( plugin )
333+ plugin_clone_dir = build_plugin_clone_dir ( work_dir , plugin )
296334 try :
297- clone_plugin_repo (normalized_repo_url , plugin_clone_dir )
335+ clone_plugin_repo (
336+ normalized_repo_url ,
337+ plugin_clone_dir ,
338+ timeout = clone_timeout ,
339+ )
298340 except subprocess .CalledProcessError as exc :
299341 message = exc .stderr .strip () or exc .stdout .strip () or str (exc )
300342 return build_result (
@@ -305,6 +347,16 @@ def validate_plugin(
305347 stage = "clone" ,
306348 message = message ,
307349 )
350+ except subprocess .TimeoutExpired as exc :
351+ return build_result (
352+ plugin = plugin ,
353+ repo = repo_url ,
354+ normalized_repo_url = normalized_repo_url ,
355+ ok = False ,
356+ stage = "clone_timeout" ,
357+ message = f"git clone timed out after { clone_timeout } seconds" ,
358+ details = build_process_output_details (stdout = exc .stdout , stderr = exc .stderr ),
359+ )
308360
309361 precheck = precheck_plugin_directory (plugin_clone_dir )
310362 if not precheck ["ok" ]:
@@ -334,7 +386,7 @@ def validate_plugin(
334386 text = True ,
335387 timeout = load_timeout ,
336388 )
337- except subprocess .TimeoutExpired :
389+ except subprocess .TimeoutExpired as exc :
338390 return build_result (
339391 plugin = plugin ,
340392 repo = repo_url ,
@@ -343,6 +395,7 @@ def validate_plugin(
343395 stage = "timeout" ,
344396 message = f"worker timed out after { load_timeout } seconds" ,
345397 plugin_dir_name = plugin_dir_name ,
398+ details = build_process_output_details (stdout = exc .stdout , stderr = exc .stderr ),
346399 )
347400
348401 return parse_worker_output (
@@ -483,13 +536,10 @@ def run_worker(args: argparse.Namespace) -> int:
483536
484537 os .environ ["ASTRBOT_ROOT" ] = str (astrbot_root )
485538 os .environ .setdefault ("TESTING" , "true" )
486- for entry in reversed (
487- build_worker_sys_path (
488- astrbot_root = astrbot_root ,
489- astrbot_path = Path (args .astrbot_path ),
490- )
491- ):
492- sys .path .insert (0 , entry )
539+ sys .path [:0 ] = build_worker_sys_path (
540+ astrbot_root = astrbot_root ,
541+ astrbot_path = Path (args .astrbot_path ),
542+ )
493543
494544 result = asyncio .run (
495545 run_worker_load_check (args .plugin_dir_name , args .normalized_repo_url )
@@ -525,6 +575,7 @@ def build_parser() -> argparse.ArgumentParser:
525575 parser .add_argument ("--astrbot-path" )
526576 parser .add_argument ("--report-path" , default = "validation-report.json" )
527577 parser .add_argument ("--work-dir" )
578+ parser .add_argument ("--clone-timeout" , type = int , default = DEFAULT_CLONE_TIMEOUT )
528579 parser .add_argument ("--load-timeout" , type = int , default = 300 )
529580 parser .add_argument ("--worker" , action = "store_true" )
530581 parser .add_argument ("--plugin-source-dir" )
@@ -578,6 +629,7 @@ def main() -> int:
578629 astrbot_path = Path (args .astrbot_path ).resolve (),
579630 script_path = Path (__file__ ).resolve (),
580631 work_dir = work_dir ,
632+ clone_timeout = args .clone_timeout ,
581633 load_timeout = args .load_timeout ,
582634 )
583635 for plugin , plugin_data in selected
0 commit comments