ErisPulse 2.2.1.dev0__py3-none-any.whl → 2.3.0.dev5__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 (35) 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} +16 -3
  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 +664 -0
  22. ErisPulse/__init__.py +587 -242
  23. ErisPulse/__main__.py +1 -1999
  24. ErisPulse/utils/__init__.py +15 -0
  25. ErisPulse/utils/cli.py +1102 -0
  26. ErisPulse/utils/package_manager.py +847 -0
  27. ErisPulse/utils/reload_handler.py +135 -0
  28. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dev5.dist-info}/METADATA +24 -6
  29. erispulse-2.3.0.dev5.dist-info/RECORD +33 -0
  30. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dev5.dist-info}/WHEEL +1 -1
  31. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dev5.dist-info}/licenses/LICENSE +1 -1
  32. ErisPulse/Core/env.py +0 -15
  33. ErisPulse/Core/module_registry.py +0 -227
  34. erispulse-2.2.1.dev0.dist-info/RECORD +0 -26
  35. {erispulse-2.2.1.dev0.dist-info → erispulse-2.3.0.dev5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,847 @@
1
+ """
2
+ ErisPulse SDK 包管理器
3
+
4
+ 提供包安装、卸载、升级和查询功能
5
+ """
6
+
7
+ import os
8
+ import asyncio
9
+ import importlib.metadata
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ import time
14
+ from typing import List, Dict, Tuple, Optional, Any
15
+
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.progress import Progress, BarColumn, TextColumn
19
+ from rich.prompt import Confirm
20
+
21
+ # 全局控制台实例,从CLI模块导入
22
+ console = Console()
23
+
24
+ class PackageManager:
25
+ """
26
+ ErisPulse包管理器
27
+
28
+ 提供包安装、卸载、升级和查询功能
29
+
30
+ {!--< tips >!--}
31
+ 1. 支持本地和远程包管理
32
+ 2. 包含1小时缓存机制
33
+ {!--< /tips >!--}
34
+ """
35
+ REMOTE_SOURCES = [
36
+ "https://erisdev.com/packages.json",
37
+ "https://raw.githubusercontent.com/ErisPulse/ErisPulse/main/packages.json"
38
+ ]
39
+
40
+ CACHE_EXPIRY = 3600 # 1小时缓存
41
+
42
+ def __init__(self):
43
+ """初始化包管理器"""
44
+ self._cache = {}
45
+ self._cache_time = {}
46
+
47
+ async def _fetch_remote_packages(self, url: str) -> Optional[dict]:
48
+ """
49
+ 从指定URL获取远程包数据
50
+
51
+ :param url: 远程包数据URL
52
+ :return: 解析后的JSON数据,失败返回None
53
+
54
+ :raises ClientError: 网络请求失败时抛出
55
+ :raises JSONDecodeError: JSON解析失败时抛出
56
+ """
57
+ import aiohttp
58
+ from aiohttp import ClientError, ClientTimeout
59
+
60
+ timeout = ClientTimeout(total=10)
61
+ try:
62
+ async with aiohttp.ClientSession(timeout=timeout) as session:
63
+ async with session.get(url) as response:
64
+ if response.status == 200:
65
+ data = await response.text()
66
+ return json.loads(data)
67
+ except (ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
68
+ console.print(f"[warning]获取远程包数据失败 ({url}): {e}[/]")
69
+ return None
70
+
71
+ async def get_remote_packages(self, force_refresh: bool = False) -> dict:
72
+ """
73
+ 获取远程包列表,带缓存机制
74
+
75
+ :param force_refresh: 是否强制刷新缓存
76
+ :return: 包含模块和适配器的字典
77
+
78
+ :return:
79
+ dict: {
80
+ "modules": {模块名: 模块信息},
81
+ "adapters": {适配器名: 适配器信息},
82
+ "cli_extensions": {扩展名: 扩展信息}
83
+ }
84
+ """
85
+ # 检查缓存
86
+ cache_key = "remote_packages"
87
+ if not force_refresh and cache_key in self._cache:
88
+ if time.time() - self._cache_time[cache_key] < self.CACHE_EXPIRY:
89
+ return self._cache[cache_key]
90
+
91
+ result = {"modules": {}, "adapters": {}, "cli_extensions": {}}
92
+
93
+ for url in self.REMOTE_SOURCES:
94
+ data = await self._fetch_remote_packages(url)
95
+ if data:
96
+ result["modules"].update(data.get("modules", {}))
97
+ result["adapters"].update(data.get("adapters", {}))
98
+ result["cli_extensions"].update(data.get("cli_extensions", {}))
99
+ break
100
+
101
+ # 更新缓存
102
+ self._cache[cache_key] = result
103
+ self._cache_time[cache_key] = time.time()
104
+
105
+ return result
106
+
107
+ def get_installed_packages(self) -> Dict[str, Dict[str, Dict[str, str]]]:
108
+ """
109
+ 获取已安装的包信息
110
+
111
+ :return: 已安装包字典,包含模块、适配器和CLI扩展
112
+
113
+ :return:
114
+ dict: {
115
+ "modules": {模块名: 模块信息},
116
+ "adapters": {适配器名: 适配器信息},
117
+ "cli_extensions": {扩展名: 扩展信息}
118
+ }
119
+ """
120
+ packages = {
121
+ "modules": {},
122
+ "adapters": {},
123
+ "cli_extensions": {}
124
+ }
125
+
126
+ try:
127
+ # 查找模块和适配器
128
+ entry_points = importlib.metadata.entry_points()
129
+
130
+ # 处理模块
131
+ if hasattr(entry_points, 'select'):
132
+ module_entries = entry_points.select(group='erispulse.module')
133
+ else:
134
+ module_entries = entry_points.get('erispulse.module', [])
135
+
136
+ for entry in module_entries:
137
+ dist = entry.dist
138
+ packages["modules"][entry.name] = {
139
+ "package": dist.metadata["Name"],
140
+ "version": dist.version,
141
+ "summary": dist.metadata["Summary"],
142
+ "enabled": self._is_module_enabled(entry.name)
143
+ }
144
+
145
+ # 处理适配器
146
+ if hasattr(entry_points, 'select'):
147
+ adapter_entries = entry_points.select(group='erispulse.adapter')
148
+ else:
149
+ adapter_entries = entry_points.get('erispulse.adapter', [])
150
+
151
+ for entry in adapter_entries:
152
+ dist = entry.dist
153
+ packages["adapters"][entry.name] = {
154
+ "package": dist.metadata["Name"],
155
+ "version": dist.version,
156
+ "summary": dist.metadata["Summary"]
157
+ }
158
+
159
+ # 查找CLI扩展
160
+ if hasattr(entry_points, 'select'):
161
+ cli_entries = entry_points.select(group='erispulse.cli')
162
+ else:
163
+ cli_entries = entry_points.get('erispulse.cli', [])
164
+
165
+ for entry in cli_entries:
166
+ dist = entry.dist
167
+ packages["cli_extensions"][entry.name] = {
168
+ "package": dist.metadata["Name"],
169
+ "version": dist.version,
170
+ "summary": dist.metadata["Summary"]
171
+ }
172
+
173
+ except Exception as e:
174
+ print(f"[error] 获取已安装包信息失败: {e}")
175
+ import traceback
176
+ print(traceback.format_exc())
177
+
178
+ return packages
179
+
180
+ def _is_module_enabled(self, module_name: str) -> bool:
181
+ """
182
+ 检查模块是否启用
183
+
184
+ :param module_name: 模块名称
185
+ :return: 模块是否启用
186
+
187
+ :raises ImportError: 核心模块不可用时抛出
188
+ """
189
+ try:
190
+ from ErisPulse.Core import module as module_manager
191
+ return module_manager.is_enabled(module_name)
192
+ except ImportError:
193
+ return True
194
+ except Exception:
195
+ return False
196
+
197
+ def _normalize_name(self, name: str) -> str:
198
+ """
199
+ 标准化包名,统一转为小写以实现大小写不敏感比较
200
+
201
+ :param name: 原始名称
202
+ :return: 标准化后的名称
203
+ """
204
+ return name.lower().strip()
205
+
206
+ async def _find_package_by_alias(self, alias: str) -> Optional[str]:
207
+ """
208
+ 通过别名查找实际包名(大小写不敏感)
209
+
210
+ :param alias: 包别名
211
+ :return: 实际包名,未找到返回None
212
+ """
213
+ normalized_alias = self._normalize_name(alias)
214
+ remote_packages = await self.get_remote_packages()
215
+
216
+ # 检查模块
217
+ for name, info in remote_packages["modules"].items():
218
+ if self._normalize_name(name) == normalized_alias:
219
+ return info["package"]
220
+
221
+ # 检查适配器
222
+ for name, info in remote_packages["adapters"].items():
223
+ if self._normalize_name(name) == normalized_alias:
224
+ return info["package"]
225
+
226
+ # 检查CLI扩展
227
+ for name, info in remote_packages.get("cli_extensions", {}).items():
228
+ if self._normalize_name(name) == normalized_alias:
229
+ return info["package"]
230
+
231
+ return None
232
+
233
+ def _find_installed_package_by_name(self, name: str) -> Optional[str]:
234
+ """
235
+ 在已安装包中查找实际包名(大小写不敏感)
236
+
237
+ :param name: 包名或别名
238
+ :return: 实际包名,未找到返回None
239
+ """
240
+ normalized_name = self._normalize_name(name)
241
+ installed = self.get_installed_packages()
242
+
243
+ # 在已安装的模块中查找
244
+ for module_info in installed["modules"].values():
245
+ if self._normalize_name(module_info["package"]) == normalized_name:
246
+ return module_info["package"]
247
+
248
+ # 在已安装的适配器中查找
249
+ for adapter_info in installed["adapters"].values():
250
+ if self._normalize_name(adapter_info["package"]) == normalized_name:
251
+ return adapter_info["package"]
252
+
253
+ # 在已安装的CLI扩展中查找
254
+ for cli_info in installed["cli_extensions"].values():
255
+ if self._normalize_name(cli_info["package"]) == normalized_name:
256
+ return cli_info["package"]
257
+
258
+ return None
259
+
260
+ def _run_pip_command_with_output(self, args: List[str], description: str) -> Tuple[bool, str, str]:
261
+ """
262
+ 执行pip命令并捕获输出
263
+
264
+ :param args: pip命令参数列表
265
+ :param description: 进度条描述
266
+ :return: (是否成功, 标准输出, 标准错误)
267
+ """
268
+ with Progress(
269
+ TextColumn(f"[progress.description]{description}"),
270
+ BarColumn(complete_style="progress.download"),
271
+ transient=True
272
+ ) as progress:
273
+ task = progress.add_task("", total=100)
274
+
275
+ try:
276
+ process = subprocess.Popen(
277
+ [sys.executable, "-m", "pip"] + args,
278
+ stdout=subprocess.PIPE,
279
+ stderr=subprocess.PIPE,
280
+ universal_newlines=True,
281
+ bufsize=1 # 行缓冲
282
+ )
283
+
284
+ stdout_lines = []
285
+ stderr_lines = []
286
+
287
+ # 使用超时机制避免永久阻塞
288
+ import threading
289
+
290
+ def read_output(pipe, lines_list):
291
+ try:
292
+ for line in iter(pipe.readline, ''):
293
+ lines_list.append(line)
294
+ progress.update(task, advance=5) # 每行增加进度
295
+ pipe.close()
296
+ except Exception:
297
+ pass
298
+
299
+ stdout_thread = threading.Thread(target=read_output, args=(process.stdout, stdout_lines))
300
+ stderr_thread = threading.Thread(target=read_output, args=(process.stderr, stderr_lines))
301
+
302
+ stdout_thread.start()
303
+ stderr_thread.start()
304
+
305
+ # 等待进程结束,最多等待5分钟
306
+ try:
307
+ process.wait(timeout=300)
308
+ except subprocess.TimeoutExpired:
309
+ process.kill()
310
+ process.wait()
311
+ console.print("[warning]命令执行超时,已强制终止[/]")
312
+ return False, "", "命令执行超时"
313
+
314
+ stdout_thread.join(timeout=10)
315
+ stderr_thread.join(timeout=10)
316
+
317
+ stdout = ''.join(stdout_lines)
318
+ stderr = ''.join(stderr_lines)
319
+
320
+ return process.returncode == 0, stdout, stderr
321
+ except subprocess.CalledProcessError as e:
322
+ console.print(f"[error]命令执行失败: {e}[/]")
323
+ return False, "", str(e)
324
+ except Exception as e:
325
+ console.print(f"[error]执行过程中发生异常: {e}[/]")
326
+ return False, "", str(e)
327
+
328
+ def _compare_versions(self, version1: str, version2: str) -> int:
329
+ """
330
+ 比较两个版本号
331
+
332
+ :param version1: 版本号1
333
+ :param version2: 版本号2
334
+ :return: 1 if version1 > version2, -1 if version1 < version2, 0 if equal
335
+ """
336
+ from packaging import version as comparison
337
+ try:
338
+ v1 = comparison.parse(version1)
339
+ v2 = comparison.parse(version2)
340
+ if v1 > v2:
341
+ return 1
342
+ elif v1 < v2:
343
+ return -1
344
+ else:
345
+ return 0
346
+ except comparison.InvalidVersion:
347
+ # 如果无法解析,使用字符串比较作为后备
348
+ if version1 > version2:
349
+ return 1
350
+ elif version1 < version2:
351
+ return -1
352
+ else:
353
+ return 0
354
+
355
+ def _check_sdk_compatibility(self, min_sdk_version: str) -> Tuple[bool, str]:
356
+ """
357
+ 检查SDK版本兼容性
358
+
359
+ :param min_sdk_version: 所需的最小SDK版本
360
+ :return: (是否兼容, 当前版本信息)
361
+ """
362
+ try:
363
+ from ErisPulse import __version__
364
+ current_version = __version__
365
+ except ImportError:
366
+ current_version = "unknown"
367
+
368
+ if current_version == "unknown":
369
+ return True, "无法确定当前SDK版本"
370
+
371
+ try:
372
+ compatibility = self._compare_versions(current_version, min_sdk_version)
373
+ if compatibility >= 0:
374
+ return True, f"当前SDK版本 {current_version} 满足最低要求 {min_sdk_version}"
375
+ else:
376
+ return False, f"当前SDK版本 {current_version} 低于最低要求 {min_sdk_version}"
377
+ except Exception:
378
+ return True, "无法验证SDK版本兼容性"
379
+
380
+ async def _get_package_info(self, package_name: str) -> Optional[Dict[str, Any]]:
381
+ """
382
+ 获取包的详细信息(包括min_sdk_version等)
383
+
384
+ :param package_name: 包名或别名
385
+ :return: 包信息字典
386
+ """
387
+ # 首先尝试通过别名查找
388
+ normalized_name = self._normalize_name(package_name)
389
+ remote_packages = await self.get_remote_packages()
390
+
391
+ # 检查模块
392
+ for name, info in remote_packages["modules"].items():
393
+ if self._normalize_name(name) == normalized_name:
394
+ return info
395
+
396
+ # 检查适配器
397
+ for name, info in remote_packages["adapters"].items():
398
+ if self._normalize_name(name) == normalized_name:
399
+ return info
400
+
401
+ # 检查CLI扩展
402
+ for name, info in remote_packages.get("cli_extensions", {}).items():
403
+ if self._normalize_name(name) == normalized_name:
404
+ return info
405
+
406
+ return None
407
+
408
+ def install_package(self, package_names: List[str], upgrade: bool = False, pre: bool = False) -> bool:
409
+ """
410
+ 安装指定包(支持多个包)
411
+
412
+ :param package_names: 要安装的包名或别名列表
413
+ :param upgrade: 是否升级已安装的包
414
+ :param pre: 是否包含预发布版本
415
+ :return: 安装是否成功
416
+ """
417
+ all_success = True
418
+
419
+ for package_name in package_names:
420
+ # 首先尝试通过别名查找实际包名
421
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
422
+
423
+ if actual_package:
424
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
425
+ current_package_name = actual_package
426
+ else:
427
+ console.print(f"[info]未找到别名,将直接安装: [package]{package_name}[/][/]")
428
+ current_package_name = package_name
429
+
430
+ # 检查SDK版本兼容性
431
+ package_info = asyncio.run(self._get_package_info(package_name))
432
+ if package_info and "min_sdk_version" in package_info:
433
+ is_compatible, message = self._check_sdk_compatibility(package_info["min_sdk_version"])
434
+ if not is_compatible:
435
+ console.print(Panel(
436
+ f"[warning]SDK版本兼容性警告[/]\n"
437
+ f"包 [package]{current_package_name}[/] 需要最低SDK版本 {package_info['min_sdk_version']}\n"
438
+ f"{message}\n\n"
439
+ f"继续安装可能会导致问题。",
440
+ title="兼容性警告",
441
+ border_style="warning"
442
+ ))
443
+ if not Confirm.ask("是否继续安装?", default=False):
444
+ console.print("[info]已取消安装[/]")
445
+ all_success = False
446
+ continue
447
+ else:
448
+ console.print(f"[success]{message}[/]")
449
+
450
+ # 构建pip命令
451
+ cmd = ["install"]
452
+ if upgrade:
453
+ cmd.append("--upgrade")
454
+ if pre:
455
+ cmd.append("--pre")
456
+ cmd.append(current_package_name)
457
+
458
+ # 执行安装命令
459
+ success, stdout, stderr = self._run_pip_command_with_output(cmd, f"安装 {current_package_name}")
460
+
461
+ if success:
462
+ console.print(Panel(
463
+ f"[success]包 {current_package_name} 安装成功[/]\n\n"
464
+ f"[dim]{stdout}[/]",
465
+ title="安装完成",
466
+ border_style="success"
467
+ ))
468
+ else:
469
+ console.print(Panel(
470
+ f"[error]包 {current_package_name} 安装失败[/]\n\n"
471
+ f"[dim]{stderr}[/]",
472
+ title="安装失败",
473
+ border_style="error"
474
+ ))
475
+ all_success = False
476
+
477
+ return all_success
478
+
479
+ def uninstall_package(self, package_names: List[str]) -> bool:
480
+ """
481
+ 卸载指定包(支持多个包,支持别名)
482
+
483
+ :param package_names: 要卸载的包名或别名列表
484
+ :return: 卸载是否成功
485
+ """
486
+ all_success = True
487
+
488
+ packages_to_uninstall = []
489
+
490
+ # 首先处理所有包名,查找实际包名
491
+ for package_name in package_names:
492
+ # 首先尝试通过别名查找实际包名
493
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
494
+
495
+ if actual_package:
496
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
497
+ packages_to_uninstall.append(actual_package)
498
+ else:
499
+ # 如果找不到别名映射,检查是否是已安装的包
500
+ installed_package = self._find_installed_package_by_name(package_name)
501
+ if installed_package:
502
+ package_name = installed_package
503
+ console.print(f"[info]找到已安装包: [bold]{package_name}[/][/]")
504
+ packages_to_uninstall.append(package_name)
505
+ else:
506
+ console.print(f"[warning]未找到别名映射,将尝试直接卸载: [package]{package_name}[/][/]")
507
+ packages_to_uninstall.append(package_name)
508
+
509
+ # 确认卸载操作
510
+ package_list = "\n".join([f" - [package]{pkg}[/]" for pkg in packages_to_uninstall])
511
+ if not Confirm.ask(f"确认卸载以下包吗?\n{package_list}", default=False):
512
+ console.print("[info]操作已取消[/]")
513
+ return False
514
+
515
+ # 执行卸载命令
516
+ for package_name in packages_to_uninstall:
517
+ success, stdout, stderr = self._run_pip_command_with_output(
518
+ ["uninstall", "-y", package_name],
519
+ f"卸载 {package_name}"
520
+ )
521
+
522
+ if success:
523
+ console.print(Panel(
524
+ f"[success]包 {package_name} 卸载成功[/]\n\n"
525
+ f"[dim]{stdout}[/]",
526
+ title="卸载完成",
527
+ border_style="success"
528
+ ))
529
+ else:
530
+ console.print(Panel(
531
+ f"[error]包 {package_name} 卸载失败[/]\n\n"
532
+ f"[dim]{stderr}[/]",
533
+ title="卸载失败",
534
+ border_style="error"
535
+ ))
536
+ all_success = False
537
+
538
+ return all_success
539
+
540
+ def upgrade_all(self) -> bool:
541
+ """
542
+ 升级所有已安装的ErisPulse包
543
+
544
+ :return: 升级是否成功
545
+
546
+ :raises KeyboardInterrupt: 用户取消操作时抛出
547
+ """
548
+ installed = self.get_installed_packages()
549
+ all_packages = set()
550
+
551
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
552
+ for pkg_info in installed[pkg_type].values():
553
+ all_packages.add(pkg_info["package"])
554
+
555
+ if not all_packages:
556
+ console.print("[info]没有找到可升级的ErisPulse包[/]")
557
+ return False
558
+
559
+ console.print(Panel(
560
+ f"找到 [bold]{len(all_packages)}[/] 个可升级的包:\n" +
561
+ "\n".join(f" - [package]{pkg}[/]" for pkg in sorted(all_packages)),
562
+ title="升级列表"
563
+ ))
564
+
565
+ if not Confirm.ask("确认升级所有包吗?", default=False):
566
+ return False
567
+
568
+ results = {}
569
+ for pkg in sorted(all_packages):
570
+ results[pkg] = self.install_package([pkg], upgrade=True)
571
+
572
+ failed = [pkg for pkg, success in results.items() if not success]
573
+ if failed:
574
+ console.print(Panel(
575
+ "以下包升级失败:\n" + "\n".join(f" - [error]{pkg}[/]" for pkg in failed),
576
+ title="警告",
577
+ style="warning"
578
+ ))
579
+ return False
580
+
581
+ return True
582
+
583
+ def upgrade_package(self, package_names: List[str], pre: bool = False) -> bool:
584
+ """
585
+ 升级指定包(支持多个包)
586
+
587
+ :param package_names: 要升级的包名或别名列表
588
+ :param pre: 是否包含预发布版本
589
+ :return: 升级是否成功
590
+ """
591
+ all_success = True
592
+
593
+ for package_name in package_names:
594
+ # 首先尝试通过别名查找实际包名
595
+ actual_package = asyncio.run(self._find_package_by_alias(package_name))
596
+
597
+ if actual_package:
598
+ console.print(f"[info]找到别名映射: [bold]{package_name}[/] → [package]{actual_package}[/][/]")
599
+ current_package_name = actual_package
600
+ else:
601
+ current_package_name = package_name
602
+
603
+ # 检查SDK版本兼容性
604
+ package_info = asyncio.run(self._get_package_info(package_name))
605
+ if package_info and "min_sdk_version" in package_info:
606
+ is_compatible, message = self._check_sdk_compatibility(package_info["min_sdk_version"])
607
+ if not is_compatible:
608
+ console.print(Panel(
609
+ f"[warning]SDK版本兼容性警告[/]\n"
610
+ f"包 [package]{current_package_name}[/] 需要最低SDK版本 {package_info['min_sdk_version']}\n"
611
+ f"{message}\n\n"
612
+ f"继续升级可能会导致问题。",
613
+ title="兼容性警告",
614
+ border_style="warning"
615
+ ))
616
+ if not Confirm.ask("是否继续升级?", default=False):
617
+ console.print("[info]已取消升级[/]")
618
+ all_success = False
619
+ continue
620
+ else:
621
+ console.print(f"[success]{message}[/]")
622
+
623
+ # 构建pip命令
624
+ cmd = ["install", "--upgrade"]
625
+ if pre:
626
+ cmd.append("--pre")
627
+ cmd.append(current_package_name)
628
+
629
+ # 执行升级命令
630
+ success, stdout, stderr = self._run_pip_command_with_output(cmd, f"升级 {current_package_name}")
631
+
632
+ if success:
633
+ console.print(Panel(
634
+ f"[success]包 {current_package_name} 升级成功[/]\n\n"
635
+ f"[dim]{stdout}[/]",
636
+ title="升级完成",
637
+ border_style="success"
638
+ ))
639
+ else:
640
+ console.print(Panel(
641
+ f"[error]包 {current_package_name} 升级失败[/]\n\n"
642
+ f"[dim]{stderr}[/]",
643
+ title="升级失败",
644
+ border_style="error"
645
+ ))
646
+ all_success = False
647
+
648
+ return all_success
649
+
650
+ def search_package(self, query: str) -> Dict[str, List[Dict[str, str]]]:
651
+ """
652
+ 搜索包(本地和远程)
653
+
654
+ :param query: 搜索关键词
655
+ :return: 匹配的包信息
656
+ """
657
+ normalized_query = self._normalize_name(query)
658
+ results = {"installed": [], "remote": []}
659
+
660
+ # 搜索已安装的包
661
+ installed = self.get_installed_packages()
662
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
663
+ for name, info in installed[pkg_type].items():
664
+ if (normalized_query in self._normalize_name(name) or
665
+ normalized_query in self._normalize_name(info["package"]) or
666
+ normalized_query in self._normalize_name(info["summary"])):
667
+ results["installed"].append({
668
+ "type": pkg_type[:-1] if pkg_type.endswith("s") else pkg_type, # 移除复数s
669
+ "name": name,
670
+ "package": info["package"],
671
+ "version": info["version"],
672
+ "summary": info["summary"]
673
+ })
674
+
675
+ # 搜索远程包
676
+ remote = asyncio.run(self.get_remote_packages())
677
+ for pkg_type in ["modules", "adapters", "cli_extensions"]:
678
+ for name, info in remote[pkg_type].items():
679
+ if (normalized_query in self._normalize_name(name) or
680
+ normalized_query in self._normalize_name(info["package"]) or
681
+ normalized_query in self._normalize_name(info.get("description", "")) or
682
+ normalized_query in self._normalize_name(info.get("summary", ""))):
683
+ results["remote"].append({
684
+ "type": pkg_type[:-1] if pkg_type.endswith("s") else pkg_type, # 移除复数s
685
+ "name": name,
686
+ "package": info["package"],
687
+ "version": info["version"],
688
+ "summary": info.get("description", info.get("summary", ""))
689
+ })
690
+
691
+ return results
692
+
693
+ def get_installed_version(self) -> str:
694
+ """
695
+ 获取当前安装的ErisPulse版本
696
+
697
+ :return: 当前版本号
698
+ """
699
+ try:
700
+ from ErisPulse import __version__
701
+ return __version__
702
+ except ImportError:
703
+ return "unknown"
704
+
705
+ async def get_pypi_versions(self) -> List[Dict[str, Any]]:
706
+ """
707
+ 从PyPI获取ErisPulse的所有可用版本
708
+
709
+ :return: 版本信息列表
710
+ """
711
+ import aiohttp
712
+ from aiohttp import ClientError, ClientTimeout
713
+ from packaging import version as comparison
714
+
715
+ timeout = ClientTimeout(total=10)
716
+ url = "https://pypi.org/pypi/ErisPulse/json"
717
+
718
+ try:
719
+ async with aiohttp.ClientSession(timeout=timeout) as session:
720
+ async with session.get(url) as response:
721
+ if response.status == 200:
722
+ data = await response.json()
723
+ versions = []
724
+ for version_str, releases in data["releases"].items():
725
+ if releases: # 只包含有文件的版本
726
+ release_info = {
727
+ "version": version_str,
728
+ "uploaded": releases[0].get("upload_time_iso_8601", ""),
729
+ "pre_release": self._is_pre_release(version_str)
730
+ }
731
+ versions.append(release_info)
732
+
733
+ # 使用版本比较函数正确排序版本
734
+ versions.sort(key=lambda x: comparison.parse(x["version"]), reverse=True)
735
+ return versions
736
+ except (ClientError, asyncio.TimeoutError, json.JSONDecodeError, KeyError, Exception) as e:
737
+ console.print(f"[error]获取PyPI版本信息失败: {e}[/]")
738
+ return []
739
+
740
+ def _is_pre_release(self, version: str) -> bool:
741
+ """
742
+ 判断版本是否为预发布版本
743
+
744
+ :param version: 版本号
745
+ :return: 是否为预发布版本
746
+ """
747
+ import re
748
+ # 检查是否包含预发布标识符 (alpha, beta, rc, dev等)
749
+ pre_release_pattern = re.compile(r'(a|b|rc|dev|alpha|beta)\d*', re.IGNORECASE)
750
+ return bool(pre_release_pattern.search(version))
751
+
752
+ def update_self(self, target_version: str = None, force: bool = False) -> bool:
753
+ """
754
+ 更新ErisPulse SDK本身
755
+
756
+ :param target_version: 目标版本号,None表示更新到最新版本
757
+ :param force: 是否强制更新
758
+ :return: 更新是否成功
759
+ """
760
+ current_version = self.get_installed_version()
761
+
762
+ if target_version and target_version == current_version and not force:
763
+ console.print(f"[info]当前已是目标版本 [bold]{current_version}[/][/]")
764
+ return True
765
+
766
+ # 确定要安装的版本
767
+ package_spec = "ErisPulse"
768
+ if target_version:
769
+ package_spec += f"=={target_version}"
770
+
771
+ # 检查是否在Windows上且尝试更新自身
772
+ if sys.platform == "win32":
773
+ # 构建更新脚本
774
+ update_script = f"""
775
+ import time
776
+ import subprocess
777
+ import sys
778
+ import os
779
+
780
+ # 等待原进程结束
781
+ time.sleep(2)
782
+
783
+ # 执行更新命令
784
+ try:
785
+ result = subprocess.run([
786
+ sys.executable, "-m", "pip", "install", "--upgrade", "{package_spec}"
787
+ ], capture_output=True, text=True, timeout=300)
788
+
789
+ if result.returncode == 0:
790
+ print("更新成功!")
791
+ print(result.stdout)
792
+ else:
793
+ print("更新失败:")
794
+ print(result.stderr)
795
+ except Exception as e:
796
+ print(f"更新过程中出错: {{e}}")
797
+
798
+ # 清理临时脚本
799
+ try:
800
+ os.remove(__file__)
801
+ except:
802
+ pass
803
+ """
804
+ # 创建临时更新脚本
805
+ import tempfile
806
+ script_path = os.path.join(tempfile.gettempdir(), "epsdk_update.py")
807
+ with open(script_path, "w", encoding="utf-8") as f:
808
+ f.write(update_script)
809
+
810
+ # 启动更新进程并退出当前进程
811
+ console.print("[info]正在启动更新进程...[/]")
812
+ console.print("[info]请稍后重新运行CLI以使用新版本[/]")
813
+
814
+ subprocess.Popen([
815
+ sys.executable, script_path
816
+ ], creationflags=subprocess.CREATE_NEW_CONSOLE)
817
+
818
+ return True
819
+ else:
820
+ # 非Windows平台
821
+ success, stdout, stderr = self._run_pip_command_with_output(
822
+ ["install", "--upgrade", package_spec],
823
+ f"更新 ErisPulse SDK {f'到 {target_version}' if target_version else '到最新版本'}"
824
+ )
825
+
826
+ if success:
827
+ new_version = target_version or "最新版本"
828
+ console.print(Panel(
829
+ f"[success]ErisPulse SDK 更新成功[/]\n"
830
+ f" 当前版本: [bold]{current_version}[/]\n"
831
+ f" 更新版本: [bold]{new_version}[/]\n\n"
832
+ f"[dim]{stdout}[/]",
833
+ title="更新完成",
834
+ border_style="success"
835
+ ))
836
+
837
+ if not target_version:
838
+ console.print("[info]请重新启动CLI以使用新版本[/]")
839
+ else:
840
+ console.print(Panel(
841
+ f"[error]ErisPulse SDK 更新失败[/]\n\n"
842
+ f"[dim]{stderr}[/]",
843
+ title="更新失败",
844
+ border_style="error"
845
+ ))
846
+
847
+ return success