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.
Files changed (143) hide show
  1. maque/__init__.py +30 -0
  2. maque/__main__.py +926 -0
  3. maque/ai_platform/__init__.py +0 -0
  4. maque/ai_platform/crawl.py +45 -0
  5. maque/ai_platform/metrics.py +258 -0
  6. maque/ai_platform/nlp_preprocess.py +67 -0
  7. maque/ai_platform/webpage_screen_shot.py +195 -0
  8. maque/algorithms/__init__.py +78 -0
  9. maque/algorithms/bezier.py +15 -0
  10. maque/algorithms/bktree.py +117 -0
  11. maque/algorithms/core.py +104 -0
  12. maque/algorithms/hilbert.py +16 -0
  13. maque/algorithms/rate_function.py +92 -0
  14. maque/algorithms/transform.py +27 -0
  15. maque/algorithms/trie.py +272 -0
  16. maque/algorithms/utils.py +63 -0
  17. maque/algorithms/video.py +587 -0
  18. maque/api/__init__.py +1 -0
  19. maque/api/common.py +110 -0
  20. maque/api/fetch.py +26 -0
  21. maque/api/static/icon.png +0 -0
  22. maque/api/static/redoc.standalone.js +1782 -0
  23. maque/api/static/swagger-ui-bundle.js +3 -0
  24. maque/api/static/swagger-ui.css +3 -0
  25. maque/cli/__init__.py +1 -0
  26. maque/cli/clean_invisible_chars.py +324 -0
  27. maque/cli/core.py +34 -0
  28. maque/cli/groups/__init__.py +26 -0
  29. maque/cli/groups/config.py +205 -0
  30. maque/cli/groups/data.py +615 -0
  31. maque/cli/groups/doctor.py +259 -0
  32. maque/cli/groups/embedding.py +222 -0
  33. maque/cli/groups/git.py +29 -0
  34. maque/cli/groups/help.py +410 -0
  35. maque/cli/groups/llm.py +223 -0
  36. maque/cli/groups/mcp.py +241 -0
  37. maque/cli/groups/mllm.py +1795 -0
  38. maque/cli/groups/mllm_simple.py +60 -0
  39. maque/cli/groups/quant.py +210 -0
  40. maque/cli/groups/service.py +490 -0
  41. maque/cli/groups/system.py +570 -0
  42. maque/cli/mllm_run.py +1451 -0
  43. maque/cli/script.py +52 -0
  44. maque/cli/tree.py +49 -0
  45. maque/clustering/__init__.py +52 -0
  46. maque/clustering/analyzer.py +347 -0
  47. maque/clustering/clusterers.py +464 -0
  48. maque/clustering/sampler.py +134 -0
  49. maque/clustering/visualizer.py +205 -0
  50. maque/constant.py +13 -0
  51. maque/core.py +133 -0
  52. maque/cv/__init__.py +1 -0
  53. maque/cv/image.py +219 -0
  54. maque/cv/utils.py +68 -0
  55. maque/cv/video/__init__.py +3 -0
  56. maque/cv/video/keyframe_extractor.py +368 -0
  57. maque/embedding/__init__.py +43 -0
  58. maque/embedding/base.py +56 -0
  59. maque/embedding/multimodal.py +308 -0
  60. maque/embedding/server.py +523 -0
  61. maque/embedding/text.py +311 -0
  62. maque/git/__init__.py +24 -0
  63. maque/git/pure_git.py +912 -0
  64. maque/io/__init__.py +29 -0
  65. maque/io/core.py +38 -0
  66. maque/io/ops.py +194 -0
  67. maque/llm/__init__.py +111 -0
  68. maque/llm/backend.py +416 -0
  69. maque/llm/base.py +411 -0
  70. maque/llm/server.py +366 -0
  71. maque/mcp_server.py +1096 -0
  72. maque/mllm_data_processor_pipeline/__init__.py +17 -0
  73. maque/mllm_data_processor_pipeline/core.py +341 -0
  74. maque/mllm_data_processor_pipeline/example.py +291 -0
  75. maque/mllm_data_processor_pipeline/steps/__init__.py +56 -0
  76. maque/mllm_data_processor_pipeline/steps/data_alignment.py +267 -0
  77. maque/mllm_data_processor_pipeline/steps/data_loader.py +172 -0
  78. maque/mllm_data_processor_pipeline/steps/data_validation.py +304 -0
  79. maque/mllm_data_processor_pipeline/steps/format_conversion.py +411 -0
  80. maque/mllm_data_processor_pipeline/steps/mllm_annotation.py +331 -0
  81. maque/mllm_data_processor_pipeline/steps/mllm_refinement.py +446 -0
  82. maque/mllm_data_processor_pipeline/steps/result_validation.py +501 -0
  83. maque/mllm_data_processor_pipeline/web_app.py +317 -0
  84. maque/nlp/__init__.py +14 -0
  85. maque/nlp/ngram.py +9 -0
  86. maque/nlp/parser.py +63 -0
  87. maque/nlp/risk_matcher.py +543 -0
  88. maque/nlp/sentence_splitter.py +202 -0
  89. maque/nlp/simple_tradition_cvt.py +31 -0
  90. maque/performance/__init__.py +21 -0
  91. maque/performance/_measure_time.py +70 -0
  92. maque/performance/_profiler.py +367 -0
  93. maque/performance/_stat_memory.py +51 -0
  94. maque/pipelines/__init__.py +15 -0
  95. maque/pipelines/clustering.py +252 -0
  96. maque/quantization/__init__.py +42 -0
  97. maque/quantization/auto_round.py +120 -0
  98. maque/quantization/base.py +145 -0
  99. maque/quantization/bitsandbytes.py +127 -0
  100. maque/quantization/llm_compressor.py +102 -0
  101. maque/retriever/__init__.py +35 -0
  102. maque/retriever/chroma.py +654 -0
  103. maque/retriever/document.py +140 -0
  104. maque/retriever/milvus.py +1140 -0
  105. maque/table_ops/__init__.py +1 -0
  106. maque/table_ops/core.py +133 -0
  107. maque/table_viewer/__init__.py +4 -0
  108. maque/table_viewer/download_assets.py +57 -0
  109. maque/table_viewer/server.py +698 -0
  110. maque/table_viewer/static/element-plus-icons.js +5791 -0
  111. maque/table_viewer/static/element-plus.css +1 -0
  112. maque/table_viewer/static/element-plus.js +65236 -0
  113. maque/table_viewer/static/main.css +268 -0
  114. maque/table_viewer/static/main.js +669 -0
  115. maque/table_viewer/static/vue.global.js +18227 -0
  116. maque/table_viewer/templates/index.html +401 -0
  117. maque/utils/__init__.py +56 -0
  118. maque/utils/color.py +68 -0
  119. maque/utils/color_string.py +45 -0
  120. maque/utils/compress.py +66 -0
  121. maque/utils/constant.py +183 -0
  122. maque/utils/core.py +261 -0
  123. maque/utils/cursor.py +143 -0
  124. maque/utils/distance.py +58 -0
  125. maque/utils/docker.py +96 -0
  126. maque/utils/downloads.py +51 -0
  127. maque/utils/excel_helper.py +542 -0
  128. maque/utils/helper_metrics.py +121 -0
  129. maque/utils/helper_parser.py +168 -0
  130. maque/utils/net.py +64 -0
  131. maque/utils/nvidia_stat.py +140 -0
  132. maque/utils/ops.py +53 -0
  133. maque/utils/packages.py +31 -0
  134. maque/utils/path.py +57 -0
  135. maque/utils/tar.py +260 -0
  136. maque/utils/untar.py +129 -0
  137. maque/web/__init__.py +0 -0
  138. maque/web/image_downloader.py +1410 -0
  139. maque-0.2.1.dist-info/METADATA +450 -0
  140. maque-0.2.1.dist-info/RECORD +143 -0
  141. maque-0.2.1.dist-info/WHEEL +4 -0
  142. maque-0.2.1.dist-info/entry_points.txt +3 -0
  143. 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()