maque 0.2.1__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.
- maque/__init__.py +30 -0
- maque/__main__.py +926 -0
- maque/ai_platform/__init__.py +0 -0
- maque/ai_platform/crawl.py +45 -0
- maque/ai_platform/metrics.py +258 -0
- maque/ai_platform/nlp_preprocess.py +67 -0
- maque/ai_platform/webpage_screen_shot.py +195 -0
- maque/algorithms/__init__.py +78 -0
- maque/algorithms/bezier.py +15 -0
- maque/algorithms/bktree.py +117 -0
- maque/algorithms/core.py +104 -0
- maque/algorithms/hilbert.py +16 -0
- maque/algorithms/rate_function.py +92 -0
- maque/algorithms/transform.py +27 -0
- maque/algorithms/trie.py +272 -0
- maque/algorithms/utils.py +63 -0
- maque/algorithms/video.py +587 -0
- maque/api/__init__.py +1 -0
- maque/api/common.py +110 -0
- maque/api/fetch.py +26 -0
- maque/api/static/icon.png +0 -0
- maque/api/static/redoc.standalone.js +1782 -0
- maque/api/static/swagger-ui-bundle.js +3 -0
- maque/api/static/swagger-ui.css +3 -0
- maque/cli/__init__.py +1 -0
- maque/cli/clean_invisible_chars.py +324 -0
- maque/cli/core.py +34 -0
- maque/cli/groups/__init__.py +26 -0
- maque/cli/groups/config.py +205 -0
- maque/cli/groups/data.py +615 -0
- maque/cli/groups/doctor.py +259 -0
- maque/cli/groups/embedding.py +222 -0
- maque/cli/groups/git.py +29 -0
- maque/cli/groups/help.py +410 -0
- maque/cli/groups/llm.py +223 -0
- maque/cli/groups/mcp.py +241 -0
- maque/cli/groups/mllm.py +1795 -0
- maque/cli/groups/mllm_simple.py +60 -0
- maque/cli/groups/quant.py +210 -0
- maque/cli/groups/service.py +490 -0
- maque/cli/groups/system.py +570 -0
- maque/cli/mllm_run.py +1451 -0
- maque/cli/script.py +52 -0
- maque/cli/tree.py +49 -0
- maque/clustering/__init__.py +52 -0
- maque/clustering/analyzer.py +347 -0
- maque/clustering/clusterers.py +464 -0
- maque/clustering/sampler.py +134 -0
- maque/clustering/visualizer.py +205 -0
- maque/constant.py +13 -0
- maque/core.py +133 -0
- maque/cv/__init__.py +1 -0
- maque/cv/image.py +219 -0
- maque/cv/utils.py +68 -0
- maque/cv/video/__init__.py +3 -0
- maque/cv/video/keyframe_extractor.py +368 -0
- maque/embedding/__init__.py +43 -0
- maque/embedding/base.py +56 -0
- maque/embedding/multimodal.py +308 -0
- maque/embedding/server.py +523 -0
- maque/embedding/text.py +311 -0
- maque/git/__init__.py +24 -0
- maque/git/pure_git.py +912 -0
- maque/io/__init__.py +29 -0
- maque/io/core.py +38 -0
- maque/io/ops.py +194 -0
- maque/llm/__init__.py +111 -0
- maque/llm/backend.py +416 -0
- maque/llm/base.py +411 -0
- maque/llm/server.py +366 -0
- maque/mcp_server.py +1096 -0
- maque/mllm_data_processor_pipeline/__init__.py +17 -0
- maque/mllm_data_processor_pipeline/core.py +341 -0
- maque/mllm_data_processor_pipeline/example.py +291 -0
- maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
- maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
- maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
- maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
- maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
- maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
- maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
- maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
- maque/mllm_data_processor_pipeline/web_app.py +317 -0
- maque/nlp/__init__.py +14 -0
- maque/nlp/ngram.py +9 -0
- maque/nlp/parser.py +63 -0
- maque/nlp/risk_matcher.py +543 -0
- maque/nlp/sentence_splitter.py +202 -0
- maque/nlp/simple_tradition_cvt.py +31 -0
- maque/performance/__init__.py +21 -0
- maque/performance/_measure_time.py +70 -0
- maque/performance/_profiler.py +367 -0
- maque/performance/_stat_memory.py +51 -0
- maque/pipelines/__init__.py +15 -0
- maque/pipelines/clustering.py +252 -0
- maque/quantization/__init__.py +42 -0
- maque/quantization/auto_round.py +120 -0
- maque/quantization/base.py +145 -0
- maque/quantization/bitsandbytes.py +127 -0
- maque/quantization/llm_compressor.py +102 -0
- maque/retriever/__init__.py +35 -0
- maque/retriever/chroma.py +654 -0
- maque/retriever/document.py +140 -0
- maque/retriever/milvus.py +1140 -0
- maque/table_ops/__init__.py +1 -0
- maque/table_ops/core.py +133 -0
- maque/table_viewer/__init__.py +4 -0
- maque/table_viewer/download_assets.py +57 -0
- maque/table_viewer/server.py +698 -0
- maque/table_viewer/static/element-plus-icons.js +5791 -0
- maque/table_viewer/static/element-plus.css +1 -0
- maque/table_viewer/static/element-plus.js +65236 -0
- maque/table_viewer/static/main.css +268 -0
- maque/table_viewer/static/main.js +669 -0
- maque/table_viewer/static/vue.global.js +18227 -0
- maque/table_viewer/templates/index.html +401 -0
- maque/utils/__init__.py +56 -0
- maque/utils/color.py +68 -0
- maque/utils/color_string.py +45 -0
- maque/utils/compress.py +66 -0
- maque/utils/constant.py +183 -0
- maque/utils/core.py +261 -0
- maque/utils/cursor.py +143 -0
- maque/utils/distance.py +58 -0
- maque/utils/docker.py +96 -0
- maque/utils/downloads.py +51 -0
- maque/utils/excel_helper.py +542 -0
- maque/utils/helper_metrics.py +121 -0
- maque/utils/helper_parser.py +168 -0
- maque/utils/net.py +64 -0
- maque/utils/nvidia_stat.py +140 -0
- maque/utils/ops.py +53 -0
- maque/utils/packages.py +31 -0
- maque/utils/path.py +57 -0
- maque/utils/tar.py +260 -0
- maque/utils/untar.py +129 -0
- maque/web/__init__.py +0 -0
- maque/web/image_downloader.py +1410 -0
- maque-0.2.1.dist-info/METADATA +450 -0
- maque-0.2.1.dist-info/RECORD +143 -0
- maque-0.2.1.dist-info/WHEEL +4 -0
- maque-0.2.1.dist-info/entry_points.txt +3 -0
- maque-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""系统工具命令组
|
|
2
|
+
|
|
3
|
+
包含端口管理、IP获取、压缩解压、文件分割合并、SSH密钥生成、计时器等系统工具。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from rich import print
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SystemGroup:
|
|
15
|
+
"""系统工具命令组"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, parent):
|
|
18
|
+
self.parent = parent
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def kill(ports, view: bool = False):
|
|
22
|
+
"""杀死指定端口的进程
|
|
23
|
+
|
|
24
|
+
跨平台支持 Linux/macOS/Windows
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ports: 端口号,可以是单个整数或逗号分隔的多个端口,如 "8080" 或 "8080,3000,5000"
|
|
28
|
+
view: 仅查看进程信息,不执行杀死操作
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
spr system kill 8080
|
|
32
|
+
spr system kill 8080,3000,5000
|
|
33
|
+
spr system kill 8080 --view # 仅查看
|
|
34
|
+
"""
|
|
35
|
+
import psutil
|
|
36
|
+
import platform
|
|
37
|
+
|
|
38
|
+
# 处理端口参数
|
|
39
|
+
if isinstance(ports, str):
|
|
40
|
+
port_list = [int(p.strip()) for p in ports.split(',') if p.strip()]
|
|
41
|
+
elif isinstance(ports, (int, float)):
|
|
42
|
+
port_list = [int(ports)]
|
|
43
|
+
elif isinstance(ports, (list, tuple)):
|
|
44
|
+
port_list = [int(p) for p in ports]
|
|
45
|
+
else:
|
|
46
|
+
print(f"[red]无效的端口参数: {ports}[/red]")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
if not port_list:
|
|
50
|
+
print("[yellow]请提供要杀死的端口号[/yellow]")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
found_any = False
|
|
54
|
+
|
|
55
|
+
for port in port_list:
|
|
56
|
+
processes_found = []
|
|
57
|
+
|
|
58
|
+
# 使用 psutil 跨平台查找进程
|
|
59
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
60
|
+
try:
|
|
61
|
+
connections = proc.connections(kind='inet')
|
|
62
|
+
for conn in connections:
|
|
63
|
+
if hasattr(conn.laddr, 'port') and conn.laddr.port == port:
|
|
64
|
+
processes_found.append({
|
|
65
|
+
'pid': proc.pid,
|
|
66
|
+
'name': proc.info['name'],
|
|
67
|
+
'port': port,
|
|
68
|
+
'process': proc
|
|
69
|
+
})
|
|
70
|
+
except (psutil.AccessDenied, psutil.NoSuchProcess, psutil.ZombieProcess):
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if not processes_found:
|
|
74
|
+
print(f"[yellow]端口 {port} 没有找到运行的进程[/yellow]")
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
found_any = True
|
|
78
|
+
|
|
79
|
+
for pinfo in processes_found:
|
|
80
|
+
if view:
|
|
81
|
+
print(f"[cyan]👁️ {pinfo['name']} (PID: {pinfo['pid']}) 占用端口 {pinfo['port']}[/cyan]")
|
|
82
|
+
else:
|
|
83
|
+
try:
|
|
84
|
+
pinfo['process'].terminate()
|
|
85
|
+
# 等待进程结束
|
|
86
|
+
try:
|
|
87
|
+
pinfo['process'].wait(timeout=3)
|
|
88
|
+
except psutil.TimeoutExpired:
|
|
89
|
+
# 强制杀死
|
|
90
|
+
pinfo['process'].kill()
|
|
91
|
+
print(f"[green]☠️ 已杀死 {pinfo['name']} (PID: {pinfo['pid']}) 端口 {pinfo['port']}[/green]")
|
|
92
|
+
except psutil.NoSuchProcess:
|
|
93
|
+
print(f"[yellow]进程 {pinfo['pid']} 已不存在[/yellow]")
|
|
94
|
+
except psutil.AccessDenied:
|
|
95
|
+
print(f"[red]无权限杀死进程 {pinfo['pid']},请使用管理员/root权限运行[/red]")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"[red]杀死进程 {pinfo['pid']} 失败: {e}[/red]")
|
|
98
|
+
|
|
99
|
+
if not found_any:
|
|
100
|
+
print(f"[yellow]🙃 没有找到占用指定端口的进程[/yellow]")
|
|
101
|
+
|
|
102
|
+
return found_any
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def get_ip(env: str = "inner"):
|
|
106
|
+
"""获取本机IP地址
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
env: "inner" 获取内网IP,"outer" 获取外网IP
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
spr system get_ip
|
|
113
|
+
spr system get_ip --env=outer
|
|
114
|
+
"""
|
|
115
|
+
import socket
|
|
116
|
+
|
|
117
|
+
if env == "inner":
|
|
118
|
+
try:
|
|
119
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
120
|
+
s.connect(('8.8.8.8', 80))
|
|
121
|
+
ip = s.getsockname()[0]
|
|
122
|
+
print(f"[green]内网IP: {ip}[/green]")
|
|
123
|
+
return ip
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"[red]获取内网IP失败: {e}[/red]")
|
|
126
|
+
return None
|
|
127
|
+
elif env == "outer":
|
|
128
|
+
try:
|
|
129
|
+
import requests
|
|
130
|
+
ip = requests.get('http://ifconfig.me/ip', timeout=5).text.strip()
|
|
131
|
+
print(f"[green]外网IP: {ip}[/green]")
|
|
132
|
+
return ip
|
|
133
|
+
except ImportError:
|
|
134
|
+
print("[red]需要安装 requests 库: pip install requests[/red]")
|
|
135
|
+
return None
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"[red]获取外网IP失败: {e}[/red]")
|
|
138
|
+
return None
|
|
139
|
+
else:
|
|
140
|
+
print(f"[red]无效的 env 参数: {env},应为 'inner' 或 'outer'[/red]")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def pack(source_path: str, target_path: str = None, format: str = 'gztar'):
|
|
145
|
+
"""压缩文件或文件夹
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
source_path: 源文件/文件夹路径
|
|
149
|
+
target_path: 目标压缩包路径(不含扩展名),默认与源同名
|
|
150
|
+
format: 压缩格式,支持 "zip", "tar", "gztar"(默认), "bztar", "xztar"
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
spr system pack my_folder
|
|
154
|
+
spr system pack my_folder --format=zip
|
|
155
|
+
spr system pack ./data --target_path=backup
|
|
156
|
+
"""
|
|
157
|
+
import shutil
|
|
158
|
+
|
|
159
|
+
if target_path is None:
|
|
160
|
+
target_path = Path(source_path).name
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
new_path = shutil.make_archive(target_path, format, root_dir=source_path)
|
|
164
|
+
print(f"[green]✓ 压缩完成: {new_path}[/green]")
|
|
165
|
+
return new_path
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"[red]压缩失败: {e}[/red]")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def unpack(filename: str, extract_dir: str = None, format: str = None):
|
|
172
|
+
"""解压文件
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
filename: 压缩包路径
|
|
176
|
+
extract_dir: 解压目标目录,默认为压缩包同名目录
|
|
177
|
+
format: 压缩格式,默认自动检测。支持 "zip", "tar", "gztar", "bztar", "xztar"
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
spr system unpack archive.tar.gz
|
|
181
|
+
spr system unpack data.zip --extract_dir=./output
|
|
182
|
+
"""
|
|
183
|
+
import shutil
|
|
184
|
+
from shutil import _find_unpack_format, _UNPACK_FORMATS
|
|
185
|
+
|
|
186
|
+
file_path = Path(filename)
|
|
187
|
+
if not file_path.exists():
|
|
188
|
+
print(f"[red]文件不存在: {filename}[/red]")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# 自动确定解压目录名
|
|
192
|
+
if extract_dir is None:
|
|
193
|
+
name = file_path.name
|
|
194
|
+
file_format = _find_unpack_format(filename)
|
|
195
|
+
if file_format:
|
|
196
|
+
file_postfix_list = _UNPACK_FORMATS[file_format][0]
|
|
197
|
+
for postfix in file_postfix_list:
|
|
198
|
+
if name.endswith(postfix):
|
|
199
|
+
target_name = name[:-len(postfix)]
|
|
200
|
+
break
|
|
201
|
+
else:
|
|
202
|
+
target_name = name.replace('.', '_')
|
|
203
|
+
else:
|
|
204
|
+
target_name = name.replace('.', '_')
|
|
205
|
+
extract_dir = f"./{target_name}/"
|
|
206
|
+
|
|
207
|
+
extract_path = Path(extract_dir)
|
|
208
|
+
if not extract_path.exists():
|
|
209
|
+
extract_path.mkdir(parents=True)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
shutil.unpack_archive(filename, extract_dir, format=format)
|
|
213
|
+
print(f"[green]✓ 解压完成: {extract_path.absolute()}[/green]")
|
|
214
|
+
return str(extract_path.absolute())
|
|
215
|
+
except Exception as e:
|
|
216
|
+
print(f"[red]解压失败: {e}[/red]")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def split(file_path: str, chunk_size: str = "1G"):
|
|
221
|
+
"""将大文件分割成多个块
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
file_path: 原始文件路径
|
|
225
|
+
chunk_size: 每个块的大小,支持 K/M/G 后缀,默认 1G
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
spr system split large_file.dat
|
|
229
|
+
spr system split video.mp4 --chunk_size=500M
|
|
230
|
+
spr system split data.bin --chunk_size=100M
|
|
231
|
+
"""
|
|
232
|
+
# 解析大小
|
|
233
|
+
size_str = str(chunk_size).upper().strip()
|
|
234
|
+
multipliers = {'K': 1024, 'M': 1024**2, 'G': 1024**3}
|
|
235
|
+
|
|
236
|
+
if size_str[-1] in multipliers:
|
|
237
|
+
chunk_bytes = int(float(size_str[:-1]) * multipliers[size_str[-1]])
|
|
238
|
+
else:
|
|
239
|
+
chunk_bytes = int(size_str)
|
|
240
|
+
|
|
241
|
+
file_path_obj = Path(file_path)
|
|
242
|
+
if not file_path_obj.exists():
|
|
243
|
+
print(f"[red]文件不存在: {file_path}[/red]")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
file_size = file_path_obj.stat().st_size
|
|
247
|
+
total_chunks = (file_size + chunk_bytes - 1) // chunk_bytes
|
|
248
|
+
|
|
249
|
+
print(f"[blue]分割文件: {file_path}[/blue]")
|
|
250
|
+
print(f"文件大小: {file_size / 1024**2:.2f} MB")
|
|
251
|
+
print(f"块大小: {chunk_bytes / 1024**2:.2f} MB")
|
|
252
|
+
print(f"预计分割为 {total_chunks} 个块")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
with open(file_path, 'rb') as f:
|
|
256
|
+
chunk_number = 0
|
|
257
|
+
while True:
|
|
258
|
+
chunk = f.read(chunk_bytes)
|
|
259
|
+
if not chunk:
|
|
260
|
+
break
|
|
261
|
+
chunk_file = f"{file_path}_part_{chunk_number:03d}"
|
|
262
|
+
with open(chunk_file, 'wb') as cf:
|
|
263
|
+
cf.write(chunk)
|
|
264
|
+
print(f" [green]✓[/green] {chunk_file} ({len(chunk) / 1024**2:.2f} MB)")
|
|
265
|
+
chunk_number += 1
|
|
266
|
+
|
|
267
|
+
print(f"[green]✓ 分割完成,共 {chunk_number} 个块[/green]")
|
|
268
|
+
return chunk_number
|
|
269
|
+
except Exception as e:
|
|
270
|
+
print(f"[red]分割失败: {e}[/red]")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def merge(input_prefix: str, input_dir: str = '.', output_path: str = None):
|
|
275
|
+
"""合并分割后的文件块
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
input_prefix: 分割文件的前缀(原文件名)
|
|
279
|
+
input_dir: 分割文件所在目录,默认当前目录
|
|
280
|
+
output_path: 合并后的文件路径,默认为 input_prefix
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
spr system merge large_file.dat
|
|
284
|
+
spr system merge video.mp4 --input_dir=./chunks
|
|
285
|
+
spr system merge data.bin --output_path=restored.bin
|
|
286
|
+
"""
|
|
287
|
+
import glob
|
|
288
|
+
|
|
289
|
+
if output_path is None:
|
|
290
|
+
output_path = os.path.join(input_dir, input_prefix)
|
|
291
|
+
|
|
292
|
+
# 查找所有分块文件
|
|
293
|
+
pattern = os.path.join(input_dir, f"{input_prefix}_part_*")
|
|
294
|
+
parts = sorted(glob.glob(pattern))
|
|
295
|
+
|
|
296
|
+
if not parts:
|
|
297
|
+
print(f"[red]没有找到匹配的分块文件: {pattern}[/red]")
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
print(f"[blue]合并文件块[/blue]")
|
|
301
|
+
print(f"找到 {len(parts)} 个分块文件")
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
total_size = 0
|
|
305
|
+
with open(output_path, 'wb') as output_file:
|
|
306
|
+
for part in parts:
|
|
307
|
+
with open(part, 'rb') as part_file:
|
|
308
|
+
data = part_file.read()
|
|
309
|
+
output_file.write(data)
|
|
310
|
+
total_size += len(data)
|
|
311
|
+
print(f" [green]✓[/green] {Path(part).name}")
|
|
312
|
+
|
|
313
|
+
print(f"[green]✓ 合并完成: {output_path} ({total_size / 1024**2:.2f} MB)[/green]")
|
|
314
|
+
return output_path
|
|
315
|
+
except Exception as e:
|
|
316
|
+
print(f"[red]合并失败: {e}[/red]")
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def gen_key(name: str, email: str = None, key_type: str = 'rsa'):
|
|
321
|
+
"""生成SSH密钥对
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
name: 密钥名称,将保存为 ~/.ssh/id_{type}_{name}
|
|
325
|
+
email: 关联的邮箱地址
|
|
326
|
+
key_type: 密钥类型,"rsa"(默认) 或 "ed25519"(推荐)
|
|
327
|
+
|
|
328
|
+
Examples:
|
|
329
|
+
spr system gen_key github
|
|
330
|
+
spr system gen_key myserver --email=me@example.com
|
|
331
|
+
spr system gen_key legacy --key_type=rsa
|
|
332
|
+
"""
|
|
333
|
+
import subprocess
|
|
334
|
+
|
|
335
|
+
ssh_dir = Path.home() / '.ssh'
|
|
336
|
+
ssh_dir.mkdir(exist_ok=True)
|
|
337
|
+
|
|
338
|
+
if key_type == 'ed25519':
|
|
339
|
+
key_path = ssh_dir / f'id_ed25519_{name}'
|
|
340
|
+
cmd = ['ssh-keygen', '-t', 'ed25519', '-f', str(key_path), '-N', '']
|
|
341
|
+
else:
|
|
342
|
+
key_path = ssh_dir / f'id_rsa_{name}'
|
|
343
|
+
cmd = ['ssh-keygen', '-t', 'rsa', '-b', '4096', '-f', str(key_path), '-N', '']
|
|
344
|
+
|
|
345
|
+
if email:
|
|
346
|
+
cmd.extend(['-C', email])
|
|
347
|
+
|
|
348
|
+
if key_path.exists():
|
|
349
|
+
print(f"[yellow]密钥已存在: {key_path}[/yellow]")
|
|
350
|
+
response = input("是否覆盖? (y/N): ")
|
|
351
|
+
if response.lower() != 'y':
|
|
352
|
+
print("操作已取消")
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
357
|
+
if result.returncode != 0:
|
|
358
|
+
print(f"[red]生成密钥失败: {result.stderr}[/red]")
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
# 读取并显示公钥
|
|
362
|
+
pub_key_path = str(key_path) + '.pub'
|
|
363
|
+
with open(pub_key_path, 'r', encoding='utf-8') as f:
|
|
364
|
+
pub_key = f.read().strip()
|
|
365
|
+
|
|
366
|
+
print(f"[green]✓ 密钥生成成功[/green]")
|
|
367
|
+
print(f"\n[cyan]私钥路径:[/cyan] {key_path}")
|
|
368
|
+
print(f"[cyan]公钥路径:[/cyan] {pub_key_path}")
|
|
369
|
+
print(f"\n[cyan]公钥内容:[/cyan]")
|
|
370
|
+
print(f"[dim]{pub_key}[/dim]")
|
|
371
|
+
|
|
372
|
+
# 显示配置提示
|
|
373
|
+
config_path = ssh_dir / 'config'
|
|
374
|
+
print(f"""
|
|
375
|
+
[yellow]提示: 你可能需要在 {config_path} 中添加以下配置:[/yellow]
|
|
376
|
+
|
|
377
|
+
[dim]# 远程服务器
|
|
378
|
+
Host {name}
|
|
379
|
+
HostName <服务器IP或域名>
|
|
380
|
+
User <用户名>
|
|
381
|
+
Port 22
|
|
382
|
+
IdentityFile {key_path}
|
|
383
|
+
|
|
384
|
+
# 或 Git 服务
|
|
385
|
+
Host {name}
|
|
386
|
+
HostName github.com
|
|
387
|
+
User git
|
|
388
|
+
IdentityFile {key_path}
|
|
389
|
+
IdentitiesOnly yes[/dim]
|
|
390
|
+
""")
|
|
391
|
+
return str(key_path)
|
|
392
|
+
except FileNotFoundError:
|
|
393
|
+
print("[red]ssh-keygen 命令不可用,请确保已安装 OpenSSH[/red]")
|
|
394
|
+
return None
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"[red]生成密钥失败: {e}[/red]")
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def timer(interval: float = 0.05):
|
|
401
|
+
"""交互式计时器工具
|
|
402
|
+
|
|
403
|
+
支持开始、暂停、记录点、停止功能
|
|
404
|
+
|
|
405
|
+
快捷键:
|
|
406
|
+
Space/S: 开始 / 暂停
|
|
407
|
+
L: 记录点 (Lap)
|
|
408
|
+
Q: 停止并退出
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
interval: 刷新间隔(秒),默认 0.05
|
|
412
|
+
|
|
413
|
+
Examples:
|
|
414
|
+
spr system timer
|
|
415
|
+
spr system timer --interval=0.1
|
|
416
|
+
"""
|
|
417
|
+
def format_time(seconds):
|
|
418
|
+
"""格式化时间显示"""
|
|
419
|
+
hours = int(seconds // 3600)
|
|
420
|
+
minutes = int((seconds % 3600) // 60)
|
|
421
|
+
secs = seconds % 60
|
|
422
|
+
if hours > 0:
|
|
423
|
+
return f"{hours:02d}:{minutes:02d}:{secs:05.2f}"
|
|
424
|
+
elif minutes > 0:
|
|
425
|
+
return f"{minutes:02d}:{secs:05.2f}"
|
|
426
|
+
else:
|
|
427
|
+
return f"{secs:.2f}"
|
|
428
|
+
|
|
429
|
+
# 跨平台非阻塞键盘输入
|
|
430
|
+
class KeyReader:
|
|
431
|
+
def __init__(self):
|
|
432
|
+
self.is_windows = os.name == 'nt'
|
|
433
|
+
if self.is_windows:
|
|
434
|
+
import msvcrt
|
|
435
|
+
self.msvcrt = msvcrt
|
|
436
|
+
else:
|
|
437
|
+
import termios
|
|
438
|
+
import tty
|
|
439
|
+
import select
|
|
440
|
+
self.termios = termios
|
|
441
|
+
self.tty = tty
|
|
442
|
+
self.select = select
|
|
443
|
+
self.fd = sys.stdin.fileno()
|
|
444
|
+
self.old_settings = termios.tcgetattr(self.fd)
|
|
445
|
+
|
|
446
|
+
def setup(self):
|
|
447
|
+
if not self.is_windows:
|
|
448
|
+
self.tty.setraw(self.fd)
|
|
449
|
+
|
|
450
|
+
def cleanup(self):
|
|
451
|
+
if not self.is_windows:
|
|
452
|
+
self.termios.tcsetattr(self.fd, self.termios.TCSADRAIN, self.old_settings)
|
|
453
|
+
|
|
454
|
+
def get_key(self):
|
|
455
|
+
"""非阻塞获取按键,返回 None 如果没有按键"""
|
|
456
|
+
if self.is_windows:
|
|
457
|
+
if self.msvcrt.kbhit():
|
|
458
|
+
ch = self.msvcrt.getch()
|
|
459
|
+
return ch.decode('utf-8', errors='ignore').lower()
|
|
460
|
+
return None
|
|
461
|
+
else:
|
|
462
|
+
if self.select.select([sys.stdin], [], [], 0)[0]:
|
|
463
|
+
ch = sys.stdin.read(1)
|
|
464
|
+
return ch.lower()
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
# 进入 raw 模式前使用 rich 格式
|
|
468
|
+
print("[cyan]═══════════════════════════════════════[/cyan]")
|
|
469
|
+
print("[cyan] 交互式计时器[/cyan]")
|
|
470
|
+
print("[cyan]═══════════════════════════════════════[/cyan]")
|
|
471
|
+
print()
|
|
472
|
+
print("快捷键:")
|
|
473
|
+
print(" [green]S / Space[/green] 开始 / 暂停")
|
|
474
|
+
print(" [yellow]L[/yellow] 记录点 (Lap)")
|
|
475
|
+
print(" [red]Q[/red] 停止并退出")
|
|
476
|
+
print()
|
|
477
|
+
print("[yellow]按 S 开始计时...[/yellow]")
|
|
478
|
+
print()
|
|
479
|
+
|
|
480
|
+
key_reader = KeyReader()
|
|
481
|
+
key_reader.setup()
|
|
482
|
+
|
|
483
|
+
# raw 模式下使用 ANSI 颜色码和 \r\n 换行
|
|
484
|
+
CYAN = "\033[36m"
|
|
485
|
+
GREEN = "\033[32m"
|
|
486
|
+
YELLOW = "\033[33m"
|
|
487
|
+
RED = "\033[31m"
|
|
488
|
+
BOLD = "\033[1m"
|
|
489
|
+
RESET = "\033[0m"
|
|
490
|
+
NL = "\r\n"
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
# 等待开始
|
|
494
|
+
while True:
|
|
495
|
+
key = key_reader.get_key()
|
|
496
|
+
if key in ('s', ' '):
|
|
497
|
+
break
|
|
498
|
+
if key == 'q':
|
|
499
|
+
key_reader.cleanup()
|
|
500
|
+
print("[yellow]已退出[/yellow]")
|
|
501
|
+
return
|
|
502
|
+
time.sleep(0.05)
|
|
503
|
+
|
|
504
|
+
t0 = time.time()
|
|
505
|
+
total_paused = 0.0
|
|
506
|
+
suspend_start = None
|
|
507
|
+
paused = False
|
|
508
|
+
laps = []
|
|
509
|
+
last_lap_time = 0.0
|
|
510
|
+
|
|
511
|
+
sys.stdout.write(f"{GREEN}▶ 计时开始{RESET}{NL}{NL}")
|
|
512
|
+
sys.stdout.flush()
|
|
513
|
+
|
|
514
|
+
while True:
|
|
515
|
+
time.sleep(interval)
|
|
516
|
+
ct = time.time()
|
|
517
|
+
|
|
518
|
+
# 检查按键
|
|
519
|
+
key = key_reader.get_key()
|
|
520
|
+
if key == 'q':
|
|
521
|
+
break
|
|
522
|
+
elif key in ('s', ' '):
|
|
523
|
+
paused = not paused
|
|
524
|
+
if paused:
|
|
525
|
+
suspend_start = ct
|
|
526
|
+
current_time = ct - t0 - total_paused
|
|
527
|
+
sys.stdout.write(f"\r\033[K{YELLOW}⏸ {format_time(current_time)} [暂停 - 按S继续]{RESET}")
|
|
528
|
+
sys.stdout.flush()
|
|
529
|
+
else:
|
|
530
|
+
if suspend_start:
|
|
531
|
+
total_paused += ct - suspend_start
|
|
532
|
+
suspend_start = None
|
|
533
|
+
sys.stdout.write(NL)
|
|
534
|
+
sys.stdout.flush()
|
|
535
|
+
elif key == 'l' and not paused:
|
|
536
|
+
current_time = ct - t0 - total_paused
|
|
537
|
+
lap_time = current_time - last_lap_time
|
|
538
|
+
laps.append((current_time, lap_time))
|
|
539
|
+
last_lap_time = current_time
|
|
540
|
+
sys.stdout.write(f"\r\033[K{YELLOW}Lap {len(laps)}: {format_time(current_time)} ({CYAN}+{format_time(lap_time)}{YELLOW}){RESET}{NL}")
|
|
541
|
+
sys.stdout.flush()
|
|
542
|
+
|
|
543
|
+
# 更新显示
|
|
544
|
+
if not paused:
|
|
545
|
+
current_time = ct - t0 - total_paused
|
|
546
|
+
sys.stdout.write(f"\r{GREEN}▶ {format_time(current_time)}{RESET}")
|
|
547
|
+
sys.stdout.flush()
|
|
548
|
+
|
|
549
|
+
# 计算最终时间
|
|
550
|
+
final_time = time.time() - t0 - total_paused
|
|
551
|
+
if suspend_start:
|
|
552
|
+
final_time -= (time.time() - suspend_start)
|
|
553
|
+
|
|
554
|
+
sys.stdout.write(f"{NL}{NL}")
|
|
555
|
+
sys.stdout.write(f"{RED}■ 计时停止{RESET}{NL}{NL}")
|
|
556
|
+
sys.stdout.write(f"{CYAN}═══════════════════════════════════════{RESET}{NL}")
|
|
557
|
+
sys.stdout.write(f"{BOLD}总计时间: {format_time(final_time)}{RESET}{NL}")
|
|
558
|
+
|
|
559
|
+
if laps:
|
|
560
|
+
sys.stdout.write(f"{NL}{YELLOW}记录点:{RESET}{NL}")
|
|
561
|
+
for i, (total, lap) in enumerate(laps, 1):
|
|
562
|
+
sys.stdout.write(f" Lap {i}: {format_time(total)} ({CYAN}+{format_time(lap)}{RESET}){NL}")
|
|
563
|
+
|
|
564
|
+
sys.stdout.write(f"{CYAN}═══════════════════════════════════════{RESET}{NL}")
|
|
565
|
+
sys.stdout.flush()
|
|
566
|
+
|
|
567
|
+
except Exception as e:
|
|
568
|
+
sys.stdout.write(f"{NL}错误: {e}{NL}")
|
|
569
|
+
finally:
|
|
570
|
+
key_reader.cleanup()
|