ErisPulse 2.1.14.dev1__py3-none-any.whl → 2.1.15__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.
ErisPulse/__main__.py CHANGED
@@ -95,7 +95,7 @@ class PackageManager:
95
95
  """
96
96
  REMOTE_SOURCES = [
97
97
  "https://erisdev.com/packages.json",
98
- "https://raw.githubusercontent.com/ErisPulse/ErisPulse-ModuleRepo/main/packages.json"
98
+ "https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/packages.json"
99
99
  ]
100
100
 
101
101
  CACHE_EXPIRY = 3600 # 1小时缓存
@@ -256,13 +256,76 @@ class PackageManager:
256
256
  except Exception:
257
257
  return False
258
258
 
259
- def _run_pip_command(self, args: List[str], description: str) -> bool:
259
+ def _normalize_name(self, name: str) -> str:
260
260
  """
261
- 执行pip命令
261
+ 标准化包名,统一转为小写以实现大小写不敏感比较
262
+
263
+ :param name: 原始名称
264
+ :return: 标准化后的名称
265
+ """
266
+ return name.lower().strip()
267
+
268
+ async def _find_package_by_alias(self, alias: str) -> Optional[str]:
269
+ """
270
+ 通过别名查找实际包名(大小写不敏感)
271
+
272
+ :param alias: 包别名
273
+ :return: 实际包名,未找到返回None
274
+ """
275
+ normalized_alias = self._normalize_name(alias)
276
+ remote_packages = await self.get_remote_packages()
277
+
278
+ # 检查模块
279
+ for name, info in remote_packages["modules"].items():
280
+ if self._normalize_name(name) == normalized_alias:
281
+ return info["package"]
282
+
283
+ # 检查适配器
284
+ for name, info in remote_packages["adapters"].items():
285
+ if self._normalize_name(name) == normalized_alias:
286
+ return info["package"]
287
+
288
+ # 检查CLI扩展
289
+ for name, info in remote_packages.get("cli_extensions", {}).items():
290
+ if self._normalize_name(name) == normalized_alias:
291
+ return info["package"]
292
+
293
+ return None
294
+
295
+ def _find_installed_package_by_name(self, name: str) -> Optional[str]:
296
+ """
297
+ 在已安装包中查找实际包名(大小写不敏感)
298
+
299
+ :param name: 包名或别名
300
+ :return: 实际包名,未找到返回None
301
+ """
302
+ normalized_name = self._normalize_name(name)
303
+ installed = self.get_installed_packages()
304
+
305
+ # 在已安装的模块中查找
306
+ for module_info in installed["modules"].values():
307
+ if self._normalize_name(module_info["package"]) == normalized_name:
308
+ return module_info["package"]
309
+
310
+ # 在已安装的适配器中查找
311
+ for adapter_info in installed["adapters"].values():
312
+ if self._normalize_name(adapter_info["package"]) == normalized_name:
313
+ return adapter_info["package"]
314
+
315
+ # 在已安装的CLI扩展中查找
316
+ for cli_info in installed["cli_extensions"].values():
317
+ if self._normalize_name(cli_info["package"]) == normalized_name:
318
+ return cli_info["package"]
319
+
320
+ return None
321
+
322
+ def _run_pip_command_with_output(self, args: List[str], description: str) -> Tuple[bool, str, str]:
323
+ """
324
+ 执行pip命令并捕获输出
262
325
 
263
326
  :param args: pip命令参数列表
264
327
  :param description: 进度条描述
265
- :return: 命令是否成功执行
328
+ :return: (是否成功, 标准输出, 标准错误)
266
329
  """
267
330
  with Progress(
268
331
  TextColumn(f"[progress.description]{description}"),
@@ -276,61 +339,266 @@ class PackageManager:
276
339
  [sys.executable, "-m", "pip"] + args,
277
340
  stdout=subprocess.PIPE,
278
341
  stderr=subprocess.PIPE,
279
- universal_newlines=True
342
+ universal_newlines=True,
343
+ bufsize=1 # 行缓冲
280
344
  )
281
345
 
282
- while True:
283
- output = process.stdout.readline()
284
- if output == '' and process.poll() is not None:
285
- break
286
- if output:
287
- progress.update(task, advance=1)
288
-
289
- return process.returncode == 0
346
+ stdout_lines = []
347
+ stderr_lines = []
348
+
349
+ # 使用超时机制避免永久阻塞
350
+ import threading
351
+ import queue
352
+
353
+ def read_output(pipe, lines_list):
354
+ try:
355
+ for line in iter(pipe.readline, ''):
356
+ lines_list.append(line)
357
+ progress.update(task, advance=5) # 每行增加进度
358
+ pipe.close()
359
+ except Exception:
360
+ pass
361
+
362
+ stdout_thread = threading.Thread(target=read_output, args=(process.stdout, stdout_lines))
363
+ stderr_thread = threading.Thread(target=read_output, args=(process.stderr, stderr_lines))
364
+
365
+ stdout_thread.start()
366
+ stderr_thread.start()
367
+
368
+ # 等待进程结束,最多等待5分钟
369
+ try:
370
+ process.wait(timeout=300)
371
+ except subprocess.TimeoutExpired:
372
+ process.kill()
373
+ process.wait()
374
+ console.print("[warning]命令执行超时,已强制终止[/]")
375
+ return False, "", "命令执行超时"
376
+
377
+ stdout_thread.join(timeout=10)
378
+ stderr_thread.join(timeout=10)
379
+
380
+ stdout = ''.join(stdout_lines)
381
+ stderr = ''.join(stderr_lines)
382
+
383
+ return process.returncode == 0, stdout, stderr
290
384
  except subprocess.CalledProcessError as e:
291
385
  console.print(f"[error]命令执行失败: {e}[/]")
292
- return False
293
-
294
- def install_package(self, package_name: str, upgrade: bool = False) -> bool:
386
+ return False, "", str(e)
387
+ except Exception as e:
388
+ console.print(f"[error]执行过程中发生异常: {e}[/]")
389
+ return False, "", str(e)
390
+
391
+ def _compare_versions(self, version1: str, version2: str) -> int:
392
+ """
393
+ 比较两个版本号
394
+
395
+ :param version1: 版本号1
396
+ :param version2: 版本号2
397
+ :return: 1 if version1 > version2, -1 if version1 < version2, 0 if equal
398
+ """
399
+ from packaging import version as comparison
400
+ try:
401
+ v1 = comparison.parse(version1)
402
+ v2 = comparison.parse(version2)
403
+ if v1 > v2:
404
+ return 1
405
+ elif v1 < v2:
406
+ return -1
407
+ else:
408
+ return 0
409
+ except comparison.InvalidVersion:
410
+ # 如果无法解析,使用字符串比较作为后备
411
+ if version1 > version2:
412
+ return 1
413
+ elif version1 < version2:
414
+ return -1
415
+ else:
416
+ return 0
417
+
418
+ def _check_sdk_compatibility(self, min_sdk_version: str) -> Tuple[bool, str]:
419
+ """
420
+ 检查SDK版本兼容性
421
+
422
+ :param min_sdk_version: 所需的最小SDK版本
423
+ :return: (是否兼容, 当前版本信息)
424
+ """
425
+ try:
426
+ from ErisPulse import __version__
427
+ current_version = __version__
428
+ except ImportError:
429
+ current_version = "unknown"
430
+
431
+ if current_version == "unknown":
432
+ return True, "无法确定当前SDK版本"
433
+
434
+ try:
435
+ compatibility = self._compare_versions(current_version, min_sdk_version)
436
+ if compatibility >= 0:
437
+ return True, f"当前SDK版本 {current_version} 满足最低要求 {min_sdk_version}"
438
+ else:
439
+ return False, f"当前SDK版本 {current_version} 低于最低要求 {min_sdk_version}"
440
+ except Exception:
441
+ return True, "无法验证SDK版本兼容性"
442
+
443
+ async def _get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]:
444
+ """
445
+ 获取包的详细信息(包括min_sdk_version等)
446
+
447
+ :param package_name: 包名或别名
448
+ :return: 包信息字典
449
+ """
450
+ # 首先尝试通过别名查找
451
+ normalized_name = self._normalize_name(package_name)
452
+ remote_packages = await self.get_remote_packages()
453
+
454
+ # 检查模块
455
+ for name, info in remote_packages["modules"].items():
456
+ if self._normalize_name(name) == normalized_name:
457
+ return info
458
+
459
+ # 检查适配器
460
+ for name, info in remote_packages["adapters"].items():
461
+ if self._normalize_name(name) == normalized_name:
462
+ return info
463
+
464
+ # 检查CLI扩展
465
+ for name, info in remote_packages.get("cli_extensions", {}).items():
466
+ if self._normalize_name(name) == normalized_name:
467
+ return info
468
+
469
+ return None
470
+
471
+ def install_package(self, package_names: List[str], upgrade: bool = False, pre: bool = False) -> bool:
295
472
  """
296
- 安装指定包
473
+ 安装指定包(支持多个包)
297
474
 
298
- :param package_name: 要安装的包名
475
+ :param package_names: 要安装的包名或别名列表
299
476
  :param upgrade: 是否升级已安装的包
477
+ :param pre: 是否包含预发布版本
300
478
  :return: 安装是否成功
301
479
  """
302
- cmd = ["install"]
303
- if upgrade:
304
- cmd.append("--upgrade")
305
- cmd.append(package_name)
480
+ all_success = True
306
481
 
307
- success = self._run_pip_command(cmd, f"安装 {package_name}")
308
-
309
- if success:
310
- console.print(f"[success]包 {package_name} 安装成功[/]")
311
- else:
312
- console.print(f"[error]包 {package_name} 安装失败[/]")
482
+ for package_name in package_names:
483
+ # 首先尝试通过别名查找实际包名
484
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
313
485
 
314
- return success
486
+ if actual_package:
487
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
488
+ current_package_name = actual_package
489
+ else:
490
+ console.print(f"[info]未找到别名,将直接安装: [package]{package_name}[/][/]")
491
+ current_package_name = package_name
492
+
493
+ # 检查SDK版本兼容性
494
+ package_info = asyncio.run(self._get_package_info(package_name))
495
+ if package_info and "min_sdk_version" in package_info:
496
+ is_compatible, message = self._check_sdk_compatibility(package_info["min_sdk_version"])
497
+ if not is_compatible:
498
+ console.print(Panel(
499
+ f"[warning]SDK版本兼容性警告[/]\n"
500
+ f"包 [package]{current_package_name}[/] 需要最低SDK版本 {package_info['min_sdk_version']}\n"
501
+ f"{message}\n\n"
502
+ f"继续安装可能会导致问题。",
503
+ title="兼容性警告",
504
+ border_style="warning"
505
+ ))
506
+ if not Confirm.ask("是否继续安装?", default=False):
507
+ console.print("[info]已取消安装[/]")
508
+ all_success = False
509
+ continue
510
+ else:
511
+ console.print(f"[success]{message}[/]")
512
+
513
+ # 构建pip命令
514
+ cmd = ["install"]
515
+ if upgrade:
516
+ cmd.append("--upgrade")
517
+ if pre:
518
+ cmd.append("--pre")
519
+ cmd.append(current_package_name)
520
+
521
+ # 执行安装命令
522
+ success, stdout, stderr = self._run_pip_command_with_output(cmd, f"安装 {current_package_name}")
523
+
524
+ if success:
525
+ console.print(Panel(
526
+ f"[success]包 {current_package_name} 安装成功[/]\n\n"
527
+ f"[dim]{stdout}[/]",
528
+ title="安装完成",
529
+ border_style="success"
530
+ ))
531
+ else:
532
+ console.print(Panel(
533
+ f"[error]包 {current_package_name} 安装失败[/]\n\n"
534
+ f"[dim]{stderr}[/]",
535
+ title="安装失败",
536
+ border_style="error"
537
+ ))
538
+ all_success = False
539
+
540
+ return all_success
315
541
 
316
- def uninstall_package(self, package_name: str) -> bool:
542
+ def uninstall_package(self, package_names: List[str]) -> bool:
317
543
  """
318
- 卸载指定包
544
+ 卸载指定包(支持多个包,支持别名)
319
545
 
320
- :param package_name: 要卸载的包名
546
+ :param package_names: 要卸载的包名或别名列表
321
547
  :return: 卸载是否成功
322
548
  """
323
- success = self._run_pip_command(
324
- ["uninstall", "-y", package_name],
325
- f"卸载 {package_name}"
326
- )
549
+ all_success = True
327
550
 
328
- if success:
329
- console.print(f"[success]包 {package_name} 卸载成功[/]")
330
- else:
331
- console.print(f"[error]包 {package_name} 卸载失败[/]")
551
+ packages_to_uninstall = []
552
+
553
+ # 首先处理所有包名,查找实际包名
554
+ for package_name in package_names:
555
+ # 首先尝试通过别名查找实际包名
556
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
332
557
 
333
- return success
558
+ if actual_package:
559
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
560
+ packages_to_uninstall.append(actual_package)
561
+ else:
562
+ # 如果找不到别名映射,检查是否是已安装的包
563
+ installed_package = self._find_installed_package_by_name(package_name)
564
+ if installed_package:
565
+ package_name = installed_package
566
+ console.print(f"[info]找到已安装包: [bold]{package_name}[/][/]")
567
+ packages_to_uninstall.append(package_name)
568
+ else:
569
+ console.print(f"[warning]未找到别名映射,将尝试直接卸载: [package]{package_name}[/][/]")
570
+ packages_to_uninstall.append(package_name)
571
+
572
+ # 确认卸载操作
573
+ package_list = "\n".join([f" - [package]{pkg}[/]" for pkg in packages_to_uninstall])
574
+ if not Confirm.ask(f"确认卸载以下包吗?\n{package_list}", default=False):
575
+ console.print("[info]操作已取消[/]")
576
+ return False
577
+
578
+ # 执行卸载命令
579
+ for package_name in packages_to_uninstall:
580
+ success, stdout, stderr = self._run_pip_command_with_output(
581
+ ["uninstall", "-y", package_name],
582
+ f"卸载 {package_name}"
583
+ )
584
+
585
+ if success:
586
+ console.print(Panel(
587
+ f"[success]包 {package_name} 卸载成功[/]\n\n"
588
+ f"[dim]{stdout}[/]",
589
+ title="卸载完成",
590
+ border_style="success"
591
+ ))
592
+ else:
593
+ console.print(Panel(
594
+ f"[error]包 {package_name} 卸载失败[/]\n\n"
595
+ f"[dim]{stderr}[/]",
596
+ title="卸载失败",
597
+ border_style="error"
598
+ ))
599
+ all_success = False
600
+
601
+ return all_success
334
602
 
335
603
  def upgrade_all(self) -> bool:
336
604
  """
@@ -362,7 +630,7 @@ class PackageManager:
362
630
 
363
631
  results = {}
364
632
  for pkg in sorted(all_packages):
365
- results[pkg] = self.install_package(pkg, upgrade=True)
633
+ results[pkg] = self.install_package([pkg], upgrade=True)
366
634
 
367
635
  failed = [pkg for pkg, success in results.items() if not success]
368
636
  if failed:
@@ -375,6 +643,272 @@ class PackageManager:
375
643
 
376
644
  return True
377
645
 
646
+ def upgrade_package(self, package_names: List[str], pre: bool = False) -> bool:
647
+ """
648
+ 升级指定包(支持多个包)
649
+
650
+ :param package_names: 要升级的包名或别名列表
651
+ :param pre: 是否包含预发布版本
652
+ :return: 升级是否成功
653
+ """
654
+ all_success = True
655
+
656
+ for package_name in package_names:
657
+ # 首先尝试通过别名查找实际包名
658
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
659
+
660
+ if actual_package:
661
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
662
+ current_package_name = actual_package
663
+ else:
664
+ current_package_name = package_name
665
+
666
+ # 检查SDK版本兼容性
667
+ package_info = asyncio.run(self._get_package_info(package_name))
668
+ if package_info and "min_sdk_version" in package_info:
669
+ is_compatible, message = self._check_sdk_compatibility(package_info["min_sdk_version"])
670
+ if not is_compatible:
671
+ console.print(Panel(
672
+ f"[warning]SDK版本兼容性警告[/]\n"
673
+ f"包 [package]{current_package_name}[/] 需要最低SDK版本 {package_info['min_sdk_version']}\n"
674
+ f"{message}\n\n"
675
+ f"继续升级可能会导致问题。",
676
+ title="兼容性警告",
677
+ border_style="warning"
678
+ ))
679
+ if not Confirm.ask("是否继续升级?", default=False):
680
+ console.print("[info]已取消升级[/]")
681
+ all_success = False
682
+ continue
683
+ else:
684
+ console.print(f"[success]{message}[/]")
685
+
686
+ # 构建pip命令
687
+ cmd = ["install", "--upgrade"]
688
+ if pre:
689
+ cmd.append("--pre")
690
+ cmd.append(current_package_name)
691
+
692
+ # 执行升级命令
693
+ success, stdout, stderr = self._run_pip_command_with_output(cmd, f"升级 {current_package_name}")
694
+
695
+ if success:
696
+ console.print(Panel(
697
+ f"[success]包 {current_package_name} 升级成功[/]\n\n"
698
+ f"[dim]{stdout}[/]",
699
+ title="升级完成",
700
+ border_style="success"
701
+ ))
702
+ else:
703
+ console.print(Panel(
704
+ f"[error]包 {current_package_name} 升级失败[/]\n\n"
705
+ f"[dim]{stderr}[/]",
706
+ title="升级失败",
707
+ border_style="error"
708
+ ))
709
+ all_success = False
710
+
711
+ return all_success
712
+
713
+ def search_package(self, query: str) -> Dict[str, List[Dict[str, str]]]:
714
+ """
715
+ 搜索包(本地和远程)
716
+
717
+ :param query: 搜索关键词
718
+ :return: 匹配的包信息
719
+ """
720
+ normalized_query = self._normalize_name(query)
721
+ results = {"installed": [], "remote": []}
722
+
723
+ # 搜索已安装的包
724
+ installed = self.get_installed_packages()
725
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
726
+ for name, info in installed[pkg_type].items():
727
+ if (normalized_query in self._normalize_name(name) or
728
+ normalized_query in self._normalize_name(info["package"]) or
729
+ normalized_query in self._normalize_name(info["summary"])):
730
+ results["installed"].append({
731
+ "type": pkg_type[:-1] if pkg_type.endswith("s") else pkg_type, # 移除复数s
732
+ "name": name,
733
+ "package": info["package"],
734
+ "version": info["version"],
735
+ "summary": info["summary"]
736
+ })
737
+
738
+ # 搜索远程包
739
+ remote = asyncio.run(self.get_remote_packages())
740
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
741
+ for name, info in remote[pkg_type].items():
742
+ if (normalized_query in self._normalize_name(name) or
743
+ normalized_query in self._normalize_name(info["package"]) or
744
+ normalized_query in self._normalize_name(info.get("description", "")) or
745
+ normalized_query in self._normalize_name(info.get("summary", ""))):
746
+ results["remote"].append({
747
+ "type": pkg_type[:-1] if pkg_type.endswith("s") else pkg_type, # 移除复数s
748
+ "name": name,
749
+ "package": info["package"],
750
+ "version": info["version"],
751
+ "summary": info.get("description", info.get("summary", ""))
752
+ })
753
+
754
+ return results
755
+
756
+ def get_installed_version(self) -> str:
757
+ """
758
+ 获取当前安装的ErisPulse版本
759
+
760
+ :return: 当前版本号
761
+ """
762
+ try:
763
+ from ErisPulse import __version__
764
+ return __version__
765
+ except ImportError:
766
+ return "unknown"
767
+
768
+ async def get_pypi_versions(self) -> List[Dict[str, Any]]:
769
+ """
770
+ 从PyPI获取ErisPulse的所有可用版本
771
+
772
+ :return: 版本信息列表
773
+ """
774
+ import aiohttp
775
+ from aiohttp import ClientError, ClientTimeout
776
+ from packaging import version as comparison
777
+
778
+ timeout = ClientTimeout(total=10)
779
+ url = "https://pypi.org/pypi/ErisPulse/json"
780
+
781
+ try:
782
+ async with aiohttp.ClientSession(timeout=timeout) as session:
783
+ async with session.get(url) as response:
784
+ if response.status == 200:
785
+ data = await response.json()
786
+ versions = []
787
+ for version_str, releases in data["releases"].items():
788
+ if releases: # 只包含有文件的版本
789
+ release_info = {
790
+ "version": version_str,
791
+ "uploaded": releases[0].get("upload_time_iso_8601", ""),
792
+ "pre_release": self._is_pre_release(version_str)
793
+ }
794
+ versions.append(release_info)
795
+
796
+ # 使用版本比较函数正确排序版本
797
+ versions.sort(key=lambda x: comparison.parse(x["version"]), reverse=True)
798
+ return versions
799
+ except (ClientError, asyncio.TimeoutError, json.JSONDecodeError, KeyError, Exception) as e:
800
+ console.print(f"[error]获取PyPI版本信息失败: {e}[/]")
801
+ return []
802
+
803
+ def _is_pre_release(self, version: str) -> bool:
804
+ """
805
+ 判断版本是否为预发布版本
806
+
807
+ :param version: 版本号
808
+ :return: 是否为预发布版本
809
+ """
810
+ import re
811
+ # 检查是否包含预发布标识符 (alpha, beta, rc, dev等)
812
+ pre_release_pattern = re.compile(r'(a|b|rc|dev|alpha|beta)\d*', re.IGNORECASE)
813
+ return bool(pre_release_pattern.search(version))
814
+
815
+ def update_self(self, target_version: str = None, force: bool = False) -> bool:
816
+ """
817
+ 更新ErisPulse SDK本身
818
+
819
+ :param target_version: 目标版本号,None表示更新到最新版本
820
+ :param force: 是否强制更新
821
+ :return: 更新是否成功
822
+ """
823
+ current_version = self.get_installed_version()
824
+
825
+ if target_version and target_version == current_version and not force:
826
+ console.print(f"[info]当前已是目标版本 [bold]{current_version}[/][/]")
827
+ return True
828
+
829
+ # 确定要安装的版本
830
+ package_spec = "ErisPulse"
831
+ if target_version:
832
+ package_spec += f"=={target_version}"
833
+
834
+ # 检查是否在Windows上且尝试更新自身
835
+ if sys.platform == "win32":
836
+ # 构建更新脚本
837
+ update_script = f"""
838
+ import time
839
+ import subprocess
840
+ import sys
841
+ import os
842
+
843
+ # 等待原进程结束
844
+ time.sleep(2)
845
+
846
+ # 执行更新命令
847
+ try:
848
+ result = subprocess.run([
849
+ sys.executable, "-m", "pip", "install", "--upgrade", "{package_spec}"
850
+ ], capture_output=True, text=True, timeout=300)
851
+
852
+ if result.returncode == 0:
853
+ print("更新成功!")
854
+ print(result.stdout)
855
+ else:
856
+ print("更新失败:")
857
+ print(result.stderr)
858
+ except Exception as e:
859
+ print(f"更新过程中出错: {{e}}")
860
+
861
+ # 清理临时脚本
862
+ try:
863
+ os.remove(__file__)
864
+ except:
865
+ pass
866
+ """
867
+ # 创建临时更新脚本
868
+ import tempfile
869
+ script_path = os.path.join(tempfile.gettempdir(), "epsdk_update.py")
870
+ with open(script_path, "w", encoding="utf-8") as f:
871
+ f.write(update_script)
872
+
873
+ # 启动更新进程并退出当前进程
874
+ console.print("[info]正在启动更新进程...[/]")
875
+ console.print("[info]请稍后重新运行CLI以使用新版本[/]")
876
+
877
+ subprocess.Popen([
878
+ sys.executable, script_path
879
+ ], creationflags=subprocess.CREATE_NEW_CONSOLE)
880
+
881
+ return True
882
+ else:
883
+ # 非Windows平台
884
+ success, stdout, stderr = self._run_pip_command_with_output(
885
+ ["install", "--upgrade", package_spec],
886
+ f"更新 ErisPulse SDK {f'到 {target_version}' if target_version else '到最新版本'}"
887
+ )
888
+
889
+ if success:
890
+ new_version = target_version or "最新版本"
891
+ console.print(Panel(
892
+ f"[success]ErisPulse SDK 更新成功[/]\n"
893
+ f" 当前版本: [bold]{current_version}[/]\n"
894
+ f" 更新版本: [bold]{new_version}[/]\n\n"
895
+ f"[dim]{stdout}[/]",
896
+ title="更新完成",
897
+ border_style="success"
898
+ ))
899
+
900
+ if not target_version:
901
+ console.print("[info]请重新启动CLI以使用新版本[/]")
902
+ else:
903
+ console.print(Panel(
904
+ f"[error]ErisPulse SDK 更新失败[/]\n\n"
905
+ f"[dim]{stderr}[/]",
906
+ title="更新失败",
907
+ border_style="error"
908
+ ))
909
+
910
+ return success
911
+
378
912
  class ReloadHandler(FileSystemEventHandler):
379
913
  """
380
914
  文件系统事件处理器
@@ -454,12 +988,44 @@ class ReloadHandler(FileSystemEventHandler):
454
988
 
455
989
  def _handle_reload(self, event, reason: str):
456
990
  """
457
- 处理重载逻辑
458
-
991
+ 处理热重载逻辑
459
992
  :param event: 文件系统事件
460
- :param reason: 重载原因描述
993
+ :param reason: 重载原因
461
994
  """
462
- console.print(f"\n[reload]{reason}: [path]{event.src_path}[/][/]")
995
+ from ErisPulse.Core import adapter, logger
996
+ # 在重载前确保所有适配器正确停止
997
+ try:
998
+ # 检查适配器是否正在运行
999
+ if hasattr(adapter, '_started_instances') and adapter._started_instances:
1000
+ logger.info("正在停止适配器...")
1001
+ # 创建新的事件循环来运行异步停止操作
1002
+ import asyncio
1003
+ import threading
1004
+
1005
+ # 如果在主线程中
1006
+ if threading.current_thread() is threading.main_thread():
1007
+ try:
1008
+ # 尝试获取当前事件循环
1009
+ loop = asyncio.get_running_loop()
1010
+ # 在新线程中运行适配器停止
1011
+ stop_thread = threading.Thread(target=lambda: asyncio.run(adapter.shutdown()))
1012
+ stop_thread.start()
1013
+ stop_thread.join(timeout=10) # 最多等待10秒
1014
+ except RuntimeError:
1015
+ # 没有运行中的事件循环
1016
+ asyncio.run(adapter.shutdown())
1017
+ else:
1018
+ # 在非主线程中,创建新的事件循环
1019
+ new_loop = asyncio.new_event_loop()
1020
+ asyncio.set_event_loop(new_loop)
1021
+ new_loop.run_until_complete(adapter.shutdown())
1022
+
1023
+ logger.info("适配器已停止")
1024
+ except Exception as e:
1025
+ logger.warning(f"停止适配器时出错: {e}")
1026
+
1027
+ # 原有的重载逻辑
1028
+ logger.info(f"检测到文件变更 ({reason}),正在重启...")
463
1029
  self._terminate_process()
464
1030
  self.start_process()
465
1031
 
@@ -519,11 +1085,12 @@ class CLI:
519
1085
  # 安装命令
520
1086
  install_parser = subparsers.add_parser(
521
1087
  'install',
522
- help='安装模块/适配器包'
1088
+ help='安装模块/适配器包(支持多个,用空格分隔)'
523
1089
  )
524
1090
  install_parser.add_argument(
525
1091
  'package',
526
- help='要安装的包名或模块/适配器简称'
1092
+ nargs='+', # 改为接受多个参数
1093
+ help='要安装的包名或模块/适配器简称(可指定多个)'
527
1094
  )
528
1095
  install_parser.add_argument(
529
1096
  '--upgrade', '-U',
@@ -539,11 +1106,12 @@ class CLI:
539
1106
  # 卸载命令
540
1107
  uninstall_parser = subparsers.add_parser(
541
1108
  'uninstall',
542
- help='卸载模块/适配器包'
1109
+ help='卸载模块/适配器包(支持多个,用空格分隔)'
543
1110
  )
544
1111
  uninstall_parser.add_argument(
545
1112
  'package',
546
- help='要卸载的包名'
1113
+ nargs='+', # 改为接受多个参数
1114
+ help='要卸载的包名(可指定多个)'
547
1115
  )
548
1116
 
549
1117
  # 模块管理命令
@@ -613,11 +1181,11 @@ class CLI:
613
1181
  # 升级命令
614
1182
  upgrade_parser = subparsers.add_parser(
615
1183
  'upgrade',
616
- help='升级组件'
1184
+ help='升级组件(支持多个,用空格分隔)'
617
1185
  )
618
1186
  upgrade_parser.add_argument(
619
1187
  'package',
620
- nargs='?',
1188
+ nargs='*', # 改为接受可选的多个参数
621
1189
  help='要升级的包名 (可选,不指定则升级所有)'
622
1190
  )
623
1191
  upgrade_parser.add_argument(
@@ -625,6 +1193,52 @@ class CLI:
625
1193
  action='store_true',
626
1194
  help='跳过确认直接升级'
627
1195
  )
1196
+ upgrade_parser.add_argument(
1197
+ '--pre',
1198
+ action='store_true',
1199
+ help='包含预发布版本'
1200
+ )
1201
+
1202
+ # 搜索命令
1203
+ search_parser = subparsers.add_parser(
1204
+ 'search',
1205
+ help='搜索模块/适配器包'
1206
+ )
1207
+ search_parser.add_argument(
1208
+ 'query',
1209
+ help='搜索关键词'
1210
+ )
1211
+ search_parser.add_argument(
1212
+ '--installed', '-i',
1213
+ action='store_true',
1214
+ help='仅搜索已安装的包'
1215
+ )
1216
+ search_parser.add_argument(
1217
+ '--remote', '-r',
1218
+ action='store_true',
1219
+ help='仅搜索远程包'
1220
+ )
1221
+
1222
+ # 自更新命令
1223
+ self_update_parser = subparsers.add_parser(
1224
+ 'self-update',
1225
+ help='更新ErisPulse SDK本身'
1226
+ )
1227
+ self_update_parser.add_argument(
1228
+ 'version',
1229
+ nargs='?',
1230
+ help='要更新到的版本号 (可选,默认为最新版本)'
1231
+ )
1232
+ self_update_parser.add_argument(
1233
+ '--pre',
1234
+ action='store_true',
1235
+ help='包含预发布版本'
1236
+ )
1237
+ self_update_parser.add_argument(
1238
+ '--force', '-f',
1239
+ action='store_true',
1240
+ help='强制更新,即使版本相同'
1241
+ )
628
1242
 
629
1243
  # 运行命令
630
1244
  run_parser = subparsers.add_parser(
@@ -899,23 +1513,133 @@ class CLI:
899
1513
 
900
1514
  def _resolve_package_name(self, short_name: str) -> Optional[str]:
901
1515
  """
902
- 解析简称到完整包名
1516
+ 解析简称到完整包名(大小写不敏感)
903
1517
 
904
1518
  :param short_name: 模块/适配器简称
905
1519
  :return: 完整包名,未找到返回None
906
1520
  """
1521
+ normalized_name = self.package_manager._normalize_name(short_name)
907
1522
  remote_packages = asyncio.run(self.package_manager.get_remote_packages())
908
1523
 
909
1524
  # 检查模块
910
- if short_name in remote_packages["modules"]:
911
- return remote_packages["modules"][short_name]["package"]
912
-
1525
+ for name, info in remote_packages["modules"].items():
1526
+ if self.package_manager._normalize_name(name) == normalized_name:
1527
+ return info["package"]
1528
+
913
1529
  # 检查适配器
914
- if short_name in remote_packages["adapters"]:
915
- return remote_packages["adapters"][short_name]["package"]
916
-
1530
+ for name, info in remote_packages["adapters"].items():
1531
+ if self.package_manager._normalize_name(name) == normalized_name:
1532
+ return info["package"]
1533
+
917
1534
  return None
918
1535
 
1536
+ def _print_search_results(self, query: str, results: Dict[str, List[Dict[str, str]]]):
1537
+ """
1538
+ 打印搜索结果
1539
+
1540
+ :param query: 搜索关键词
1541
+ :param results: 搜索结果
1542
+ """
1543
+ if not results["installed"] and not results["remote"]:
1544
+ console.print(f"[info]未找到与 '[bold]{query}[/]' 匹配的包[/]")
1545
+ return
1546
+
1547
+ # 打印已安装的包
1548
+ if results["installed"]:
1549
+ table = Table(
1550
+ title="已安装的包",
1551
+ box=SIMPLE,
1552
+ header_style="info"
1553
+ )
1554
+ table.add_column("类型")
1555
+ table.add_column("名称")
1556
+ table.add_column("包名")
1557
+ table.add_column("版本")
1558
+ table.add_column("描述")
1559
+
1560
+ for item in results["installed"]:
1561
+ table.add_row(
1562
+ item["type"],
1563
+ item["name"],
1564
+ item["package"],
1565
+ item["version"],
1566
+ item["summary"]
1567
+ )
1568
+
1569
+ console.print(table)
1570
+
1571
+ # 打印远程包
1572
+ if results["remote"]:
1573
+ table = Table(
1574
+ title="远程包",
1575
+ box=SIMPLE,
1576
+ header_style="info"
1577
+ )
1578
+ table.add_column("类型")
1579
+ table.add_column("名称")
1580
+ table.add_column("包名")
1581
+ table.add_column("版本")
1582
+ table.add_column("描述")
1583
+
1584
+ for item in results["remote"]:
1585
+ table.add_row(
1586
+ item["type"],
1587
+ item["name"],
1588
+ item["package"],
1589
+ item["version"],
1590
+ item["summary"]
1591
+ )
1592
+
1593
+ console.print(table)
1594
+
1595
+ def _print_version_list(self, versions: List[Dict[str, Any]], include_pre: bool = False):
1596
+ """
1597
+ 打印版本列表
1598
+
1599
+ :param versions: 版本信息列表
1600
+ :param include_pre: 是否包含预发布版本
1601
+ """
1602
+ if not versions:
1603
+ console.print("[info]未找到可用版本[/]")
1604
+ return
1605
+
1606
+ table = Table(
1607
+ title="可用版本",
1608
+ box=SIMPLE,
1609
+ header_style="info"
1610
+ )
1611
+ table.add_column("序号")
1612
+ table.add_column("版本")
1613
+ table.add_column("类型")
1614
+ table.add_column("上传时间")
1615
+
1616
+ displayed = 0
1617
+ version_list = []
1618
+ for version_info in versions:
1619
+ # 如果不包含预发布版本,则跳过预发布版本
1620
+ if not include_pre and version_info["pre_release"]:
1621
+ continue
1622
+
1623
+ version_list.append(version_info)
1624
+ version_type = "[yellow]预发布[/]" if version_info["pre_release"] else "[green]稳定版[/]"
1625
+ table.add_row(
1626
+ str(displayed + 1),
1627
+ version_info["version"],
1628
+ version_type,
1629
+ version_info["uploaded"][:10] if version_info["uploaded"] else "未知"
1630
+ )
1631
+ displayed += 1
1632
+
1633
+ # 只显示前10个版本
1634
+ if displayed >= 10:
1635
+ break
1636
+
1637
+ if displayed == 0:
1638
+ console.print("[info]没有找到符合条件的版本[/]")
1639
+ else:
1640
+ console.print(table)
1641
+ return version_list
1642
+
919
1643
  def _setup_watchdog(self, script_path: str, reload_mode: bool):
920
1644
  """
921
1645
  设置文件监控
@@ -980,16 +1704,19 @@ class CLI:
980
1704
 
981
1705
  try:
982
1706
  if args.command == "install":
983
- full_package = self._resolve_package_name(args.package)
984
- if full_package:
985
- console.print(f"[info]找到远程包: [bold]{args.package}[/] → [package]{full_package}[/][/]")
986
- self.package_manager.install_package(full_package, args.upgrade)
987
- else:
988
- self.package_manager.install_package(args.package, args.upgrade)
1707
+ success = self.package_manager.install_package(
1708
+ args.package, # 现在是列表
1709
+ upgrade=args.upgrade,
1710
+ pre=args.pre
1711
+ )
1712
+ if not success:
1713
+ sys.exit(1)
989
1714
 
990
1715
  elif args.command == "uninstall":
991
- self.package_manager.uninstall_package(args.package)
992
-
1716
+ success = self.package_manager.uninstall_package(args.package) # 现在是列表
1717
+ if not success:
1718
+ sys.exit(1)
1719
+
993
1720
  elif args.command == "module":
994
1721
  from ErisPulse.Core import mods
995
1722
  installed = self.package_manager.get_installed_packages()
@@ -1030,11 +1757,165 @@ class CLI:
1030
1757
 
1031
1758
  elif args.command == "upgrade":
1032
1759
  if args.package:
1033
- self.package_manager.install_package(args.package, upgrade=True)
1760
+ success = self.package_manager.upgrade_package(
1761
+ args.package, # 现在是列表
1762
+ pre=args.pre
1763
+ )
1764
+ if not success:
1765
+ sys.exit(1)
1034
1766
  else:
1035
- if args.force or Confirm.ask("确定要升级所有ErisPulse组件吗?"):
1036
- self.package_manager.upgrade_all()
1767
+ if args.force or Confirm.ask("确定要升级所有ErisPulse组件吗?", default=False):
1768
+ success = self.package_manager.upgrade_all()
1769
+ if not success:
1770
+ sys.exit(1)
1771
+
1772
+ elif args.command == "search":
1773
+ results = self.package_manager.search_package(args.query)
1774
+
1775
+ # 根据选项过滤结果
1776
+ if args.installed:
1777
+ results["remote"] = []
1778
+ elif args.remote:
1779
+ results["installed"] = []
1780
+
1781
+ self._print_search_results(args.query, results)
1782
+
1783
+ elif args.command == "self-update":
1784
+ current_version = self.package_manager.get_installed_version()
1785
+ console.print(Panel(
1786
+ f"[title]ErisPulse SDK 自更新[/]\n"
1787
+ f"当前版本: [bold]{current_version}[/]",
1788
+ title_align="left"
1789
+ ))
1790
+
1791
+ # 获取可用版本
1792
+ with console.status("[bold green]正在获取版本信息...", spinner="dots"):
1793
+ versions = asyncio.run(self.package_manager.get_pypi_versions())
1794
+
1795
+ if not versions:
1796
+ console.print("[error]无法获取版本信息[/]")
1797
+ sys.exit(1)
1798
+
1799
+ # 交互式选择更新选项
1800
+ if not args.version:
1801
+ # 显示最新版本
1802
+ stable_versions = [v for v in versions if not v["pre_release"]]
1803
+ pre_versions = [v for v in versions if v["pre_release"]]
1804
+
1805
+ latest_stable = stable_versions[0] if stable_versions else None
1806
+ latest_pre = pre_versions[0] if pre_versions and args.pre else None
1807
+
1808
+ choices = []
1809
+ choice_versions = {}
1810
+ choice_index = {}
1811
+
1812
+ if latest_stable:
1813
+ choice = f"最新稳定版 ({latest_stable['version']})"
1814
+ choices.append(choice)
1815
+ choice_versions[choice] = latest_stable['version']
1816
+ choice_index[len(choices)] = choice
1037
1817
 
1818
+ if args.pre and latest_pre:
1819
+ choice = f"最新预发布版 ({latest_pre['version']})"
1820
+ choices.append(choice)
1821
+ choice_versions[choice] = latest_pre['version']
1822
+ choice_index[len(choices)] = choice
1823
+
1824
+ # 添加其他选项
1825
+ choices.append("查看所有版本")
1826
+ choices.append("手动指定版本")
1827
+ choices.append("取消")
1828
+
1829
+ # 创建数字索引映射
1830
+ for i, choice in enumerate(choices, 1):
1831
+ choice_index[i] = choice
1832
+
1833
+ # 显示选项
1834
+ console.print("\n[info]请选择更新选项:[/]")
1835
+ for i, choice in enumerate(choices, 1):
1836
+ console.print(f" {i}. {choice}")
1837
+
1838
+ while True:
1839
+ try:
1840
+ selected_input = Prompt.ask(
1841
+ "请输入选项编号",
1842
+ default="1"
1843
+ )
1844
+
1845
+ if selected_input.isdigit():
1846
+ selected_index = int(selected_input)
1847
+ if selected_index in choice_index:
1848
+ selected = choice_index[selected_index]
1849
+ break
1850
+ else:
1851
+ console.print("[warning]请输入有效的选项编号[/]")
1852
+ else:
1853
+ # 检查是否是选项文本
1854
+ if selected_input in choices:
1855
+ selected = selected_input
1856
+ break
1857
+ else:
1858
+ console.print("[warning]请输入有效的选项编号或选项名称[/]")
1859
+ except KeyboardInterrupt:
1860
+ console.print("\n[info]操作已取消[/]")
1861
+ sys.exit(0)
1862
+
1863
+ if selected == "取消":
1864
+ console.print("[info]操作已取消[/]")
1865
+ sys.exit(0)
1866
+ elif selected == "手动指定版本":
1867
+ target_version = Prompt.ask("请输入要更新到的版本号")
1868
+ if not any(v['version'] == target_version for v in versions):
1869
+ console.print(f"[warning]版本 {target_version} 可能不存在[/]")
1870
+ if not Confirm.ask("是否继续?", default=False):
1871
+ sys.exit(0)
1872
+ elif selected == "查看所有版本":
1873
+ version_list = self._print_version_list(versions, include_pre=args.pre)
1874
+ if not version_list:
1875
+ console.print("[info]没有可用版本[/]")
1876
+ sys.exit(0)
1877
+
1878
+ # 显示版本选择
1879
+ console.print("\n[info]请选择要更新到的版本:[/]")
1880
+ while True:
1881
+ try:
1882
+ version_input = Prompt.ask("请输入版本序号或版本号")
1883
+ if version_input.isdigit():
1884
+ version_index = int(version_input)
1885
+ if 1 <= version_index <= len(version_list):
1886
+ target_version = version_list[version_index - 1]['version']
1887
+ break
1888
+ else:
1889
+ console.print("[warning]请输入有效的版本序号[/]")
1890
+ else:
1891
+ # 检查是否是有效的版本号
1892
+ if any(v['version'] == version_input for v in version_list):
1893
+ target_version = version_input
1894
+ break
1895
+ else:
1896
+ console.print("[warning]请输入有效的版本序号或版本号[/]")
1897
+ except KeyboardInterrupt:
1898
+ console.print("\n[info]操作已取消[/]")
1899
+ sys.exit(0)
1900
+ else:
1901
+ target_version = choice_versions[selected]
1902
+ else:
1903
+ target_version = args.version
1904
+
1905
+ # 确认更新
1906
+ if target_version == current_version and not args.force:
1907
+ console.print(f"[info]当前已是目标版本 [bold]{current_version}[/][/]")
1908
+ sys.exit(0)
1909
+ elif not args.force:
1910
+ if not Confirm.ask(f"确认将ErisPulse SDK从 [bold]{current_version}[/] 更新到 [bold]{target_version}[/] 吗?", default=False):
1911
+ console.print("[info]操作已取消[/]")
1912
+ sys.exit(0)
1913
+
1914
+ # 执行更新
1915
+ success = self.package_manager.update_self(target_version, args.force)
1916
+ if not success:
1917
+ sys.exit(1)
1918
+
1038
1919
  elif args.command == "run":
1039
1920
  script = args.script or "main.py"
1040
1921
  if not os.path.exists(script):
@@ -1046,9 +1927,10 @@ class CLI:
1046
1927
 
1047
1928
  try:
1048
1929
  while True:
1049
- time.sleep(1)
1930
+ time.sleep(0.5)
1050
1931
  except KeyboardInterrupt:
1051
1932
  console.print("\n[info]正在安全关闭...[/]")
1933
+ self._cleanup_adapters()
1052
1934
  self._cleanup()
1053
1935
  console.print("[success]已安全退出[/]")
1054
1936
 
@@ -1087,6 +1969,44 @@ class CLI:
1087
1969
  console.print(traceback.format_exc())
1088
1970
  self._cleanup()
1089
1971
  sys.exit(1)
1972
+
1973
+ def _cleanup_adapters(self):
1974
+ """
1975
+ 清理适配器资源
1976
+ """
1977
+ from ErisPulse import adapter, logger
1978
+ try:
1979
+ import asyncio
1980
+ import threading
1981
+
1982
+ # 检查是否有正在运行的适配器
1983
+ if (hasattr(adapter, '_started_instances') and
1984
+ adapter._started_instances):
1985
+
1986
+ logger.info("正在停止所有适配器...")
1987
+
1988
+ if threading.current_thread() is threading.main_thread():
1989
+ try:
1990
+ loop = asyncio.get_running_loop()
1991
+ if loop.is_running():
1992
+ # 在新线程中运行
1993
+ stop_thread = threading.Thread(
1994
+ target=lambda: asyncio.run(adapter.shutdown())
1995
+ )
1996
+ stop_thread.start()
1997
+ stop_thread.join(timeout=5)
1998
+ else:
1999
+ asyncio.run(adapter.shutdown())
2000
+ except RuntimeError:
2001
+ asyncio.run(adapter.shutdown())
2002
+ else:
2003
+ new_loop = asyncio.new_event_loop()
2004
+ asyncio.set_event_loop(new_loop)
2005
+ new_loop.run_until_complete(adapter.shutdown())
2006
+
2007
+ logger.info("适配器已全部停止")
2008
+ except Exception as e:
2009
+ logger.error(f"清理适配器资源时出错: {e}")
1090
2010
 
1091
2011
  def main():
1092
2012
  """