ErisPulse 2.1.12__py3-none-any.whl → 2.1.13__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/__init__.py +3 -0
- ErisPulse/__main__.py +924 -554
- {erispulse-2.1.12.dist-info → erispulse-2.1.13.dist-info}/METADATA +1 -1
- {erispulse-2.1.12.dist-info → erispulse-2.1.13.dist-info}/RECORD +7 -7
- {erispulse-2.1.12.dist-info → erispulse-2.1.13.dist-info}/WHEEL +0 -0
- {erispulse-2.1.12.dist-info → erispulse-2.1.13.dist-info}/entry_points.txt +0 -0
- {erispulse-2.1.12.dist-info → erispulse-2.1.13.dist-info}/licenses/LICENSE +0 -0
ErisPulse/__main__.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ErisPulse SDK 命令行工具
|
|
3
|
+
|
|
4
|
+
提供ErisPulse生态系统的包管理、模块控制和开发工具功能。
|
|
5
|
+
|
|
6
|
+
{!--< tips >!--}
|
|
7
|
+
1. 需要Python 3.8+环境
|
|
8
|
+
2. Windows平台需要colorama支持ANSI颜色
|
|
9
|
+
{!--< /tips >!--}
|
|
10
|
+
"""
|
|
11
|
+
|
|
1
12
|
import argparse
|
|
2
13
|
import importlib.metadata
|
|
3
14
|
import subprocess
|
|
@@ -7,7 +18,7 @@ import time
|
|
|
7
18
|
import json
|
|
8
19
|
import asyncio
|
|
9
20
|
from urllib.parse import urlparse
|
|
10
|
-
from typing import List, Dict, Tuple, Optional
|
|
21
|
+
from typing import List, Dict, Tuple, Optional, Callable, Any
|
|
11
22
|
from importlib.metadata import version, PackageNotFoundError
|
|
12
23
|
from watchdog.observers import Observer
|
|
13
24
|
from watchdog.events import FileSystemEventHandler
|
|
@@ -24,114 +35,159 @@ from rich.style import Style
|
|
|
24
35
|
from rich.theme import Theme
|
|
25
36
|
from rich.layout import Layout
|
|
26
37
|
from rich.live import Live
|
|
38
|
+
from rich.markdown import Markdown
|
|
39
|
+
from rich.highlighter import RegexHighlighter
|
|
27
40
|
|
|
28
41
|
# 确保在Windows上启用颜色
|
|
29
|
-
import sys
|
|
30
42
|
if sys.platform == "win32":
|
|
31
43
|
from colorama import init
|
|
32
44
|
init()
|
|
33
45
|
|
|
46
|
+
class CommandHighlighter(RegexHighlighter):
|
|
47
|
+
"""
|
|
48
|
+
高亮CLI命令和参数
|
|
49
|
+
|
|
50
|
+
{!--< tips >!--}
|
|
51
|
+
使用正则表达式匹配命令行参数和选项
|
|
52
|
+
{!--< /tips >!--}
|
|
53
|
+
"""
|
|
54
|
+
highlights = [
|
|
55
|
+
r"(?P<switch>\-\-?\w+)",
|
|
56
|
+
r"(?P<option>\[\w+\])",
|
|
57
|
+
r"(?P<command>\b\w+\b)",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# 主题配置
|
|
34
61
|
theme = Theme({
|
|
35
|
-
"info": "cyan",
|
|
36
|
-
"success": "green",
|
|
37
|
-
"warning": "yellow",
|
|
38
|
-
"error": "red",
|
|
39
|
-
"title": "magenta",
|
|
62
|
+
"info": "dim cyan",
|
|
63
|
+
"success": "bold green",
|
|
64
|
+
"warning": "bold yellow",
|
|
65
|
+
"error": "bold red",
|
|
66
|
+
"title": "bold magenta",
|
|
40
67
|
"default": "default",
|
|
41
68
|
"progress": "green",
|
|
42
69
|
"progress.remaining": "white",
|
|
70
|
+
"cmd": "bold blue",
|
|
71
|
+
"param": "italic cyan",
|
|
72
|
+
"switch": "bold yellow",
|
|
73
|
+
"module": "bold green",
|
|
74
|
+
"adapter": "bold yellow",
|
|
75
|
+
"cli": "bold magenta",
|
|
43
76
|
})
|
|
44
77
|
|
|
45
|
-
|
|
78
|
+
# 全局控制台实例
|
|
79
|
+
console = Console(
|
|
80
|
+
theme=theme,
|
|
81
|
+
color_system="auto",
|
|
82
|
+
force_terminal=True,
|
|
83
|
+
highlighter=CommandHighlighter()
|
|
84
|
+
)
|
|
46
85
|
|
|
47
|
-
class
|
|
86
|
+
class PackageManager:
|
|
48
87
|
"""
|
|
49
|
-
|
|
88
|
+
ErisPulse包管理器
|
|
89
|
+
|
|
90
|
+
提供包安装、卸载、升级和查询功能
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
{!--< tips >!--}
|
|
93
|
+
1. 支持本地和远程包管理
|
|
94
|
+
2. 包含1小时缓存机制
|
|
95
|
+
{!--< /tips >!--}
|
|
52
96
|
"""
|
|
53
97
|
REMOTE_SOURCES = [
|
|
54
98
|
"https://erisdev.com/packages.json",
|
|
55
99
|
"https://raw.githubusercontent.com/ErisPulse/ErisPulse-ModuleRepo/main/packages.json"
|
|
56
100
|
]
|
|
57
101
|
|
|
58
|
-
|
|
59
|
-
|
|
102
|
+
CACHE_EXPIRY = 3600 # 1小时缓存
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
"""初始化包管理器"""
|
|
106
|
+
self._cache = {}
|
|
107
|
+
self._cache_time = {}
|
|
108
|
+
|
|
109
|
+
async def _fetch_remote_packages(self, url: str) -> Optional[dict]:
|
|
60
110
|
"""
|
|
61
|
-
|
|
111
|
+
从指定URL获取远程包数据
|
|
62
112
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- adapters: 适配器字典 {适配器名: 适配器信息}
|
|
69
|
-
|
|
70
|
-
:raises ClientError: 当网络请求失败时抛出
|
|
71
|
-
:raises asyncio.TimeoutError: 当请求超时时抛出
|
|
113
|
+
:param url: 远程包数据URL
|
|
114
|
+
:return: 解析后的JSON数据,失败返回None
|
|
115
|
+
|
|
116
|
+
:raises ClientError: 网络请求失败时抛出
|
|
117
|
+
:raises JSONDecodeError: JSON解析失败时抛出
|
|
72
118
|
"""
|
|
73
119
|
import aiohttp
|
|
74
120
|
from aiohttp import ClientError, ClientTimeout
|
|
75
121
|
|
|
76
|
-
timeout = ClientTimeout(total=
|
|
77
|
-
last_error = None
|
|
78
|
-
|
|
122
|
+
timeout = ClientTimeout(total=10)
|
|
79
123
|
try:
|
|
80
124
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
81
|
-
async with session.get(
|
|
125
|
+
async with session.get(url) as response:
|
|
82
126
|
if response.status == 200:
|
|
83
127
|
data = await response.text()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
f"官方源请求失败,尝试备用源: {e}",
|
|
93
|
-
title="警告",
|
|
94
|
-
style="warning"
|
|
95
|
-
))
|
|
128
|
+
return json.loads(data)
|
|
129
|
+
except (ClientError, asyncio.TimeoutError, json.JSONDecodeError) as e:
|
|
130
|
+
console.print(f"[warning]获取远程包数据失败 ({url}): {e}[/]")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
async def get_remote_packages(self, force_refresh: bool = False) -> dict:
|
|
134
|
+
"""
|
|
135
|
+
获取远程包列表,带缓存机制
|
|
96
136
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
:param force_refresh: 是否强制刷新缓存
|
|
138
|
+
:return: 包含模块和适配器的字典
|
|
139
|
+
|
|
140
|
+
:return:
|
|
141
|
+
dict: {
|
|
142
|
+
"modules": {模块名: 模块信息},
|
|
143
|
+
"adapters": {适配器名: 适配器信息},
|
|
144
|
+
"cli_extensions": {扩展名: 扩展信息}
|
|
145
|
+
}
|
|
146
|
+
"""
|
|
147
|
+
# 检查缓存
|
|
148
|
+
cache_key = "remote_packages"
|
|
149
|
+
if not force_refresh and cache_key in self._cache:
|
|
150
|
+
if time.time() - self._cache_time[cache_key] < self.CACHE_EXPIRY:
|
|
151
|
+
return self._cache[cache_key]
|
|
152
|
+
|
|
153
|
+
last_error = None
|
|
154
|
+
result = {"modules": {}, "adapters": {}, "cli_extensions": {}}
|
|
155
|
+
|
|
156
|
+
for url in self.REMOTE_SOURCES:
|
|
157
|
+
data = await self._fetch_remote_packages(url)
|
|
158
|
+
if data:
|
|
159
|
+
result["modules"].update(data.get("modules", {}))
|
|
160
|
+
result["adapters"].update(data.get("adapters", {}))
|
|
161
|
+
result["cli_extensions"].update(data.get("cli_extensions", {}))
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
# 更新缓存
|
|
165
|
+
self._cache[cache_key] = result
|
|
166
|
+
self._cache_time[cache_key] = time.time()
|
|
167
|
+
|
|
168
|
+
return result
|
|
117
169
|
|
|
118
|
-
|
|
119
|
-
def get_installed_packages() -> Dict[str, Dict[str, str]]:
|
|
170
|
+
def get_installed_packages(self) -> Dict[str, Dict[str, Dict[str, str]]]:
|
|
120
171
|
"""
|
|
121
172
|
获取已安装的包信息
|
|
122
173
|
|
|
123
|
-
:return:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
174
|
+
:return: 已安装包字典,包含模块、适配器和CLI扩展
|
|
175
|
+
|
|
176
|
+
:return:
|
|
177
|
+
dict: {
|
|
178
|
+
"modules": {模块名: 模块信息},
|
|
179
|
+
"adapters": {适配器名: 适配器信息},
|
|
180
|
+
"cli_extensions": {扩展名: 扩展信息}
|
|
181
|
+
}
|
|
127
182
|
"""
|
|
128
183
|
packages = {
|
|
129
184
|
"modules": {},
|
|
130
|
-
"adapters": {}
|
|
185
|
+
"adapters": {},
|
|
186
|
+
"cli_extensions": {}
|
|
131
187
|
}
|
|
132
188
|
|
|
133
189
|
try:
|
|
134
|
-
#
|
|
190
|
+
# 查找模块和适配器
|
|
135
191
|
for dist in importlib.metadata.distributions():
|
|
136
192
|
if "ErisPulse-" in dist.metadata["Name"]:
|
|
137
193
|
entry_points = dist.entry_points
|
|
@@ -140,7 +196,8 @@ class PyPIManager:
|
|
|
140
196
|
packages["modules"][ep.name] = {
|
|
141
197
|
"package": dist.metadata["Name"],
|
|
142
198
|
"version": dist.version,
|
|
143
|
-
"summary": dist.metadata["Summary"]
|
|
199
|
+
"summary": dist.metadata["Summary"],
|
|
200
|
+
"enabled": self._is_module_enabled(ep.name)
|
|
144
201
|
}
|
|
145
202
|
elif ep.group == "erispulse.adapter":
|
|
146
203
|
packages["adapters"][ep.name] = {
|
|
@@ -148,576 +205,889 @@ class PyPIManager:
|
|
|
148
205
|
"version": dist.version,
|
|
149
206
|
"summary": dist.metadata["Summary"]
|
|
150
207
|
}
|
|
208
|
+
|
|
209
|
+
# 查找CLI扩展
|
|
210
|
+
entry_points = importlib.metadata.entry_points()
|
|
211
|
+
if hasattr(entry_points, 'select'):
|
|
212
|
+
cli_entries = entry_points.select(group='erispulse.cli')
|
|
213
|
+
else:
|
|
214
|
+
cli_entries = entry_points.get('erispulse.cli', [])
|
|
215
|
+
|
|
216
|
+
for entry in cli_entries:
|
|
217
|
+
dist = entry.dist
|
|
218
|
+
packages["cli_extensions"][entry.name] = {
|
|
219
|
+
"package": dist.metadata["Name"],
|
|
220
|
+
"version": dist.version,
|
|
221
|
+
"summary": dist.metadata["Summary"]
|
|
222
|
+
}
|
|
223
|
+
|
|
151
224
|
except Exception as e:
|
|
152
|
-
console.print(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
style="error"
|
|
156
|
-
))
|
|
225
|
+
console.print(f"[error]获取已安装包信息失败: {e}[/]")
|
|
226
|
+
import traceback
|
|
227
|
+
console.print(traceback.format_exc())
|
|
157
228
|
|
|
158
229
|
return packages
|
|
159
230
|
|
|
160
|
-
|
|
161
|
-
def uv_install_package(package_name: str, upgrade: bool = False) -> bool:
|
|
231
|
+
def _is_module_enabled(self, module_name: str) -> bool:
|
|
162
232
|
"""
|
|
163
|
-
|
|
233
|
+
检查模块是否启用
|
|
234
|
+
|
|
235
|
+
:param module_name: 模块名称
|
|
236
|
+
:return: 模块是否启用
|
|
164
237
|
|
|
165
|
-
:
|
|
166
|
-
:param upgrade: bool 是否升级已安装的包 (默认: False)
|
|
167
|
-
:return: bool 安装是否成功
|
|
238
|
+
:raises ImportError: 核心模块不可用时抛出
|
|
168
239
|
"""
|
|
169
240
|
try:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
cmd = ["uv", "pip", "install"]
|
|
176
|
-
if upgrade:
|
|
177
|
-
cmd.append("--upgrade")
|
|
178
|
-
cmd.append(package_name)
|
|
179
|
-
|
|
180
|
-
with console.status(f"使用uv安装 {package_name}..."):
|
|
181
|
-
result = subprocess.run(cmd, check=True)
|
|
182
|
-
return result.returncode == 0
|
|
183
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
241
|
+
from ErisPulse.Core import mods
|
|
242
|
+
return mods.get_module_status(module_name)
|
|
243
|
+
except ImportError:
|
|
244
|
+
return True
|
|
245
|
+
except Exception:
|
|
184
246
|
return False
|
|
185
247
|
|
|
186
|
-
|
|
187
|
-
def install_package(package_name: str, upgrade: bool = False) -> bool:
|
|
248
|
+
def _run_pip_command(self, args: List[str], description: str) -> bool:
|
|
188
249
|
"""
|
|
189
|
-
|
|
250
|
+
执行pip命令
|
|
190
251
|
|
|
191
|
-
:param
|
|
192
|
-
:param
|
|
193
|
-
:return:
|
|
252
|
+
:param args: pip命令参数列表
|
|
253
|
+
:param description: 进度条描述
|
|
254
|
+
:return: 命令是否成功执行
|
|
194
255
|
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
))
|
|
202
|
-
return True
|
|
256
|
+
with Progress(
|
|
257
|
+
TextColumn(f"[progress.description]{description}"),
|
|
258
|
+
BarColumn(complete_style="progress.download"),
|
|
259
|
+
transient=True
|
|
260
|
+
) as progress:
|
|
261
|
+
task = progress.add_task("", total=100)
|
|
203
262
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
try:
|
|
264
|
+
process = subprocess.Popen(
|
|
265
|
+
[sys.executable, "-m", "pip"] + args,
|
|
266
|
+
stdout=subprocess.PIPE,
|
|
267
|
+
stderr=subprocess.PIPE,
|
|
268
|
+
universal_newlines=True
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
while True:
|
|
272
|
+
output = process.stdout.readline()
|
|
273
|
+
if output == '' and process.poll() is not None:
|
|
274
|
+
break
|
|
275
|
+
if output:
|
|
276
|
+
progress.update(task, advance=1)
|
|
277
|
+
|
|
278
|
+
return process.returncode == 0
|
|
279
|
+
except subprocess.CalledProcessError as e:
|
|
280
|
+
console.print(f"[error]命令执行失败: {e}[/]")
|
|
220
281
|
return False
|
|
221
|
-
except subprocess.CalledProcessError as e:
|
|
222
|
-
console.print(Panel(
|
|
223
|
-
f"安装包 {package_name} 失败: {e}",
|
|
224
|
-
title="错误",
|
|
225
|
-
style="error"
|
|
226
|
-
))
|
|
227
|
-
return False
|
|
228
282
|
|
|
229
|
-
|
|
230
|
-
|
|
283
|
+
def install_package(self, package_name: str, upgrade: bool = False) -> bool:
|
|
284
|
+
"""
|
|
285
|
+
安装指定包
|
|
286
|
+
|
|
287
|
+
:param package_name: 要安装的包名
|
|
288
|
+
:param upgrade: 是否升级已安装的包
|
|
289
|
+
:return: 安装是否成功
|
|
290
|
+
"""
|
|
291
|
+
cmd = ["install"]
|
|
292
|
+
if upgrade:
|
|
293
|
+
cmd.append("--upgrade")
|
|
294
|
+
cmd.append(package_name)
|
|
295
|
+
|
|
296
|
+
success = self._run_pip_command(cmd, f"安装 {package_name}")
|
|
297
|
+
|
|
298
|
+
if success:
|
|
299
|
+
console.print(f"[success]包 {package_name} 安装成功[/]")
|
|
300
|
+
else:
|
|
301
|
+
console.print(f"[error]包 {package_name} 安装失败[/]")
|
|
302
|
+
|
|
303
|
+
return success
|
|
304
|
+
|
|
305
|
+
def uninstall_package(self, package_name: str) -> bool:
|
|
231
306
|
"""
|
|
232
307
|
卸载指定包
|
|
233
308
|
|
|
234
|
-
:param package_name:
|
|
235
|
-
:return:
|
|
309
|
+
:param package_name: 要卸载的包名
|
|
310
|
+
:return: 卸载是否成功
|
|
236
311
|
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
))
|
|
249
|
-
return True
|
|
250
|
-
return False
|
|
251
|
-
except subprocess.CalledProcessError as e:
|
|
252
|
-
console.print(Panel(
|
|
253
|
-
f"卸载包 {package_name} 失败: {e}",
|
|
254
|
-
title="错误",
|
|
255
|
-
style="error"
|
|
256
|
-
))
|
|
257
|
-
return False
|
|
312
|
+
success = self._run_pip_command(
|
|
313
|
+
["uninstall", "-y", package_name],
|
|
314
|
+
f"卸载 {package_name}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if success:
|
|
318
|
+
console.print(f"[success]包 {package_name} 卸载成功[/]")
|
|
319
|
+
else:
|
|
320
|
+
console.print(f"[error]包 {package_name} 卸载失败[/]")
|
|
321
|
+
|
|
322
|
+
return success
|
|
258
323
|
|
|
259
|
-
|
|
260
|
-
def upgrade_all() -> bool:
|
|
324
|
+
def upgrade_all(self) -> bool:
|
|
261
325
|
"""
|
|
262
326
|
升级所有已安装的ErisPulse包
|
|
263
327
|
|
|
264
|
-
:return:
|
|
328
|
+
:return: 升级是否成功
|
|
329
|
+
|
|
330
|
+
:raises KeyboardInterrupt: 用户取消操作时抛出
|
|
265
331
|
"""
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
for
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"没有找到可升级的ErisPulse包",
|
|
277
|
-
title="提示",
|
|
278
|
-
style="info"
|
|
279
|
-
))
|
|
280
|
-
return False
|
|
281
|
-
|
|
282
|
-
console.print(Panel(
|
|
283
|
-
f"找到 {len(all_packages)} 个可升级的包:\n" +
|
|
284
|
-
"\n".join(f" - {pkg}" for pkg in all_packages),
|
|
285
|
-
title="升级列表",
|
|
286
|
-
style="info"
|
|
287
|
-
))
|
|
332
|
+
installed = self.get_installed_packages()
|
|
333
|
+
all_packages = set()
|
|
334
|
+
|
|
335
|
+
for pkg_type in ["modules", "adapters", "cli_extensions"]:
|
|
336
|
+
for pkg_info in installed[pkg_type].values():
|
|
337
|
+
all_packages.add(pkg_info["package"])
|
|
338
|
+
|
|
339
|
+
if not all_packages:
|
|
340
|
+
console.print("[info]没有找到可升级的ErisPulse包[/]")
|
|
341
|
+
return False
|
|
288
342
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
343
|
+
console.print(Panel(
|
|
344
|
+
f"找到 [bold]{len(all_packages)}[/] 个可升级的包:\n" +
|
|
345
|
+
"\n".join(f" - [package]{pkg}[/]" for pkg in sorted(all_packages)),
|
|
346
|
+
title="升级列表"
|
|
347
|
+
))
|
|
348
|
+
|
|
349
|
+
if not Confirm.ask("确认升级所有包吗?", default=False):
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
results = {}
|
|
353
|
+
for pkg in sorted(all_packages):
|
|
354
|
+
results[pkg] = self.install_package(pkg, upgrade=True)
|
|
355
|
+
|
|
356
|
+
failed = [pkg for pkg, success in results.items() if not success]
|
|
357
|
+
if failed:
|
|
297
358
|
console.print(Panel(
|
|
298
|
-
f"
|
|
299
|
-
title="
|
|
300
|
-
style="
|
|
359
|
+
f"以下包升级失败:\n" + "\n".join(f" - [error]{pkg}[/]" for pkg in failed),
|
|
360
|
+
title="警告",
|
|
361
|
+
style="warning"
|
|
301
362
|
))
|
|
302
363
|
return False
|
|
364
|
+
|
|
365
|
+
return True
|
|
303
366
|
|
|
304
367
|
class ReloadHandler(FileSystemEventHandler):
|
|
305
368
|
"""
|
|
306
|
-
|
|
369
|
+
文件系统事件处理器
|
|
370
|
+
|
|
371
|
+
实现热重载功能,监控文件变化并重启进程
|
|
307
372
|
|
|
308
|
-
|
|
373
|
+
{!--< tips >!--}
|
|
374
|
+
1. 支持.py文件修改重载
|
|
375
|
+
2. 支持配置文件修改重载
|
|
376
|
+
{!--< /tips >!--}
|
|
309
377
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
378
|
+
|
|
379
|
+
def __init__(self, script_path: str, reload_mode: bool = False):
|
|
380
|
+
"""
|
|
381
|
+
初始化处理器
|
|
382
|
+
|
|
383
|
+
:param script_path: 要监控的脚本路径
|
|
384
|
+
:param reload_mode: 是否启用重载模式
|
|
385
|
+
"""
|
|
386
|
+
super().__init__()
|
|
387
|
+
self.script_path = os.path.abspath(script_path)
|
|
313
388
|
self.process = None
|
|
314
389
|
self.last_reload = time.time()
|
|
315
390
|
self.reload_mode = reload_mode
|
|
316
391
|
self.start_process()
|
|
392
|
+
self.watched_files = set()
|
|
317
393
|
|
|
318
394
|
def start_process(self):
|
|
395
|
+
"""启动监控进程"""
|
|
396
|
+
if self.process:
|
|
397
|
+
self._terminate_process()
|
|
398
|
+
|
|
399
|
+
console.print(f"[bold]启动进程: [path]{self.script_path}[/][/]")
|
|
400
|
+
try:
|
|
401
|
+
self.process = subprocess.Popen(
|
|
402
|
+
[sys.executable, self.script_path],
|
|
403
|
+
stdin=sys.stdin,
|
|
404
|
+
stdout=sys.stdout,
|
|
405
|
+
stderr=sys.stderr
|
|
406
|
+
)
|
|
407
|
+
self.last_reload = time.time()
|
|
408
|
+
except Exception as e:
|
|
409
|
+
console.print(f"[error]启动进程失败: {e}[/]")
|
|
410
|
+
raise
|
|
411
|
+
|
|
412
|
+
def _terminate_process(self):
|
|
319
413
|
"""
|
|
320
|
-
|
|
414
|
+
终止当前进程
|
|
415
|
+
|
|
416
|
+
:raises subprocess.TimeoutExpired: 进程终止超时时抛出
|
|
321
417
|
"""
|
|
322
|
-
|
|
418
|
+
try:
|
|
323
419
|
self.process.terminate()
|
|
420
|
+
# 等待最多2秒让进程正常退出
|
|
421
|
+
self.process.wait(timeout=2)
|
|
422
|
+
except subprocess.TimeoutExpired:
|
|
423
|
+
console.print("[warning]进程未正常退出,强制终止...[/]")
|
|
424
|
+
self.process.kill()
|
|
324
425
|
self.process.wait()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
self.process = subprocess.Popen([sys.executable, self.script_path])
|
|
328
|
-
self.last_reload = time.time()
|
|
426
|
+
except Exception as e:
|
|
427
|
+
console.print(f"[error]终止进程时出错: {e}[/]")
|
|
329
428
|
|
|
330
429
|
def on_modified(self, event):
|
|
331
430
|
"""
|
|
332
431
|
文件修改事件处理
|
|
333
432
|
|
|
334
|
-
:param event:
|
|
433
|
+
:param event: 文件系统事件
|
|
335
434
|
"""
|
|
336
435
|
now = time.time()
|
|
337
|
-
if now - self.last_reload < 1.0:
|
|
436
|
+
if now - self.last_reload < 1.0: # 防抖
|
|
338
437
|
return
|
|
339
438
|
|
|
340
439
|
if event.src_path.endswith(".py") and self.reload_mode:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
console.print(f"\n[cyan][热重载] 配置发生变动: {event.src_path}[/]")
|
|
345
|
-
self.start_process()
|
|
346
|
-
|
|
347
|
-
def start_reloader(script_path, reload_mode=False):
|
|
348
|
-
"""
|
|
349
|
-
启动热重载监控
|
|
350
|
-
|
|
351
|
-
:param script_path: str 要监控的脚本路径
|
|
352
|
-
:param reload_mode: bool 是否启用完整重载模式 (默认: False)
|
|
353
|
-
"""
|
|
354
|
-
if not os.path.exists(script_path):
|
|
355
|
-
console.print(Panel(
|
|
356
|
-
f"找不到指定文件: {script_path}",
|
|
357
|
-
title="错误",
|
|
358
|
-
style="error"
|
|
359
|
-
))
|
|
360
|
-
return
|
|
361
|
-
watch_dirs = [
|
|
362
|
-
os.path.dirname(os.path.abspath(script_path)),
|
|
363
|
-
]
|
|
364
|
-
|
|
365
|
-
handler = ReloadHandler(script_path, reload_mode)
|
|
366
|
-
observer = Observer()
|
|
440
|
+
self._handle_reload(event, "文件变动")
|
|
441
|
+
elif event.src_path.endswith(("config.toml", ".env")):
|
|
442
|
+
self._handle_reload(event, "配置变动")
|
|
367
443
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
console.print("\n[bold green][热重载] 已启动[/]")
|
|
379
|
-
mode_desc = "开发重载模式" if reload_mode else "配置监控模式"
|
|
380
|
-
console.print(f"[dim]模式: {mode_desc}\n监控目录: {', '.join(watch_dirs)}[/]\n")
|
|
381
|
-
try:
|
|
382
|
-
first_interrupt = True
|
|
383
|
-
while True:
|
|
384
|
-
time.sleep(1)
|
|
385
|
-
except KeyboardInterrupt:
|
|
386
|
-
if first_interrupt:
|
|
387
|
-
first_interrupt = False
|
|
388
|
-
console.print("\n[bold green]正在安全关闭热重载...[/]")
|
|
389
|
-
console.print("[bold red]再次按下Ctrl+C以强制关闭[/]")
|
|
390
|
-
try:
|
|
391
|
-
observer.stop()
|
|
392
|
-
if handler.process:
|
|
393
|
-
handler.process.terminate()
|
|
394
|
-
observer.join()
|
|
395
|
-
except KeyboardInterrupt:
|
|
396
|
-
console.print("[bold red]强制关闭热重载[/]")
|
|
397
|
-
raise
|
|
398
|
-
else:
|
|
399
|
-
console.print(Panel("[bold red]强制关闭热重载[/]"))
|
|
400
|
-
raise
|
|
444
|
+
def _handle_reload(self, event, reason: str):
|
|
445
|
+
"""
|
|
446
|
+
处理重载逻辑
|
|
447
|
+
|
|
448
|
+
:param event: 文件系统事件
|
|
449
|
+
:param reason: 重载原因描述
|
|
450
|
+
"""
|
|
451
|
+
console.print(f"\n[reload]{reason}: [path]{event.src_path}[/][/]")
|
|
452
|
+
self._terminate_process()
|
|
453
|
+
self.start_process()
|
|
401
454
|
|
|
402
|
-
|
|
455
|
+
class CLI:
|
|
403
456
|
"""
|
|
404
|
-
|
|
457
|
+
ErisPulse命令行接口
|
|
405
458
|
|
|
406
|
-
|
|
407
|
-
"""
|
|
408
|
-
try:
|
|
409
|
-
return version("ErisPulse")
|
|
410
|
-
except PackageNotFoundError:
|
|
411
|
-
return "unknown version"
|
|
412
|
-
|
|
413
|
-
def main():
|
|
414
|
-
"""
|
|
415
|
-
CLI主入口
|
|
459
|
+
提供完整的命令行交互功能
|
|
416
460
|
|
|
417
|
-
|
|
461
|
+
{!--< tips >!--}
|
|
462
|
+
1. 支持动态加载第三方命令
|
|
463
|
+
2. 支持模块化子命令系统
|
|
464
|
+
{!--< /tips >!--}
|
|
418
465
|
"""
|
|
419
466
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
467
|
+
def __init__(self):
|
|
468
|
+
"""初始化CLI"""
|
|
469
|
+
self.parser = self._create_parser()
|
|
470
|
+
self.package_manager = PackageManager()
|
|
471
|
+
self.observer = None
|
|
472
|
+
self.handler = None
|
|
473
|
+
|
|
474
|
+
def _create_parser(self) -> argparse.ArgumentParser:
|
|
475
|
+
"""
|
|
476
|
+
创建命令行参数解析器
|
|
477
|
+
|
|
478
|
+
:return: 配置好的ArgumentParser实例
|
|
479
|
+
"""
|
|
480
|
+
parser = argparse.ArgumentParser(
|
|
481
|
+
prog="epsdk",
|
|
482
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
483
|
+
description="ErisPulse SDK 命令行工具\n\n一个功能强大的模块化系统管理工具,用于管理ErisPulse生态系统中的模块、适配器和扩展。",
|
|
484
|
+
)
|
|
485
|
+
parser._positionals.title = "命令"
|
|
486
|
+
parser._optionals.title = "选项"
|
|
487
|
+
|
|
488
|
+
# 全局选项
|
|
489
|
+
parser.add_argument(
|
|
490
|
+
"--version", "-V",
|
|
491
|
+
action="store_true",
|
|
492
|
+
help="显示版本信息"
|
|
493
|
+
)
|
|
494
|
+
parser.add_argument(
|
|
495
|
+
"--verbose", "-v",
|
|
496
|
+
action="count",
|
|
497
|
+
default=0,
|
|
498
|
+
help="增加输出详细程度 (-v, -vv, -vvv)"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# 子命令
|
|
502
|
+
subparsers = parser.add_subparsers(
|
|
503
|
+
dest='command',
|
|
504
|
+
metavar="<命令>",
|
|
505
|
+
help="要执行的操作"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# 安装命令
|
|
509
|
+
install_parser = subparsers.add_parser(
|
|
510
|
+
'install',
|
|
511
|
+
help='安装模块/适配器包'
|
|
512
|
+
)
|
|
513
|
+
install_parser.add_argument(
|
|
514
|
+
'package',
|
|
515
|
+
help='要安装的包名或模块/适配器简称'
|
|
516
|
+
)
|
|
517
|
+
install_parser.add_argument(
|
|
518
|
+
'--upgrade', '-U',
|
|
519
|
+
action='store_true',
|
|
520
|
+
help='升级已安装的包'
|
|
521
|
+
)
|
|
522
|
+
install_parser.add_argument(
|
|
523
|
+
'--pre',
|
|
524
|
+
action='store_true',
|
|
525
|
+
help='包含预发布版本'
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# 卸载命令
|
|
529
|
+
uninstall_parser = subparsers.add_parser(
|
|
530
|
+
'uninstall',
|
|
531
|
+
help='卸载模块/适配器包'
|
|
532
|
+
)
|
|
533
|
+
uninstall_parser.add_argument(
|
|
534
|
+
'package',
|
|
535
|
+
help='要卸载的包名'
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# 模块管理命令
|
|
539
|
+
module_parser = subparsers.add_parser(
|
|
540
|
+
'module',
|
|
541
|
+
help='模块管理'
|
|
542
|
+
)
|
|
543
|
+
module_subparsers = module_parser.add_subparsers(
|
|
544
|
+
dest='module_command',
|
|
545
|
+
metavar="<子命令>"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# 启用模块
|
|
549
|
+
enable_parser = module_subparsers.add_parser(
|
|
550
|
+
'enable',
|
|
551
|
+
help='启用模块'
|
|
552
|
+
)
|
|
553
|
+
enable_parser.add_argument(
|
|
554
|
+
'module',
|
|
555
|
+
help='要启用的模块名'
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# 禁用模块
|
|
559
|
+
disable_parser = module_subparsers.add_parser(
|
|
560
|
+
'disable',
|
|
561
|
+
help='禁用模块'
|
|
562
|
+
)
|
|
563
|
+
disable_parser.add_argument(
|
|
564
|
+
'module',
|
|
565
|
+
help='要禁用的模块名'
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# 列表命令
|
|
569
|
+
list_parser = subparsers.add_parser(
|
|
570
|
+
'list',
|
|
571
|
+
help='列出已安装的组件'
|
|
572
|
+
)
|
|
573
|
+
list_parser.add_argument(
|
|
574
|
+
'--type', '-t',
|
|
575
|
+
choices=['modules', 'adapters', 'cli', 'all'],
|
|
576
|
+
default='all',
|
|
577
|
+
help='列出类型 (默认: all)'
|
|
578
|
+
)
|
|
579
|
+
list_parser.add_argument(
|
|
580
|
+
'--outdated', '-o',
|
|
581
|
+
action='store_true',
|
|
582
|
+
help='仅显示可升级的包'
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# 远程列表命令
|
|
586
|
+
list_remote_parser = subparsers.add_parser(
|
|
587
|
+
'list-remote',
|
|
588
|
+
help='列出远程可用的组件'
|
|
589
|
+
)
|
|
590
|
+
list_remote_parser.add_argument(
|
|
591
|
+
'--type', '-t',
|
|
592
|
+
choices=['modules', 'adapters', 'cli', 'all'],
|
|
593
|
+
default='all',
|
|
594
|
+
help='列出类型 (默认: all)'
|
|
595
|
+
)
|
|
596
|
+
list_remote_parser.add_argument(
|
|
597
|
+
'--refresh', '-r',
|
|
598
|
+
action='store_true',
|
|
599
|
+
help='强制刷新远程包列表'
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# 升级命令
|
|
603
|
+
upgrade_parser = subparsers.add_parser(
|
|
604
|
+
'upgrade',
|
|
605
|
+
help='升级组件'
|
|
606
|
+
)
|
|
607
|
+
upgrade_parser.add_argument(
|
|
608
|
+
'package',
|
|
609
|
+
nargs='?',
|
|
610
|
+
help='要升级的包名 (可选,不指定则升级所有)'
|
|
611
|
+
)
|
|
612
|
+
upgrade_parser.add_argument(
|
|
613
|
+
'--force', '-f',
|
|
614
|
+
action='store_true',
|
|
615
|
+
help='跳过确认直接升级'
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# 运行命令
|
|
619
|
+
run_parser = subparsers.add_parser(
|
|
620
|
+
'run',
|
|
621
|
+
help='运行主程序'
|
|
622
|
+
)
|
|
623
|
+
run_parser.add_argument(
|
|
624
|
+
'script',
|
|
625
|
+
nargs='?',
|
|
626
|
+
help='要运行的主程序路径 (默认: main.py)'
|
|
627
|
+
)
|
|
628
|
+
run_parser.add_argument(
|
|
629
|
+
'--reload',
|
|
630
|
+
action='store_true',
|
|
631
|
+
help='启用热重载模式'
|
|
632
|
+
)
|
|
633
|
+
run_parser.add_argument(
|
|
634
|
+
'--no-reload',
|
|
635
|
+
action='store_true',
|
|
636
|
+
help='禁用热重载模式'
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# 初始化命令
|
|
640
|
+
init_parser = subparsers.add_parser(
|
|
641
|
+
'init',
|
|
642
|
+
help='初始化ErisPulse项目'
|
|
643
|
+
)
|
|
644
|
+
init_parser.add_argument(
|
|
645
|
+
'--force', '-f',
|
|
646
|
+
action='store_true',
|
|
647
|
+
help='强制覆盖现有配置'
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# 加载第三方命令
|
|
651
|
+
self._load_external_commands(subparsers)
|
|
652
|
+
|
|
653
|
+
return parser
|
|
429
654
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
# 在main()函数中修改第三方命令加载部分
|
|
436
|
-
try:
|
|
437
|
-
entry_points = importlib.metadata.entry_points()
|
|
438
|
-
if hasattr(entry_points, 'select'):
|
|
439
|
-
cli_entries = entry_points.select(group='erispulse.cli')
|
|
440
|
-
else:
|
|
441
|
-
cli_entries = entry_points.get('erispulse.cli', [])
|
|
655
|
+
def _get_external_commands(self) -> List[str]:
|
|
656
|
+
"""
|
|
657
|
+
获取所有已注册的第三方命令名称
|
|
442
658
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
console.print(traceback.format_exc())
|
|
455
|
-
except Exception as e:
|
|
456
|
-
console.print(f"[yellow]加载第三方CLI命令失败: {e}[/]", style="warning")
|
|
659
|
+
:return: 第三方命令名称列表
|
|
660
|
+
"""
|
|
661
|
+
try:
|
|
662
|
+
entry_points = importlib.metadata.entry_points()
|
|
663
|
+
if hasattr(entry_points, 'select'):
|
|
664
|
+
cli_entries = entry_points.select(group='erispulse.cli')
|
|
665
|
+
else:
|
|
666
|
+
cli_entries = entry_points.get('erispulse.cli', [])
|
|
667
|
+
return [entry.name for entry in cli_entries]
|
|
668
|
+
except Exception:
|
|
669
|
+
return []
|
|
457
670
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
671
|
+
def _load_external_commands(self, subparsers):
|
|
672
|
+
"""
|
|
673
|
+
加载第三方CLI命令
|
|
674
|
+
|
|
675
|
+
:param subparsers: 子命令解析器
|
|
676
|
+
|
|
677
|
+
:raises ImportError: 加载命令失败时抛出
|
|
678
|
+
"""
|
|
679
|
+
try:
|
|
680
|
+
entry_points = importlib.metadata.entry_points()
|
|
681
|
+
if hasattr(entry_points, 'select'):
|
|
682
|
+
cli_entries = entry_points.select(group='erispulse.cli')
|
|
683
|
+
else:
|
|
684
|
+
cli_entries = entry_points.get('erispulse.cli', [])
|
|
685
|
+
|
|
686
|
+
for entry in cli_entries:
|
|
687
|
+
try:
|
|
688
|
+
cli_func = entry.load()
|
|
689
|
+
if callable(cli_func):
|
|
690
|
+
cli_func(subparsers, console)
|
|
691
|
+
else:
|
|
692
|
+
console.print(f"[warning]模块 {entry.name} 的入口点不是可调用对象[/]")
|
|
693
|
+
except Exception as e:
|
|
694
|
+
console.print(f"[error]加载第三方命令 {entry.name} 失败: {e}[/]")
|
|
695
|
+
except Exception as e:
|
|
696
|
+
console.print(f"[warning]加载第三方CLI命令失败: {e}[/]")
|
|
462
697
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
698
|
+
def _print_version(self):
|
|
699
|
+
"""打印版本信息"""
|
|
700
|
+
from ErisPulse import __version__
|
|
701
|
+
console.print(Panel(
|
|
702
|
+
f"[title]ErisPulse SDK[/] 版本: [bold]{__version__}[/]",
|
|
703
|
+
subtitle=f"Python {sys.version.split()[0]}",
|
|
704
|
+
style="title"
|
|
705
|
+
))
|
|
466
706
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
707
|
+
def _print_installed_packages(self, pkg_type: str, outdated_only: bool = False):
|
|
708
|
+
"""
|
|
709
|
+
打印已安装包信息
|
|
710
|
+
|
|
711
|
+
:param pkg_type: 包类型 (modules/adapters/cli/all)
|
|
712
|
+
:param outdated_only: 是否只显示可升级的包
|
|
713
|
+
"""
|
|
714
|
+
installed = self.package_manager.get_installed_packages()
|
|
715
|
+
|
|
716
|
+
if pkg_type == "modules" and installed["modules"]:
|
|
717
|
+
table = Table(
|
|
718
|
+
title="已安装模块",
|
|
719
|
+
box=SIMPLE,
|
|
720
|
+
header_style="module"
|
|
721
|
+
)
|
|
722
|
+
table.add_column("模块名", style="module")
|
|
723
|
+
table.add_column("包名")
|
|
724
|
+
table.add_column("版本")
|
|
725
|
+
table.add_column("状态")
|
|
726
|
+
table.add_column("描述")
|
|
727
|
+
|
|
728
|
+
for name, info in installed["modules"].items():
|
|
729
|
+
if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
status = "[green]已启用[/]" if info.get("enabled", True) else "[yellow]已禁用[/]"
|
|
733
|
+
table.add_row(
|
|
734
|
+
name,
|
|
735
|
+
info["package"],
|
|
736
|
+
info["version"],
|
|
737
|
+
status,
|
|
738
|
+
info["summary"]
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
console.print(table)
|
|
742
|
+
|
|
743
|
+
if pkg_type == "adapters" and installed["adapters"]:
|
|
744
|
+
table = Table(
|
|
745
|
+
title="已安装适配器",
|
|
746
|
+
box=SIMPLE,
|
|
747
|
+
header_style="adapter"
|
|
748
|
+
)
|
|
749
|
+
table.add_column("适配器名", style="adapter")
|
|
750
|
+
table.add_column("包名")
|
|
751
|
+
table.add_column("版本")
|
|
752
|
+
table.add_column("描述")
|
|
753
|
+
|
|
754
|
+
for name, info in installed["adapters"].items():
|
|
755
|
+
if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
|
|
756
|
+
continue
|
|
757
|
+
|
|
758
|
+
table.add_row(
|
|
759
|
+
name,
|
|
760
|
+
info["package"],
|
|
761
|
+
info["version"],
|
|
762
|
+
info["summary"]
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
console.print(table)
|
|
766
|
+
|
|
767
|
+
if pkg_type == "cli" and installed["cli_extensions"]:
|
|
768
|
+
table = Table(
|
|
769
|
+
title="已安装CLI扩展",
|
|
770
|
+
box=SIMPLE,
|
|
771
|
+
header_style="cli"
|
|
772
|
+
)
|
|
773
|
+
table.add_column("命令名", style="cli")
|
|
774
|
+
table.add_column("包名")
|
|
775
|
+
table.add_column("版本")
|
|
776
|
+
table.add_column("描述")
|
|
777
|
+
|
|
778
|
+
for name, info in installed["cli_extensions"].items():
|
|
779
|
+
if outdated_only and not self._is_package_outdated(info["package"], info["version"]):
|
|
780
|
+
continue
|
|
781
|
+
|
|
782
|
+
table.add_row(
|
|
783
|
+
name,
|
|
784
|
+
info["package"],
|
|
785
|
+
info["version"],
|
|
786
|
+
info["summary"]
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
console.print(table)
|
|
479
790
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
791
|
+
def _print_remote_packages(self, pkg_type: str):
|
|
792
|
+
"""
|
|
793
|
+
打印远程包信息
|
|
794
|
+
|
|
795
|
+
:param pkg_type: 包类型 (modules/adapters/cli/all)
|
|
796
|
+
"""
|
|
797
|
+
remote_packages = asyncio.run(self.package_manager.get_remote_packages())
|
|
798
|
+
|
|
799
|
+
if pkg_type == "modules" and remote_packages["modules"]:
|
|
800
|
+
table = Table(
|
|
801
|
+
title="远程模块",
|
|
802
|
+
box=SIMPLE,
|
|
803
|
+
header_style="module"
|
|
804
|
+
)
|
|
805
|
+
table.add_column("模块名", style="module")
|
|
806
|
+
table.add_column("包名")
|
|
807
|
+
table.add_column("最新版本")
|
|
808
|
+
table.add_column("描述")
|
|
809
|
+
|
|
810
|
+
for name, info in remote_packages["modules"].items():
|
|
811
|
+
table.add_row(
|
|
812
|
+
name,
|
|
813
|
+
info["package"],
|
|
814
|
+
info["version"],
|
|
815
|
+
info["description"]
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
console.print(table)
|
|
819
|
+
|
|
820
|
+
if pkg_type == "adapters" and remote_packages["adapters"]:
|
|
821
|
+
table = Table(
|
|
822
|
+
title="远程适配器",
|
|
823
|
+
box=SIMPLE,
|
|
824
|
+
header_style="adapter"
|
|
825
|
+
)
|
|
826
|
+
table.add_column("适配器名", style="adapter")
|
|
827
|
+
table.add_column("包名")
|
|
828
|
+
table.add_column("最新版本")
|
|
829
|
+
table.add_column("描述")
|
|
830
|
+
|
|
831
|
+
for name, info in remote_packages["adapters"].items():
|
|
832
|
+
table.add_row(
|
|
833
|
+
name,
|
|
834
|
+
info["package"],
|
|
835
|
+
info["version"],
|
|
836
|
+
info["description"]
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
console.print(table)
|
|
840
|
+
|
|
841
|
+
if pkg_type == "cli" and remote_packages.get("cli_extensions"):
|
|
842
|
+
table = Table(
|
|
843
|
+
title="远程CLI扩展",
|
|
844
|
+
box=SIMPLE,
|
|
845
|
+
header_style="cli"
|
|
846
|
+
)
|
|
847
|
+
table.add_column("命令名", style="cli")
|
|
848
|
+
table.add_column("包名")
|
|
849
|
+
table.add_column("最新版本")
|
|
850
|
+
table.add_column("描述")
|
|
851
|
+
|
|
852
|
+
for name, info in remote_packages["cli_extensions"].items():
|
|
853
|
+
table.add_row(
|
|
854
|
+
name,
|
|
855
|
+
info["package"],
|
|
856
|
+
info["version"],
|
|
857
|
+
info["description"]
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
console.print(table)
|
|
487
861
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
862
|
+
def _is_package_outdated(self, package_name: str, current_version: str) -> bool:
|
|
863
|
+
"""
|
|
864
|
+
检查包是否过时
|
|
865
|
+
|
|
866
|
+
:param package_name: 包名
|
|
867
|
+
:param current_version: 当前版本
|
|
868
|
+
:return: 是否有新版本可用
|
|
869
|
+
"""
|
|
870
|
+
remote_packages = asyncio.run(self.package_manager.get_remote_packages())
|
|
871
|
+
|
|
872
|
+
# 检查模块
|
|
873
|
+
for module_info in remote_packages["modules"].values():
|
|
874
|
+
if module_info["package"] == package_name:
|
|
875
|
+
return module_info["version"] != current_version
|
|
876
|
+
|
|
877
|
+
# 检查适配器
|
|
878
|
+
for adapter_info in remote_packages["adapters"].values():
|
|
879
|
+
if adapter_info["package"] == package_name:
|
|
880
|
+
return adapter_info["version"] != current_version
|
|
881
|
+
|
|
882
|
+
# 检查CLI扩展
|
|
883
|
+
for cli_info in remote_packages.get("cli_extensions", {}).values():
|
|
884
|
+
if cli_info["package"] == package_name:
|
|
885
|
+
return cli_info["version"] != current_version
|
|
886
|
+
|
|
887
|
+
return False
|
|
492
888
|
|
|
493
|
-
|
|
889
|
+
def _resolve_package_name(self, short_name: str) -> Optional[str]:
|
|
890
|
+
"""
|
|
891
|
+
解析简称到完整包名
|
|
892
|
+
|
|
893
|
+
:param short_name: 模块/适配器简称
|
|
894
|
+
:return: 完整包名,未找到返回None
|
|
895
|
+
"""
|
|
896
|
+
remote_packages = asyncio.run(self.package_manager.get_remote_packages())
|
|
897
|
+
|
|
898
|
+
# 检查模块
|
|
899
|
+
if short_name in remote_packages["modules"]:
|
|
900
|
+
return remote_packages["modules"][short_name]["package"]
|
|
901
|
+
|
|
902
|
+
# 检查适配器
|
|
903
|
+
if short_name in remote_packages["adapters"]:
|
|
904
|
+
return remote_packages["adapters"][short_name]["package"]
|
|
905
|
+
|
|
906
|
+
return None
|
|
494
907
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
908
|
+
def _setup_watchdog(self, script_path: str, reload_mode: bool):
|
|
909
|
+
"""
|
|
910
|
+
设置文件监控
|
|
911
|
+
|
|
912
|
+
:param script_path: 要监控的脚本路径
|
|
913
|
+
:param reload_mode: 是否启用重载模式
|
|
914
|
+
"""
|
|
915
|
+
watch_dirs = [
|
|
916
|
+
os.path.dirname(os.path.abspath(script_path)),
|
|
917
|
+
]
|
|
918
|
+
|
|
919
|
+
# 添加配置目录
|
|
920
|
+
config_dir = os.path.abspath(os.getcwd())
|
|
921
|
+
if config_dir not in watch_dirs:
|
|
922
|
+
watch_dirs.append(config_dir)
|
|
923
|
+
|
|
924
|
+
self.handler = ReloadHandler(script_path, reload_mode)
|
|
925
|
+
self.observer = Observer()
|
|
926
|
+
|
|
927
|
+
for d in watch_dirs:
|
|
928
|
+
if os.path.exists(d):
|
|
929
|
+
self.observer.schedule(
|
|
930
|
+
self.handler,
|
|
931
|
+
d,
|
|
932
|
+
recursive=reload_mode
|
|
933
|
+
)
|
|
934
|
+
console.print(f"[dim]监控目录: [path]{d}[/][/]")
|
|
935
|
+
|
|
936
|
+
self.observer.start()
|
|
498
937
|
|
|
499
|
-
|
|
500
|
-
|
|
938
|
+
mode_desc = "[bold]开发重载模式[/]" if reload_mode else "[bold]配置监控模式[/]"
|
|
939
|
+
console.print(Panel(
|
|
940
|
+
f"{mode_desc}\n监控目录: [path]{', '.join(watch_dirs)}[/]",
|
|
941
|
+
title="热重载已启动",
|
|
942
|
+
border_style="info"
|
|
943
|
+
))
|
|
501
944
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
title="信息",
|
|
523
|
-
style="info"
|
|
524
|
-
))
|
|
525
|
-
PyPIManager.install_package(full_package_name, args.upgrade)
|
|
526
|
-
else:
|
|
527
|
-
# 否则按原样安装
|
|
528
|
-
PyPIManager.install_package(args.package, args.upgrade)
|
|
945
|
+
def _cleanup(self):
|
|
946
|
+
"""清理资源"""
|
|
947
|
+
if self.observer:
|
|
948
|
+
self.observer.stop()
|
|
949
|
+
if self.handler and self.handler.process:
|
|
950
|
+
self.handler._terminate_process()
|
|
951
|
+
self.observer.join()
|
|
952
|
+
|
|
953
|
+
def run(self):
|
|
954
|
+
"""
|
|
955
|
+
运行CLI
|
|
956
|
+
|
|
957
|
+
:raises KeyboardInterrupt: 用户中断时抛出
|
|
958
|
+
:raises Exception: 命令执行失败时抛出
|
|
959
|
+
"""
|
|
960
|
+
args = self.parser.parse_args()
|
|
961
|
+
|
|
962
|
+
if args.version:
|
|
963
|
+
self._print_version()
|
|
964
|
+
return
|
|
529
965
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
elif args.command == "enable":
|
|
534
|
-
from ErisPulse.Core import mods
|
|
535
|
-
installed = PyPIManager.get_installed_packages()
|
|
536
|
-
if args.module not in installed["modules"]:
|
|
537
|
-
console.print(Panel(
|
|
538
|
-
f"模块 [red]{args.module}[/] 不存在或未安装",
|
|
539
|
-
title="错误",
|
|
540
|
-
style="error"
|
|
541
|
-
))
|
|
542
|
-
else:
|
|
543
|
-
mods.set_module_status(args.module, True)
|
|
544
|
-
console.print(Panel(
|
|
545
|
-
f"模块 [green]{args.module}[/] 已启用",
|
|
546
|
-
title="成功",
|
|
547
|
-
style="success"
|
|
548
|
-
))
|
|
549
|
-
|
|
550
|
-
elif args.command == "disable":
|
|
551
|
-
from ErisPulse.Core import mods
|
|
552
|
-
installed = PyPIManager.get_installed_packages()
|
|
553
|
-
if args.module not in installed["modules"]:
|
|
554
|
-
console.print(Panel(
|
|
555
|
-
f"模块 [red]{args.module}[/] 不存在或未安装",
|
|
556
|
-
title="错误",
|
|
557
|
-
style="error"
|
|
558
|
-
))
|
|
559
|
-
else:
|
|
560
|
-
mods.set_module_status(args.module, False)
|
|
561
|
-
console.print(Panel(
|
|
562
|
-
f"模块 [yellow]{args.module}[/] 已禁用",
|
|
563
|
-
title="成功",
|
|
564
|
-
style="warning"
|
|
565
|
-
))
|
|
566
|
-
|
|
567
|
-
elif args.command == "list":
|
|
568
|
-
installed = PyPIManager.get_installed_packages()
|
|
966
|
+
if not args.command:
|
|
967
|
+
self.parser.print_help()
|
|
968
|
+
return
|
|
569
969
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
970
|
+
try:
|
|
971
|
+
if args.command == "install":
|
|
972
|
+
full_package = self._resolve_package_name(args.package)
|
|
973
|
+
if full_package:
|
|
974
|
+
console.print(f"[info]找到远程包: [bold]{args.package}[/] → [package]{full_package}[/][/]")
|
|
975
|
+
self.package_manager.install_package(full_package, args.upgrade)
|
|
576
976
|
else:
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
for entry in cli_entries:
|
|
580
|
-
dist = entry.dist
|
|
581
|
-
cli_extensions[entry.name] = {
|
|
582
|
-
"package": dist.metadata["Name"],
|
|
583
|
-
"version": dist.version,
|
|
584
|
-
"summary": dist.metadata["Summary"]
|
|
585
|
-
}
|
|
586
|
-
except Exception as e:
|
|
587
|
-
console.print(f"[yellow]获取CLI扩展信息失败: {e}[/]", style="warning")
|
|
588
|
-
|
|
589
|
-
if args.type in ["modules", "all"] and installed["modules"]:
|
|
590
|
-
table = Table(title="已安装模块", box=SIMPLE)
|
|
591
|
-
table.add_column("模块名", style="green")
|
|
592
|
-
table.add_column("包名", style="blue")
|
|
593
|
-
table.add_column("版本")
|
|
594
|
-
table.add_column("描述")
|
|
595
|
-
|
|
596
|
-
for name, info in installed["modules"].items():
|
|
597
|
-
table.add_row(name, info["package"], info["version"], info["summary"])
|
|
598
|
-
|
|
599
|
-
console.print(table)
|
|
600
|
-
|
|
601
|
-
if args.type in ["adapters", "all"] and installed["adapters"]:
|
|
602
|
-
table = Table(title="已安装适配器", box=SIMPLE)
|
|
603
|
-
table.add_column("适配器名", style="yellow")
|
|
604
|
-
table.add_column("包名", style="blue")
|
|
605
|
-
table.add_column("版本")
|
|
606
|
-
table.add_column("描述")
|
|
607
|
-
|
|
608
|
-
for name, info in installed["adapters"].items():
|
|
609
|
-
table.add_row(name, info["package"], info["version"], info["summary"])
|
|
610
|
-
|
|
611
|
-
console.print(table)
|
|
612
|
-
|
|
613
|
-
if cli_extensions and args.type in ["cli", "all"]:
|
|
614
|
-
table = Table(title="已安装CLI扩展", box=SIMPLE)
|
|
615
|
-
table.add_column("命令名", style="magenta")
|
|
616
|
-
table.add_column("包名", style="blue")
|
|
617
|
-
table.add_column("版本")
|
|
618
|
-
table.add_column("描述")
|
|
977
|
+
self.package_manager.install_package(args.package, args.upgrade)
|
|
619
978
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
console.print(table)
|
|
623
|
-
|
|
624
|
-
if not installed["modules"] and not installed["adapters"] and not cli_extensions:
|
|
625
|
-
console.print(Panel(
|
|
626
|
-
"没有安装任何ErisPulse模块、适配器或CLI扩展",
|
|
627
|
-
title="提示",
|
|
628
|
-
style="info"
|
|
629
|
-
))
|
|
979
|
+
elif args.command == "uninstall":
|
|
980
|
+
self.package_manager.uninstall_package(args.package)
|
|
630
981
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
982
|
+
elif args.command == "module":
|
|
983
|
+
from ErisPulse.Core import mods
|
|
984
|
+
installed = self.package_manager.get_installed_packages()
|
|
634
985
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
remote_packages = asyncio.run(PyPIManager.get_remote_packages())
|
|
651
|
-
|
|
652
|
-
if args.type in ["modules", "all"] and remote_packages["modules"]:
|
|
653
|
-
table = Table(title="远程模块", box=SIMPLE)
|
|
654
|
-
table.add_column("模块名", style="green")
|
|
655
|
-
table.add_column("包名", style="blue")
|
|
656
|
-
table.add_column("版本")
|
|
657
|
-
table.add_column("描述")
|
|
658
|
-
|
|
659
|
-
for name, info in remote_packages["modules"].items():
|
|
660
|
-
table.add_row(name, info["package"], info["version"], info["description"])
|
|
986
|
+
if args.module_command == "enable":
|
|
987
|
+
if args.module not in installed["modules"]:
|
|
988
|
+
console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
|
|
989
|
+
else:
|
|
990
|
+
mods.set_module_status(args.module, True)
|
|
991
|
+
console.print(f"[success]模块 [bold]{args.module}[/] 已启用[/]")
|
|
992
|
+
|
|
993
|
+
elif args.module_command == "disable":
|
|
994
|
+
if args.module not in installed["modules"]:
|
|
995
|
+
console.print(f"[error]模块 [bold]{args.module}[/] 不存在或未安装[/]")
|
|
996
|
+
else:
|
|
997
|
+
mods.set_module_status(args.module, False)
|
|
998
|
+
console.print(f"[warning]模块 [bold]{args.module}[/] 已禁用[/]")
|
|
999
|
+
else:
|
|
1000
|
+
self.parser.parse_args(["module", "--help"])
|
|
661
1001
|
|
|
662
|
-
|
|
1002
|
+
elif args.command == "list":
|
|
1003
|
+
pkg_type = args.type
|
|
1004
|
+
if pkg_type == "all":
|
|
1005
|
+
self._print_installed_packages("modules", args.outdated)
|
|
1006
|
+
self._print_installed_packages("adapters", args.outdated)
|
|
1007
|
+
self._print_installed_packages("cli", args.outdated)
|
|
1008
|
+
else:
|
|
1009
|
+
self._print_installed_packages(pkg_type, args.outdated)
|
|
663
1010
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1011
|
+
elif args.command == "list-remote":
|
|
1012
|
+
pkg_type = args.type
|
|
1013
|
+
if pkg_type == "all":
|
|
1014
|
+
self._print_remote_packages("modules")
|
|
1015
|
+
self._print_remote_packages("adapters")
|
|
1016
|
+
self._print_remote_packages("cli")
|
|
1017
|
+
else:
|
|
1018
|
+
self._print_remote_packages(pkg_type)
|
|
670
1019
|
|
|
671
|
-
|
|
672
|
-
|
|
1020
|
+
elif args.command == "upgrade":
|
|
1021
|
+
if args.package:
|
|
1022
|
+
self.package_manager.install_package(args.package, upgrade=True)
|
|
1023
|
+
else:
|
|
1024
|
+
if args.force or Confirm.ask("确定要升级所有ErisPulse组件吗?"):
|
|
1025
|
+
self.package_manager.upgrade_all()
|
|
1026
|
+
|
|
1027
|
+
elif args.command == "run":
|
|
1028
|
+
script = args.script or "main.py"
|
|
1029
|
+
if not os.path.exists(script):
|
|
1030
|
+
console.print(f"[error]找不到指定文件: [path]{script}[/][/]")
|
|
1031
|
+
return
|
|
673
1032
|
|
|
674
|
-
|
|
1033
|
+
reload_mode = args.reload and not args.no_reload
|
|
1034
|
+
self._setup_watchdog(script, reload_mode)
|
|
675
1035
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
for name, info in remote_packages["cli_extensions"].items():
|
|
685
|
-
commands = info.get("command", [])
|
|
686
|
-
command_str = ", ".join(commands) if isinstance(commands, list) else str(commands)
|
|
687
|
-
table.add_row(command_str, info["package"], info["version"], info["description"])
|
|
688
|
-
|
|
689
|
-
console.print(table)
|
|
1036
|
+
try:
|
|
1037
|
+
while True:
|
|
1038
|
+
time.sleep(1)
|
|
1039
|
+
except KeyboardInterrupt:
|
|
1040
|
+
console.print("\n[info]正在安全关闭...[/]")
|
|
1041
|
+
self._cleanup()
|
|
1042
|
+
console.print("[success]已安全退出[/]")
|
|
690
1043
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1044
|
+
elif args.command == "init":
|
|
1045
|
+
from ErisPulse import sdk
|
|
1046
|
+
sdk.init()
|
|
1047
|
+
console.print("[success]ErisPulse项目初始化完成[/]")
|
|
1048
|
+
|
|
1049
|
+
# 处理第三方命令
|
|
1050
|
+
elif args.command in self._get_external_commands():
|
|
1051
|
+
# 获取第三方命令的处理函数并执行
|
|
1052
|
+
entry_points = importlib.metadata.entry_points()
|
|
1053
|
+
if hasattr(entry_points, 'select'):
|
|
1054
|
+
cli_entries = entry_points.select(group='erispulse.cli')
|
|
1055
|
+
else:
|
|
1056
|
+
cli_entries = entry_points.get('erispulse.cli', [])
|
|
1057
|
+
|
|
1058
|
+
for entry in cli_entries:
|
|
1059
|
+
if entry.name == args.command:
|
|
1060
|
+
cli_func = entry.load()
|
|
1061
|
+
if callable(cli_func):
|
|
1062
|
+
# 创建一个新的解析器来解析第三方命令的参数
|
|
1063
|
+
subparser = self.parser._subparsers._group_actions[0].choices[args.command]
|
|
1064
|
+
parsed_args = subparser.parse_args(sys.argv[2:])
|
|
1065
|
+
# 调用第三方命令处理函数
|
|
1066
|
+
parsed_args.func(parsed_args)
|
|
1067
|
+
break
|
|
1068
|
+
|
|
1069
|
+
except KeyboardInterrupt:
|
|
1070
|
+
console.print("\n[warning]操作被用户中断[/]")
|
|
1071
|
+
self._cleanup()
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
console.print(f"[error]执行命令时出错: {e}[/]")
|
|
1074
|
+
if args.verbose >= 1:
|
|
1075
|
+
import traceback
|
|
1076
|
+
console.print(traceback.format_exc())
|
|
1077
|
+
self._cleanup()
|
|
1078
|
+
sys.exit(1)
|
|
1079
|
+
|
|
1080
|
+
def main():
|
|
1081
|
+
"""
|
|
1082
|
+
CLI入口点
|
|
1083
|
+
|
|
1084
|
+
{!--< tips >!--}
|
|
1085
|
+
1. 创建CLI实例并运行
|
|
1086
|
+
2. 处理全局异常
|
|
1087
|
+
{!--< /tips >!--}
|
|
1088
|
+
"""
|
|
1089
|
+
cli = CLI()
|
|
1090
|
+
cli.run()
|
|
721
1091
|
|
|
722
1092
|
if __name__ == "__main__":
|
|
723
1093
|
main()
|