ErisPulse 2.2.1.dev0__py3-none-any.whl → 2.3.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.
Files changed (36) hide show
  1. ErisPulse/Core/Bases/__init__.py +14 -0
  2. ErisPulse/Core/Bases/adapter.py +196 -0
  3. ErisPulse/Core/Bases/module.py +54 -0
  4. ErisPulse/Core/Event/__init__.py +14 -0
  5. ErisPulse/Core/Event/base.py +15 -2
  6. ErisPulse/Core/Event/command.py +21 -2
  7. ErisPulse/Core/Event/message.py +15 -0
  8. ErisPulse/Core/Event/meta.py +15 -0
  9. ErisPulse/Core/Event/notice.py +15 -0
  10. ErisPulse/Core/Event/request.py +16 -1
  11. ErisPulse/Core/__init__.py +38 -19
  12. ErisPulse/Core/{erispulse_config.py → _self_config.py} +27 -2
  13. ErisPulse/Core/adapter.py +374 -377
  14. ErisPulse/Core/config.py +137 -38
  15. ErisPulse/Core/exceptions.py +6 -1
  16. ErisPulse/Core/lifecycle.py +167 -0
  17. ErisPulse/Core/logger.py +97 -49
  18. ErisPulse/Core/module.py +279 -56
  19. ErisPulse/Core/router.py +112 -23
  20. ErisPulse/Core/storage.py +258 -77
  21. ErisPulse/Core/ux.py +635 -0
  22. ErisPulse/__init__.py +722 -244
  23. ErisPulse/__main__.py +1 -1999
  24. ErisPulse/utils/__init__.py +17 -0
  25. ErisPulse/utils/cli.py +1092 -0
  26. ErisPulse/utils/console.py +53 -0
  27. ErisPulse/utils/package_manager.py +845 -0
  28. ErisPulse/utils/reload_handler.py +111 -0
  29. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dist-info}/METADATA +24 -6
  30. erispulse-2.3.0.dist-info/RECORD +34 -0
  31. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dist-info}/WHEEL +1 -1
  32. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dist-info}/licenses/LICENSE +1 -1
  33. ErisPulse/Core/env.py +0 -15
  34. ErisPulse/Core/module_registry.py +0 -227
  35. erispulse-2.2.1.dev0.dist-info/RECORD +0 -26
  36. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dist-info}/entry_points.txt +0 -0
ErisPulse/__main__.py CHANGED
@@ -8,2005 +8,7 @@ ErisPulse SDK 命令行工具
8
8
  2. Windows平台需要colorama支持ANSI颜色
9
9
  {!--< /tips >!--}
10
10
  """
11
-
12
- import argparse
13
- import importlib.metadata
14
- import subprocess
15
- import sys
16
- import os
17
- import time
18
- import json
19
- import asyncio
20
- from urllib.parse import urlparse
21
- from typing import List, Dict, Tuple, Optional, Callable, Any
22
- from watchdog.observers import Observer
23
- from watchdog.events import FileSystemEventHandler
24
-
25
- # Rich console setup
26
- from rich.console import Console
27
- from rich.panel import Panel
28
- from rich.table import Table
29
- from rich.progress import Progress, BarColumn, TextColumn
30
- from rich.prompt import Confirm, Prompt
31
- from rich.text import Text
32
- from rich.box import SIMPLE, ROUNDED, DOUBLE
33
- from rich.style import Style
34
- from rich.theme import Theme
35
- from rich.layout import Layout
36
- from rich.live import Live
37
- from rich.markdown import Markdown
38
- from rich.highlighter import RegexHighlighter
39
-
40
- # 确保在Windows上启用颜色
41
- if sys.platform == "win32":
42
- from colorama import init
43
- init()
44
-
45
- class CommandHighlighter(RegexHighlighter):
46
- """
47
- 高亮CLI命令和参数
48
-
49
- {!--< tips >!--}
50
- 使用正则表达式匹配命令行参数和选项
51
- {!--< /tips >!--}
52
- """
53
- highlights = [
54
- r"(?P<switch>\-\-?\w+)",
55
- r"(?P<option>\[\w+\])",
56
- r"(?P<command>\b\w+\b)",
57
- ]
58
-
59
- # 主题配置
60
- theme = Theme({
61
- "info": "dim cyan",
62
- "success": "bold green",
63
- "warning": "bold yellow",
64
- "error": "bold red",
65
- "title": "bold magenta",
66
- "default": "default",
67
- "progress": "green",
68
- "progress.remaining": "white",
69
- "cmd": "bold blue",
70
- "param": "italic cyan",
71
- "switch": "bold yellow",
72
- "module": "bold green",
73
- "adapter": "bold yellow",
74
- "cli": "bold magenta",
75
- })
76
-
77
- # 全局控制台实例
78
- console = Console(
79
- theme=theme,
80
- color_system="auto",
81
- force_terminal=True,
82
- highlighter=CommandHighlighter()
83
- )
84
-
85
- class PackageManager:
86
- """
87
- ErisPulse包管理器
88
-
89
- 提供包安装、卸载、升级和查询功能
90
-
91
- {!--< tips >!--}
92
- 1. 支持本地和远程包管理
93
- 2. 包含1小时缓存机制
94
- {!--< /tips >!--}
95
- """
96
- REMOTE_SOURCES = [
97
- "https://erisdev.com/packages.json",
98
- "https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/packages.json"
99
- ]
100
-
101
- CACHE_EXPIRY = 3600 # 1小时缓存
102
-
103
- def __init__(self):
104
- """初始化包管理器"""
105
- self._cache = {}
106
- self._cache_time = {}
107
-
108
- async def _fetch_remote_packages(self, url: str) -> Optional[dict]:
109
- """
110
- 从指定URL获取远程包数据
111
-
112
- :param url: 远程包数据URL
113
- :return: 解析后的JSON数据,失败返回None
114
-
115
- :raises ClientError: 网络请求失败时抛出
116
- :raises JSONDecodeError: JSON解析失败时抛出
117
- """
118
- import aiohttp
119
- from aiohttp import ClientError, ClientTimeout
120
-
121
- timeout = ClientTimeout(total=10)
122
- try:
123
- async with aiohttp.ClientSession(timeout=timeout) as session:
124
- async with session.get(url) as response:
125
- if response.status == 200:
126
- data = await response.text()
127
- return json.loads(data)
128
- except (ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
129
- console.print(f"[warning]获取远程包数据失败 ({url}): {e}[/]")
130
- return None
131
-
132
- async def get_remote_packages(self, force_refresh: bool = False) -> dict:
133
- """
134
- 获取远程包列表,带缓存机制
135
-
136
- :param force_refresh: 是否强制刷新缓存
137
- :return: 包含模块和适配器的字典
138
-
139
- :return:
140
- dict: {
141
- "modules": {模块名: 模块信息},
142
- "adapters": {适配器名: 适配器信息},
143
- "cli_extensions": {扩展名: 扩展信息}
144
- }
145
- """
146
- # 检查缓存
147
- cache_key = "remote_packages"
148
- if not force_refresh and cache_key in self._cache:
149
- if time.time() - self._cache_time[cache_key] < self.CACHE_EXPIRY:
150
- return self._cache[cache_key]
151
-
152
- last_error = None
153
- result = {"modules": {}, "adapters": {}, "cli_extensions": {}}
154
-
155
- for url in self.REMOTE_SOURCES:
156
- data = await self._fetch_remote_packages(url)
157
- if data:
158
- result["modules"].update(data.get("modules", {}))
159
- result["adapters"].update(data.get("adapters", {}))
160
- result["cli_extensions"].update(data.get("cli_extensions", {}))
161
- break
162
-
163
- # 更新缓存
164
- self._cache[cache_key] = result
165
- self._cache_time[cache_key] = time.time()
166
-
167
- return result
168
-
169
- def get_installed_packages(self) -> Dict[str, Dict[str, Dict[str, str]]]:
170
- """
171
- 获取已安装的包信息
172
-
173
- :return: 已安装包字典,包含模块、适配器和CLI扩展
174
-
175
- :return:
176
- dict: {
177
- "modules": {模块名: 模块信息},
178
- "adapters": {适配器名: 适配器信息},
179
- "cli_extensions": {扩展名: 扩展信息}
180
- }
181
- """
182
- packages = {
183
- "modules": {},
184
- "adapters": {},
185
- "cli_extensions": {}
186
- }
187
-
188
- try:
189
- # 查找模块和适配器
190
- entry_points = importlib.metadata.entry_points()
191
-
192
- # 处理模块
193
- if hasattr(entry_points, 'select'):
194
- module_entries = entry_points.select(group='erispulse.module')
195
- else:
196
- module_entries = entry_points.get('erispulse.module', [])
197
-
198
- for entry in module_entries:
199
- dist = entry.dist
200
- packages["modules"][entry.name] = {
201
- "package": dist.metadata["Name"],
202
- "version": dist.version,
203
- "summary": dist.metadata["Summary"],
204
- "enabled": self._is_module_enabled(entry.name)
205
- }
206
-
207
- # 处理适配器
208
- if hasattr(entry_points, 'select'):
209
- adapter_entries = entry_points.select(group='erispulse.adapter')
210
- else:
211
- adapter_entries = entry_points.get('erispulse.adapter', [])
212
-
213
- for entry in adapter_entries:
214
- dist = entry.dist
215
- packages["adapters"][entry.name] = {
216
- "package": dist.metadata["Name"],
217
- "version": dist.version,
218
- "summary": dist.metadata["Summary"]
219
- }
220
-
221
- # 查找CLI扩展
222
- if hasattr(entry_points, 'select'):
223
- cli_entries = entry_points.select(group='erispulse.cli')
224
- else:
225
- cli_entries = entry_points.get('erispulse.cli', [])
226
-
227
- for entry in cli_entries:
228
- dist = entry.dist
229
- packages["cli_extensions"][entry.name] = {
230
- "package": dist.metadata["Name"],
231
- "version": dist.version,
232
- "summary": dist.metadata["Summary"]
233
- }
234
-
235
- except Exception as e:
236
- print(f"[error] 获取已安装包信息失败: {e}")
237
- import traceback
238
- print(traceback.format_exc())
239
-
240
- return packages
241
-
242
- def _is_module_enabled(self, module_name: str) -> bool:
243
- """
244
- 检查模块是否启用
245
-
246
- :param module_name: 模块名称
247
- :return: 模块是否启用
248
-
249
- :raises ImportError: 核心模块不可用时抛出
250
- """
251
- try:
252
- from ErisPulse.Core import module_registry
253
- return module_registry.get_module_status(module_name)
254
- except ImportError:
255
- return True
256
- except Exception:
257
- return False
258
-
259
- def _normalize_name(self, name: str) -> str:
260
- """
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命令并捕获输出
325
-
326
- :param args: pip命令参数列表
327
- :param description: 进度条描述
328
- :return: (是否成功, 标准输出, 标准错误)
329
- """
330
- with Progress(
331
- TextColumn(f"[progress.description]{description}"),
332
- BarColumn(complete_style="progress.download"),
333
- transient=True
334
- ) as progress:
335
- task = progress.add_task("", total=100)
336
-
337
- try:
338
- process = subprocess.Popen(
339
- [sys.executable, "-m", "pip"] + args,
340
- stdout=subprocess.PIPE,
341
- stderr=subprocess.PIPE,
342
- universal_newlines=True,
343
- bufsize=1 # 行缓冲
344
- )
345
-
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
384
- except subprocess.CalledProcessError as e:
385
- console.print(f"[error]命令执行失败: {e}[/]")
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:
472
- """
473
- 安装指定包(支持多个包)
474
-
475
- :param package_names: 要安装的包名或别名列表
476
- :param upgrade: 是否升级已安装的包
477
- :param pre: 是否包含预发布版本
478
- :return: 安装是否成功
479
- """
480
- all_success = True
481
-
482
- for package_name in package_names:
483
- # 首先尝试通过别名查找实际包名
484
- actual_package = asyncio.run(self._find_package_by_alias(package_name))
485
-
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
541
-
542
- def uninstall_package(self, package_names: List[str]) -> bool:
543
- """
544
- 卸载指定包(支持多个包,支持别名)
545
-
546
- :param package_names: 要卸载的包名或别名列表
547
- :return: 卸载是否成功
548
- """
549
- all_success = True
550
-
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))
557
-
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
602
-
603
- def upgrade_all(self) -> bool:
604
- """
605
- 升级所有已安装的ErisPulse包
606
-
607
- :return: 升级是否成功
608
-
609
- :raises KeyboardInterrupt: 用户取消操作时抛出
610
- """
611
- installed = self.get_installed_packages()
612
- all_packages = set()
613
-
614
- for pkg_type in ["modules", "adapters", "cli_extensions"]:
615
- for pkg_info in installed[pkg_type].values():
616
- all_packages.add(pkg_info["package"])
617
-
618
- if not all_packages:
619
- console.print("[info]没有找到可升级的ErisPulse包[/]")
620
- return False
621
-
622
- console.print(Panel(
623
- f"找到 [bold]{len(all_packages)}[/] 个可升级的包:\n" +
624
- "\n".join(f" - [package]{pkg}[/]" for pkg in sorted(all_packages)),
625
- title="升级列表"
626
- ))
627
-
628
- if not Confirm.ask("确认升级所有包吗?", default=False):
629
- return False
630
-
631
- results = {}
632
- for pkg in sorted(all_packages):
633
- results[pkg] = self.install_package([pkg], upgrade=True)
634
-
635
- failed = [pkg for pkg, success in results.items() if not success]
636
- if failed:
637
- console.print(Panel(
638
- f"以下包升级失败:\n" + "\n".join(f" - [error]{pkg}[/]" for pkg in failed),
639
- title="警告",
640
- style="warning"
641
- ))
642
- return False
643
-
644
- return True
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
-
912
- class ReloadHandler(FileSystemEventHandler):
913
- """
914
- 文件系统事件处理器
915
-
916
- 实现热重载功能,监控文件变化并重启进程
917
-
918
- {!--< tips >!--}
919
- 1. 支持.py文件修改重载
920
- 2. 支持配置文件修改重载
921
- {!--< /tips >!--}
922
- """
923
-
924
- def __init__(self, script_path: str, reload_mode: bool = False):
925
- """
926
- 初始化处理器
927
-
928
- :param script_path: 要监控的脚本路径
929
- :param reload_mode: 是否启用重载模式
930
- """
931
- super().__init__()
932
- self.script_path = os.path.abspath(script_path)
933
- self.process = None
934
- self.last_reload = time.time()
935
- self.reload_mode = reload_mode
936
- self.start_process()
937
- self.watched_files = set()
938
-
939
- def start_process(self):
940
- """启动监控进程"""
941
- if self.process:
942
- self._terminate_process()
943
-
944
- console.print(f"[bold]启动进程: [path]{self.script_path}[/][/]")
945
- try:
946
- self.process = subprocess.Popen(
947
- [sys.executable, self.script_path],
948
- stdin=sys.stdin,
949
- stdout=sys.stdout,
950
- stderr=sys.stderr
951
- )
952
- self.last_reload = time.time()
953
- except Exception as e:
954
- console.print(f"[error]启动进程失败: {e}[/]")
955
- raise
956
-
957
- def _terminate_process(self):
958
- """
959
- 终止当前进程
960
-
961
- :raises subprocess.TimeoutExpired: 进程终止超时时抛出
962
- """
963
- try:
964
- self.process.terminate()
965
- # 等待最多2秒让进程正常退出
966
- self.process.wait(timeout=2)
967
- except subprocess.TimeoutExpired:
968
- console.print("[warning]进程未正常退出,强制终止...[/]")
969
- self.process.kill()
970
- self.process.wait()
971
- except Exception as e:
972
- console.print(f"[error]终止进程时出错: {e}[/]")
973
-
974
- def on_modified(self, event):
975
- """
976
- 文件修改事件处理
977
-
978
- :param event: 文件系统事件
979
- """
980
- now = time.time()
981
- if now - self.last_reload < 1.0: # 防抖
982
- return
983
-
984
- if event.src_path.endswith(".py") and self.reload_mode:
985
- self._handle_reload(event, "文件变动")
986
- elif event.src_path.endswith(("config.toml", ".env")):
987
- self._handle_reload(event, "配置变动")
988
-
989
- def _handle_reload(self, event, reason: str):
990
- """
991
- 处理热重载逻辑
992
- :param event: 文件系统事件
993
- :param reason: 重载原因
994
- """
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}),正在重启...")
1029
- self._terminate_process()
1030
- self.start_process()
1031
-
1032
- class CLI:
1033
- """
1034
- ErisPulse命令行接口
1035
-
1036
- 提供完整的命令行交互功能
1037
-
1038
- {!--< tips >!--}
1039
- 1. 支持动态加载第三方命令
1040
- 2. 支持模块化子命令系统
1041
- {!--< /tips >!--}
1042
- """
1043
-
1044
- def __init__(self):
1045
- """初始化CLI"""
1046
- self.parser = self._create_parser()
1047
- self.package_manager = PackageManager()
1048
- self.observer = None
1049
- self.handler = None
1050
-
1051
- def _create_parser(self) -> argparse.ArgumentParser:
1052
- """
1053
- 创建命令行参数解析器
1054
-
1055
- :return: 配置好的ArgumentParser实例
1056
- """
1057
- parser = argparse.ArgumentParser(
1058
- prog="epsdk",
1059
- formatter_class=argparse.RawDescriptionHelpFormatter,
1060
- description="ErisPulse SDK 命令行工具\n\n一个功能强大的模块化系统管理工具,用于管理ErisPulse生态系统中的模块、适配器和扩展。",
1061
- )
1062
- parser._positionals.title = "命令"
1063
- parser._optionals.title = "选项"
1064
-
1065
- # 全局选项
1066
- parser.add_argument(
1067
- "--version", "-V",
1068
- action="store_true",
1069
- help="显示版本信息"
1070
- )
1071
- parser.add_argument(
1072
- "--verbose", "-v",
1073
- action="count",
1074
- default=0,
1075
- help="增加输出详细程度 (-v, -vv, -vvv)"
1076
- )
1077
-
1078
- # 子命令
1079
- subparsers = parser.add_subparsers(
1080
- dest='command',
1081
- metavar="<命令>",
1082
- help="要执行的操作"
1083
- )
1084
-
1085
- # 安装命令
1086
- install_parser = subparsers.add_parser(
1087
- 'install',
1088
- help='安装模块/适配器包(支持多个,用空格分隔)'
1089
- )
1090
- install_parser.add_argument(
1091
- 'package',
1092
- nargs='+', # 改为接受多个参数
1093
- help='要安装的包名或模块/适配器简称(可指定多个)'
1094
- )
1095
- install_parser.add_argument(
1096
- '--upgrade', '-U',
1097
- action='store_true',
1098
- help='升级已安装的包'
1099
- )
1100
- install_parser.add_argument(
1101
- '--pre',
1102
- action='store_true',
1103
- help='包含预发布版本'
1104
- )
1105
-
1106
- # 卸载命令
1107
- uninstall_parser = subparsers.add_parser(
1108
- 'uninstall',
1109
- help='卸载模块/适配器包(支持多个,用空格分隔)'
1110
- )
1111
- uninstall_parser.add_argument(
1112
- 'package',
1113
- nargs='+', # 改为接受多个参数
1114
- help='要卸载的包名(可指定多个)'
1115
- )
1116
-
1117
- # 模块管理命令
1118
- module_parser = subparsers.add_parser(
1119
- 'module',
1120
- help='模块管理'
1121
- )
1122
- module_subparsers = module_parser.add_subparsers(
1123
- dest='module_command',
1124
- metavar="<子命令>"
1125
- )
1126
-
1127
- # 启用模块
1128
- enable_parser = module_subparsers.add_parser(
1129
- 'enable',
1130
- help='启用模块'
1131
- )
1132
- enable_parser.add_argument(
1133
- 'module',
1134
- help='要启用的模块名'
1135
- )
1136
-
1137
- # 禁用模块
1138
- disable_parser = module_subparsers.add_parser(
1139
- 'disable',
1140
- help='禁用模块'
1141
- )
1142
- disable_parser.add_argument(
1143
- 'module',
1144
- help='要禁用的模块名'
1145
- )
1146
-
1147
- # 列表命令
1148
- list_parser = subparsers.add_parser(
1149
- 'list',
1150
- help='列出已安装的组件'
1151
- )
1152
- list_parser.add_argument(
1153
- '--type', '-t',
1154
- choices=['modules', 'adapters', 'cli', 'all'],
1155
- default='all',
1156
- help='列出类型 (默认: all)'
1157
- )
1158
- list_parser.add_argument(
1159
- '--outdated', '-o',
1160
- action='store_true',
1161
- help='仅显示可升级的包'
1162
- )
1163
-
1164
- # 远程列表命令
1165
- list_remote_parser = subparsers.add_parser(
1166
- 'list-remote',
1167
- help='列出远程可用的组件'
1168
- )
1169
- list_remote_parser.add_argument(
1170
- '--type', '-t',
1171
- choices=['modules', 'adapters', 'cli', 'all'],
1172
- default='all',
1173
- help='列出类型 (默认: all)'
1174
- )
1175
- list_remote_parser.add_argument(
1176
- '--refresh', '-r',
1177
- action='store_true',
1178
- help='强制刷新远程包列表'
1179
- )
1180
-
1181
- # 升级命令
1182
- upgrade_parser = subparsers.add_parser(
1183
- 'upgrade',
1184
- help='升级组件(支持多个,用空格分隔)'
1185
- )
1186
- upgrade_parser.add_argument(
1187
- 'package',
1188
- nargs='*', # 改为接受可选的多个参数
1189
- help='要升级的包名 (可选,不指定则升级所有)'
1190
- )
1191
- upgrade_parser.add_argument(
1192
- '--force', '-f',
1193
- action='store_true',
1194
- help='跳过确认直接升级'
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
- )
1242
-
1243
- # 运行命令
1244
- run_parser = subparsers.add_parser(
1245
- 'run',
1246
- help='运行主程序'
1247
- )
1248
- run_parser.add_argument(
1249
- 'script',
1250
- nargs='?',
1251
- help='要运行的主程序路径 (默认: main.py)'
1252
- )
1253
- run_parser.add_argument(
1254
- '--reload',
1255
- action='store_true',
1256
- help='启用热重载模式'
1257
- )
1258
- run_parser.add_argument(
1259
- '--no-reload',
1260
- action='store_true',
1261
- help='禁用热重载模式'
1262
- )
1263
-
1264
- # 初始化命令
1265
- init_parser = subparsers.add_parser(
1266
- 'init',
1267
- help='初始化ErisPulse项目'
1268
- )
1269
- init_parser.add_argument(
1270
- '--force', '-f',
1271
- action='store_true',
1272
- help='强制覆盖现有配置'
1273
- )
1274
-
1275
- # 加载第三方命令
1276
- self._load_external_commands(subparsers)
1277
-
1278
- return parser
1279
-
1280
- def _get_external_commands(self) -> List[str]:
1281
- """
1282
- 获取所有已注册的第三方命令名称
1283
-
1284
- :return: 第三方命令名称列表
1285
- """
1286
- try:
1287
- entry_points = importlib.metadata.entry_points()
1288
- if hasattr(entry_points, 'select'):
1289
- cli_entries = entry_points.select(group='erispulse.cli')
1290
- else:
1291
- cli_entries = entry_points.get('erispulse.cli', [])
1292
- return [entry.name for entry in cli_entries]
1293
- except Exception:
1294
- return []
1295
-
1296
- def _load_external_commands(self, subparsers):
1297
- """
1298
- 加载第三方CLI命令
1299
-
1300
- :param subparsers: 子命令解析器
1301
-
1302
- :raises ImportError: 加载命令失败时抛出
1303
- """
1304
- try:
1305
- entry_points = importlib.metadata.entry_points()
1306
- if hasattr(entry_points, 'select'):
1307
- cli_entries = entry_points.select(group='erispulse.cli')
1308
- else:
1309
- cli_entries = entry_points.get('erispulse.cli', [])
1310
-
1311
- for entry in cli_entries:
1312
- try:
1313
- cli_func = entry.load()
1314
- if callable(cli_func):
1315
- cli_func(subparsers, console)
1316
- else:
1317
- console.print(f"[warning]模块 {entry.name} 的入口点不是可调用对象[/]")
1318
- except Exception as e:
1319
- console.print(f"[error]加载第三方命令 {entry.name} 失败: {e}[/]")
1320
- except Exception as e:
1321
- console.print(f"[warning]加载第三方CLI命令失败: {e}[/]")
1322
-
1323
- def _print_version(self):
1324
- """打印版本信息"""
1325
- from ErisPulse import __version__
1326
- console.print(Panel(
1327
- f"[title]ErisPulse SDK[/] 版本: [bold]{__version__}[/]",
1328
- subtitle=f"Python {sys.version.split()[0]}",
1329
- style="title"
1330
- ))
1331
-
1332
- def _print_installed_packages(self, pkg_type: str, outdated_only: bool = False):
1333
- """
1334
- 打印已安装包信息
1335
-
1336
- :param pkg_type: 包类型 (modules/adapters/cli/all)
1337
- :param outdated_only: 是否只显示可升级的包
1338
- """
1339
- installed = self.package_manager.get_installed_packages()
1340
-
1341
- if pkg_type == "modules" and installed["modules"]:
1342
- table = Table(
1343
- title="已安装模块",
1344
- box=SIMPLE,
1345
- header_style="module"
1346
- )
1347
- table.add_column("模块名", style="module")
1348
- table.add_column("包名")
1349
- table.add_column("版本")
1350
- table.add_column("状态")
1351
- table.add_column("描述")
1352
-
1353
- for name, info in installed["modules"].items():
1354
- if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
1355
- continue
1356
-
1357
- status = "[green]已启用[/]" if info.get("enabled", True) else "[yellow]已禁用[/]"
1358
- table.add_row(
1359
- name,
1360
- info["package"],
1361
- info["version"],
1362
- status,
1363
- info["summary"]
1364
- )
1365
-
1366
- console.print(table)
1367
-
1368
- if pkg_type == "adapters" and installed["adapters"]:
1369
- table = Table(
1370
- title="已安装适配器",
1371
- box=SIMPLE,
1372
- header_style="adapter"
1373
- )
1374
- table.add_column("适配器名", style="adapter")
1375
- table.add_column("包名")
1376
- table.add_column("版本")
1377
- table.add_column("描述")
1378
-
1379
- for name, info in installed["adapters"].items():
1380
- if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
1381
- continue
1382
-
1383
- table.add_row(
1384
- name,
1385
- info["package"],
1386
- info["version"],
1387
- info["summary"]
1388
- )
1389
-
1390
- console.print(table)
1391
-
1392
- if pkg_type == "cli" and installed["cli_extensions"]:
1393
- table = Table(
1394
- title="已安装CLI扩展",
1395
- box=SIMPLE,
1396
- header_style="cli"
1397
- )
1398
- table.add_column("命令名", style="cli")
1399
- table.add_column("包名")
1400
- table.add_column("版本")
1401
- table.add_column("描述")
1402
-
1403
- for name, info in installed["cli_extensions"].items():
1404
- if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
1405
- continue
1406
-
1407
- table.add_row(
1408
- name,
1409
- info["package"],
1410
- info["version"],
1411
- info["summary"]
1412
- )
1413
-
1414
- console.print(table)
1415
-
1416
- def _print_remote_packages(self, pkg_type: str):
1417
- """
1418
- 打印远程包信息
1419
-
1420
- :param pkg_type: 包类型 (modules/adapters/cli/all)
1421
- """
1422
- remote_packages = asyncio.run(self.package_manager.get_remote_packages())
1423
-
1424
- if pkg_type == "modules" and remote_packages["modules"]:
1425
- table = Table(
1426
- title="远程模块",
1427
- box=SIMPLE,
1428
- header_style="module"
1429
- )
1430
- table.add_column("模块名", style="module")
1431
- table.add_column("包名")
1432
- table.add_column("最新版本")
1433
- table.add_column("描述")
1434
-
1435
- for name, info in remote_packages["modules"].items():
1436
- table.add_row(
1437
- name,
1438
- info["package"],
1439
- info["version"],
1440
- info["description"]
1441
- )
1442
-
1443
- console.print(table)
1444
-
1445
- if pkg_type == "adapters" and remote_packages["adapters"]:
1446
- table = Table(
1447
- title="远程适配器",
1448
- box=SIMPLE,
1449
- header_style="adapter"
1450
- )
1451
- table.add_column("适配器名", style="adapter")
1452
- table.add_column("包名")
1453
- table.add_column("最新版本")
1454
- table.add_column("描述")
1455
-
1456
- for name, info in remote_packages["adapters"].items():
1457
- table.add_row(
1458
- name,
1459
- info["package"],
1460
- info["version"],
1461
- info["description"]
1462
- )
1463
-
1464
- console.print(table)
1465
-
1466
- if pkg_type == "cli" and remote_packages.get("cli_extensions"):
1467
- table = Table(
1468
- title="远程CLI扩展",
1469
- box=SIMPLE,
1470
- header_style="cli"
1471
- )
1472
- table.add_column("命令名", style="cli")
1473
- table.add_column("包名")
1474
- table.add_column("最新版本")
1475
- table.add_column("描述")
1476
-
1477
- for name, info in remote_packages["cli_extensions"].items():
1478
- table.add_row(
1479
- name,
1480
- info["package"],
1481
- info["version"],
1482
- info["description"]
1483
- )
1484
-
1485
- console.print(table)
1486
-
1487
- def _is_package_outdated(self, package_name: str, current_version: str) -> bool:
1488
- """
1489
- 检查包是否过时
1490
-
1491
- :param package_name: 包名
1492
- :param current_version: 当前版本
1493
- :return: 是否有新版本可用
1494
- """
1495
- remote_packages = asyncio.run(self.package_manager.get_remote_packages())
1496
-
1497
- # 检查模块
1498
- for module_info in remote_packages["modules"].values():
1499
- if module_info["package"] == package_name:
1500
- return module_info["version"] != current_version
1501
-
1502
- # 检查适配器
1503
- for adapter_info in remote_packages["adapters"].values():
1504
- if adapter_info["package"] == package_name:
1505
- return adapter_info["version"] != current_version
1506
-
1507
- # 检查CLI扩展
1508
- for cli_info in remote_packages.get("cli_extensions", {}).values():
1509
- if cli_info["package"] == package_name:
1510
- return cli_info["version"] != current_version
1511
-
1512
- return False
1513
-
1514
- def _resolve_package_name(self, short_name: str) -> Optional[str]:
1515
- """
1516
- 解析简称到完整包名(大小写不敏感)
1517
-
1518
- :param short_name: 模块/适配器简称
1519
- :return: 完整包名,未找到返回None
1520
- """
1521
- normalized_name = self.package_manager._normalize_name(short_name)
1522
- remote_packages = asyncio.run(self.package_manager.get_remote_packages())
1523
-
1524
- # 检查模块
1525
- for name, info in remote_packages["modules"].items():
1526
- if self.package_manager._normalize_name(name) == normalized_name:
1527
- return info["package"]
1528
-
1529
- # 检查适配器
1530
- for name, info in remote_packages["adapters"].items():
1531
- if self.package_manager._normalize_name(name) == normalized_name:
1532
- return info["package"]
1533
-
1534
- return None
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
-
1643
- def _setup_watchdog(self, script_path: str, reload_mode: bool):
1644
- """
1645
- 设置文件监控
1646
-
1647
- :param script_path: 要监控的脚本路径
1648
- :param reload_mode: 是否启用重载模式
1649
- """
1650
- watch_dirs = [
1651
- os.path.dirname(os.path.abspath(script_path)),
1652
- ]
1653
-
1654
- # 添加配置目录
1655
- config_dir = os.path.abspath(os.getcwd())
1656
- if config_dir not in watch_dirs:
1657
- watch_dirs.append(config_dir)
1658
-
1659
- self.handler = ReloadHandler(script_path, reload_mode)
1660
- self.observer = Observer()
1661
-
1662
- for d in watch_dirs:
1663
- if os.path.exists(d):
1664
- self.observer.schedule(
1665
- self.handler,
1666
- d,
1667
- recursive=reload_mode
1668
- )
1669
- console.print(f"[dim]监控目录: [path]{d}[/][/]")
1670
-
1671
- self.observer.start()
1672
-
1673
- mode_desc = "[bold]开发重载模式[/]" if reload_mode else "[bold]配置监控模式[/]"
1674
- console.print(Panel(
1675
- f"{mode_desc}\n监控目录: [path]{', '.join(watch_dirs)}[/]",
1676
- title="热重载已启动",
1677
- border_style="info"
1678
- ))
1679
-
1680
- def _cleanup(self):
1681
- """清理资源"""
1682
- if self.observer:
1683
- self.observer.stop()
1684
- if self.handler and self.handler.process:
1685
- self.handler._terminate_process()
1686
- self.observer.join()
1687
-
1688
- def run(self):
1689
- """
1690
- 运行CLI
1691
-
1692
- :raises KeyboardInterrupt: 用户中断时抛出
1693
- :raises Exception: 命令执行失败时抛出
1694
- """
1695
- args = self.parser.parse_args()
1696
-
1697
- if args.version:
1698
- self._print_version()
1699
- return
1700
-
1701
- if not args.command:
1702
- self.parser.print_help()
1703
- return
1704
-
1705
- try:
1706
- if args.command == "install":
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)
1714
-
1715
- elif args.command == "uninstall":
1716
- success = self.package_manager.uninstall_package(args.package) # 现在是列表
1717
- if not success:
1718
- sys.exit(1)
1719
-
1720
- elif args.command == "module":
1721
- from ErisPulse.Core import module_registry
1722
- installed = self.package_manager.get_installed_packages()
1723
-
1724
- if args.module_command == "enable":
1725
- if args.module not in installed["modules"]:
1726
- console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
1727
- else:
1728
- module_registry.set_module_status(args.module, True)
1729
- console.print(f"[success]模块 [bold]{args.module}[/] 已启用[/]")
1730
-
1731
- elif args.module_command == "disable":
1732
- if args.module not in installed["modules"]:
1733
- console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
1734
- else:
1735
- module_registry.set_module_status(args.module, False)
1736
- console.print(f"[warning]模块 [bold]{args.module}[/] 已禁用[/]")
1737
- else:
1738
- self.parser.parse_args(["module", "--help"])
1739
-
1740
- elif args.command == "list":
1741
- pkg_type = args.type
1742
- if pkg_type == "all":
1743
- self._print_installed_packages("modules", args.outdated)
1744
- self._print_installed_packages("adapters", args.outdated)
1745
- self._print_installed_packages("cli", args.outdated)
1746
- else:
1747
- self._print_installed_packages(pkg_type, args.outdated)
1748
-
1749
- elif args.command == "list-remote":
1750
- pkg_type = args.type
1751
- if pkg_type == "all":
1752
- self._print_remote_packages("modules")
1753
- self._print_remote_packages("adapters")
1754
- self._print_remote_packages("cli")
1755
- else:
1756
- self._print_remote_packages(pkg_type)
1757
-
1758
- elif args.command == "upgrade":
1759
- if args.package:
1760
- success = self.package_manager.upgrade_package(
1761
- args.package, # 现在是列表
1762
- pre=args.pre
1763
- )
1764
- if not success:
1765
- sys.exit(1)
1766
- else:
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
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
-
1919
- elif args.command == "run":
1920
- script = args.script or "main.py"
1921
- if not os.path.exists(script):
1922
- console.print(f"[error]找不到指定文件: [path]{script}[/][/]")
1923
- return
1924
-
1925
- reload_mode = args.reload and not args.no_reload
1926
- self._setup_watchdog(script, reload_mode)
1927
-
1928
- try:
1929
- while True:
1930
- time.sleep(0.5)
1931
- except KeyboardInterrupt:
1932
- console.print("\n[info]正在安全关闭...[/]")
1933
- self._cleanup_adapters()
1934
- self._cleanup()
1935
- console.print("[success]已安全退出[/]")
1936
-
1937
- elif args.command == "init":
1938
- from ErisPulse import sdk
1939
- sdk.init()
1940
- console.print("[success]ErisPulse项目初始化完成[/]")
1941
-
1942
- # 处理第三方命令
1943
- elif args.command in self._get_external_commands():
1944
- # 获取第三方命令的处理函数并执行
1945
- entry_points = importlib.metadata.entry_points()
1946
- if hasattr(entry_points, 'select'):
1947
- cli_entries = entry_points.select(group='erispulse.cli')
1948
- else:
1949
- cli_entries = entry_points.get('erispulse.cli', [])
1950
-
1951
- for entry in cli_entries:
1952
- if entry.name == args.command:
1953
- cli_func = entry.load()
1954
- if callable(cli_func):
1955
- # 创建一个新的解析器来解析第三方命令的参数
1956
- subparser = self.parser._subparsers._group_actions[0].choices[args.command]
1957
- parsed_args = subparser.parse_args(sys.argv[2:])
1958
- # 调用第三方命令处理函数
1959
- parsed_args.func(parsed_args)
1960
- break
1961
-
1962
- except KeyboardInterrupt:
1963
- console.print("\n[warning]操作被用户中断[/]")
1964
- self._cleanup()
1965
- except Exception as e:
1966
- console.print(f"[error]执行命令时出错: {e}[/]")
1967
- if args.verbose >= 1:
1968
- import traceback
1969
- console.print(traceback.format_exc())
1970
- self._cleanup()
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}")
11
+ from .utils import CLI
2010
12
 
2011
13
  def main():
2012
14
  """