@agentunion/kite 1.3.2 → 1.4.0
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.
- package/CHANGELOG.md +200 -0
- package/cli.js +76 -0
- package/extensions/agents/assistant/entry.py +111 -1
- package/extensions/agents/assistant/server.py +263 -215
- package/extensions/channels/acp_channel/entry.py +111 -1
- package/extensions/channels/acp_channel/module.md +23 -22
- package/extensions/channels/acp_channel/server.py +263 -215
- package/extensions/event_hub_bench/entry.py +107 -1
- package/extensions/services/backup/entry.py +299 -21
- package/extensions/services/backup/module.md +24 -22
- package/extensions/services/model_service/entry.py +145 -19
- package/extensions/services/model_service/module.md +21 -22
- package/extensions/services/watchdog/entry.py +188 -25
- package/extensions/services/watchdog/monitor.py +144 -34
- package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
- package/extensions/services/web/config_example.py +35 -0
- package/extensions/services/web/config_loader.py +110 -0
- package/extensions/services/web/entry.py +114 -26
- package/extensions/services/web/module.md +35 -24
- package/extensions/services/web/pairing.py +250 -0
- package/extensions/services/web/pairing_codes.jsonl +16 -0
- package/extensions/services/web/relay.py +643 -0
- package/extensions/services/web/relay_config.json5 +67 -0
- package/extensions/services/web/routes/routes_management_ws.py +127 -0
- package/extensions/services/web/routes/routes_rpc.py +89 -0
- package/extensions/services/web/routes/routes_test.py +61 -0
- package/extensions/services/web/routes/schemas.py +0 -22
- package/extensions/services/web/server.py +421 -98
- package/extensions/services/web/static/css/style.css +67 -28
- package/extensions/services/web/static/index.html +234 -44
- package/extensions/services/web/static/js/app.js +1335 -48
- package/extensions/services/web/static/js/kernel-client-example.js +161 -0
- package/extensions/services/web/static/js/kernel-client.js +383 -0
- package/extensions/services/web/static/js/registry-tests.js +558 -0
- package/extensions/services/web/static/js/token-manager.js +175 -0
- package/extensions/services/web/static/pairing.html +248 -0
- package/extensions/services/web/static/test_registry.html +262 -0
- package/extensions/services/web/web_config.json5 +29 -0
- package/kernel/entry.py +120 -32
- package/kernel/event_hub.py +141 -16
- package/kernel/module.md +36 -33
- package/kernel/registry_store.py +48 -15
- package/kernel/rpc_router.py +120 -53
- package/kernel/server.py +219 -12
- package/kite_cli/__init__.py +3 -0
- package/kite_cli/__main__.py +5 -0
- package/kite_cli/commands/__init__.py +1 -0
- package/kite_cli/commands/clean.py +101 -0
- package/kite_cli/commands/doctor.py +35 -0
- package/kite_cli/commands/history.py +111 -0
- package/kite_cli/commands/info.py +96 -0
- package/kite_cli/commands/install.py +313 -0
- package/kite_cli/commands/list.py +143 -0
- package/kite_cli/commands/log.py +81 -0
- package/kite_cli/commands/rollback.py +88 -0
- package/kite_cli/commands/search.py +73 -0
- package/kite_cli/commands/uninstall.py +85 -0
- package/kite_cli/commands/update.py +118 -0
- package/kite_cli/core/__init__.py +1 -0
- package/kite_cli/core/checker.py +142 -0
- package/kite_cli/core/dependency.py +229 -0
- package/kite_cli/core/downloader.py +209 -0
- package/kite_cli/core/install_info.py +40 -0
- package/kite_cli/core/tool_installer.py +397 -0
- package/kite_cli/core/validator.py +78 -0
- package/kite_cli/main.py +289 -0
- package/kite_cli/utils/__init__.py +1 -0
- package/kite_cli/utils/i18n.py +252 -0
- package/kite_cli/utils/interactive.py +63 -0
- package/kite_cli/utils/operation_log.py +77 -0
- package/kite_cli/utils/paths.py +34 -0
- package/kite_cli/utils/version.py +308 -0
- package/launcher/entry.py +819 -158
- package/launcher/logging_setup.py +104 -0
- package/launcher/module.md +37 -37
- package/package.json +2 -1
- package/scripts/plan_manager.py +315 -0
- package/extensions/services/web/routes/routes_modules.py +0 -249
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""交互式问答工具"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def select_source(candidates: list) -> dict:
|
|
5
|
+
"""让用户选择来源"""
|
|
6
|
+
print("\n找到多个来源:")
|
|
7
|
+
for i, candidate in enumerate(candidates, 1):
|
|
8
|
+
platform = candidate["platform"]
|
|
9
|
+
name = candidate.get("name", "")
|
|
10
|
+
version = candidate.get("latest", candidate.get("version", ""))
|
|
11
|
+
desc = candidate.get("description", "")
|
|
12
|
+
|
|
13
|
+
if platform == "pypi":
|
|
14
|
+
print(f" [{i}] PyPI: {name} v{version}")
|
|
15
|
+
elif platform == "npm":
|
|
16
|
+
print(f" [{i}] npm: {name} v{version}")
|
|
17
|
+
elif platform == "git":
|
|
18
|
+
full_name = candidate.get("full_name", name)
|
|
19
|
+
print(f" [{i}] Git: {full_name}")
|
|
20
|
+
if desc:
|
|
21
|
+
print(f" {desc}")
|
|
22
|
+
elif platform == "local":
|
|
23
|
+
print(f" [{i}] 本地: {candidate.get('path', '')}")
|
|
24
|
+
|
|
25
|
+
while True:
|
|
26
|
+
try:
|
|
27
|
+
choice = input("\n? 选择来源 [1]: ").strip() or "1"
|
|
28
|
+
index = int(choice) - 1
|
|
29
|
+
if 0 <= index < len(candidates):
|
|
30
|
+
return candidates[index]
|
|
31
|
+
print("无效选择,请重试")
|
|
32
|
+
except (ValueError, KeyboardInterrupt):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def select_location() -> str:
|
|
37
|
+
"""让用户选择安装位置"""
|
|
38
|
+
print("\n? 选择安装位置:")
|
|
39
|
+
print(" [1] 开发环境 (Kite/extensions/) — 用于模块开发")
|
|
40
|
+
print(" [2] 本地实例 (~/.kite/workspace/Kite/extensions/) — 当前实例专用")
|
|
41
|
+
print(" [3] 全局共享 (~/.kite/modules/) — 所有实例共享")
|
|
42
|
+
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
choice = input("\n选择 [3]: ").strip() or "3"
|
|
46
|
+
if choice == "1":
|
|
47
|
+
return "dev"
|
|
48
|
+
elif choice == "2":
|
|
49
|
+
return "local"
|
|
50
|
+
elif choice == "3":
|
|
51
|
+
return "global"
|
|
52
|
+
print("无效选择,请重试")
|
|
53
|
+
except KeyboardInterrupt:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def confirm_action(message: str) -> bool:
|
|
58
|
+
"""确认操作"""
|
|
59
|
+
try:
|
|
60
|
+
answer = input(f"{message} [y/N]: ").strip().lower()
|
|
61
|
+
return answer in ("y", "yes")
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
return False
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""操作日志记录"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_log_file() -> Path:
|
|
9
|
+
"""获取日志文件路径"""
|
|
10
|
+
# 使用 KITE_DATA 环境变量,如果没有则使用 ~/.kite/data
|
|
11
|
+
kite_data = os.environ.get("KITE_DATA")
|
|
12
|
+
if kite_data:
|
|
13
|
+
log_dir = Path(kite_data) / "cli_logs"
|
|
14
|
+
else:
|
|
15
|
+
home = Path.home()
|
|
16
|
+
log_dir = home / ".kite" / "data" / "cli_logs"
|
|
17
|
+
|
|
18
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
return log_dir / "operations.jsonl"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log_operation(operation: str, details: dict, success: bool = True, error: str = None):
|
|
23
|
+
"""记录操作日志"""
|
|
24
|
+
log_entry = {
|
|
25
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
26
|
+
"operation": operation,
|
|
27
|
+
"details": details,
|
|
28
|
+
"success": success,
|
|
29
|
+
"error": error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
log_file = get_log_file()
|
|
34
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
35
|
+
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
# 日志记录失败不应该影响主流程
|
|
38
|
+
print(f"[Warning] 记录操作日志失败: {e}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_operations(limit: int = 50) -> list:
|
|
42
|
+
"""读取最近的操作日志"""
|
|
43
|
+
log_file = get_log_file()
|
|
44
|
+
if not log_file.exists():
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
49
|
+
lines = f.readlines()
|
|
50
|
+
|
|
51
|
+
# 返回最后 N 条
|
|
52
|
+
operations = []
|
|
53
|
+
for line in lines[-limit:]:
|
|
54
|
+
try:
|
|
55
|
+
operations.append(json.loads(line.strip()))
|
|
56
|
+
except json.JSONDecodeError:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
return operations
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"[Error] 读取操作日志失败: {e}")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_last_operation(operation_type: str = None) -> dict:
|
|
66
|
+
"""获取最后一次操作"""
|
|
67
|
+
operations = read_operations(limit=100)
|
|
68
|
+
|
|
69
|
+
if operation_type:
|
|
70
|
+
# 查找指定类型的最后一次操作
|
|
71
|
+
for op in reversed(operations):
|
|
72
|
+
if op.get("operation") == operation_type:
|
|
73
|
+
return op
|
|
74
|
+
return None
|
|
75
|
+
else:
|
|
76
|
+
# 返回最后一次操作
|
|
77
|
+
return operations[-1] if operations else None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""路径处理工具"""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_install_path(location: str, module_name: str) -> Path:
|
|
7
|
+
"""获取安装路径"""
|
|
8
|
+
if location == "dev":
|
|
9
|
+
# 开发环境: {KITE_PROJECT}/extensions/
|
|
10
|
+
kite_project = os.environ.get("KITE_PROJECT")
|
|
11
|
+
if not kite_project:
|
|
12
|
+
# 如果没有环境变量,使用当前目录
|
|
13
|
+
kite_project = Path.cwd()
|
|
14
|
+
return Path(kite_project) / "extensions" / module_name
|
|
15
|
+
|
|
16
|
+
elif location == "local":
|
|
17
|
+
# 本地实例: {KITE_INSTANCE_DIR}/extensions/
|
|
18
|
+
kite_instance = os.environ.get("KITE_INSTANCE_DIR")
|
|
19
|
+
if not kite_instance:
|
|
20
|
+
# 默认路径
|
|
21
|
+
home = Path.home()
|
|
22
|
+
kite_instance = home / ".kite" / "workspace" / "Kite"
|
|
23
|
+
return Path(kite_instance) / "extensions" / module_name
|
|
24
|
+
|
|
25
|
+
elif location == "global":
|
|
26
|
+
# 全局共享: {KITE_MODULES}
|
|
27
|
+
kite_modules = os.environ.get("KITE_MODULES")
|
|
28
|
+
if not kite_modules:
|
|
29
|
+
# 默认路径
|
|
30
|
+
home = Path.home()
|
|
31
|
+
kite_modules = home / ".kite" / "modules"
|
|
32
|
+
return Path(kite_modules) / module_name
|
|
33
|
+
|
|
34
|
+
raise ValueError(f"未知的安装位置: {location}")
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""版本号解析和比较工具
|
|
2
|
+
|
|
3
|
+
支持 semver 版本号解析、比较和匹配。
|
|
4
|
+
"""
|
|
5
|
+
import re
|
|
6
|
+
from typing import Tuple, Optional, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Version:
|
|
10
|
+
"""版本号类"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, version_string: str):
|
|
13
|
+
"""解析版本号字符串"""
|
|
14
|
+
self.original = version_string
|
|
15
|
+
self.major = 0
|
|
16
|
+
self.minor = 0
|
|
17
|
+
self.patch = 0
|
|
18
|
+
self.prerelease = None
|
|
19
|
+
self.build = None
|
|
20
|
+
|
|
21
|
+
self._parse(version_string)
|
|
22
|
+
|
|
23
|
+
def _parse(self, version_string: str):
|
|
24
|
+
"""解析版本号"""
|
|
25
|
+
# 移除 'v' 前缀
|
|
26
|
+
version_string = version_string.lstrip('v')
|
|
27
|
+
|
|
28
|
+
# 分离 prerelease 和 build
|
|
29
|
+
if '+' in version_string:
|
|
30
|
+
version_string, self.build = version_string.split('+', 1)
|
|
31
|
+
|
|
32
|
+
if '-' in version_string:
|
|
33
|
+
version_string, self.prerelease = version_string.split('-', 1)
|
|
34
|
+
|
|
35
|
+
# 解析主版本号
|
|
36
|
+
parts = version_string.split('.')
|
|
37
|
+
if len(parts) >= 1:
|
|
38
|
+
self.major = int(parts[0]) if parts[0].isdigit() else 0
|
|
39
|
+
if len(parts) >= 2:
|
|
40
|
+
self.minor = int(parts[1]) if parts[1].isdigit() else 0
|
|
41
|
+
if len(parts) >= 3:
|
|
42
|
+
self.patch = int(parts[2]) if parts[2].isdigit() else 0
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
"""转为字符串"""
|
|
46
|
+
version = f"{self.major}.{self.minor}.{self.patch}"
|
|
47
|
+
if self.prerelease:
|
|
48
|
+
version += f"-{self.prerelease}"
|
|
49
|
+
if self.build:
|
|
50
|
+
version += f"+{self.build}"
|
|
51
|
+
return version
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return f"Version('{self}')"
|
|
55
|
+
|
|
56
|
+
def __eq__(self, other):
|
|
57
|
+
"""相等比较"""
|
|
58
|
+
if not isinstance(other, Version):
|
|
59
|
+
other = Version(str(other))
|
|
60
|
+
return (self.major, self.minor, self.patch, self.prerelease) == \
|
|
61
|
+
(other.major, other.minor, other.patch, other.prerelease)
|
|
62
|
+
|
|
63
|
+
def __lt__(self, other):
|
|
64
|
+
"""小于比较"""
|
|
65
|
+
if not isinstance(other, Version):
|
|
66
|
+
other = Version(str(other))
|
|
67
|
+
|
|
68
|
+
# 比较主版本号
|
|
69
|
+
if self.major != other.major:
|
|
70
|
+
return self.major < other.major
|
|
71
|
+
if self.minor != other.minor:
|
|
72
|
+
return self.minor < other.minor
|
|
73
|
+
if self.patch != other.patch:
|
|
74
|
+
return self.patch < other.patch
|
|
75
|
+
|
|
76
|
+
# 比较 prerelease
|
|
77
|
+
if self.prerelease is None and other.prerelease is None:
|
|
78
|
+
return False
|
|
79
|
+
if self.prerelease is None:
|
|
80
|
+
return False # 正式版 > 预发布版
|
|
81
|
+
if other.prerelease is None:
|
|
82
|
+
return True
|
|
83
|
+
return self.prerelease < other.prerelease
|
|
84
|
+
|
|
85
|
+
def __le__(self, other):
|
|
86
|
+
return self == other or self < other
|
|
87
|
+
|
|
88
|
+
def __gt__(self, other):
|
|
89
|
+
return not self <= other
|
|
90
|
+
|
|
91
|
+
def __ge__(self, other):
|
|
92
|
+
return not self < other
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class VersionSpec:
|
|
96
|
+
"""版本规范类"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, spec: str):
|
|
99
|
+
"""解析版本规范"""
|
|
100
|
+
self.original = spec
|
|
101
|
+
self.operator = None
|
|
102
|
+
self.version = None
|
|
103
|
+
self.range_start = None
|
|
104
|
+
self.range_end = None
|
|
105
|
+
|
|
106
|
+
self._parse(spec)
|
|
107
|
+
|
|
108
|
+
def _parse(self, spec: str):
|
|
109
|
+
"""解析版本规范"""
|
|
110
|
+
spec = spec.strip()
|
|
111
|
+
|
|
112
|
+
# latest
|
|
113
|
+
if spec == "latest":
|
|
114
|
+
self.operator = "latest"
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# 范围: 1.0.0 - 2.0.0
|
|
118
|
+
if ' - ' in spec:
|
|
119
|
+
parts = spec.split(' - ')
|
|
120
|
+
if len(parts) == 2:
|
|
121
|
+
self.operator = "range"
|
|
122
|
+
self.range_start = Version(parts[0].strip())
|
|
123
|
+
self.range_end = Version(parts[1].strip())
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
# ^1.2.3 (兼容版本)
|
|
127
|
+
if spec.startswith('^'):
|
|
128
|
+
self.operator = "^"
|
|
129
|
+
self.version = Version(spec[1:])
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# ~1.2.3 (近似版本)
|
|
133
|
+
if spec.startswith('~'):
|
|
134
|
+
self.operator = "~"
|
|
135
|
+
self.version = Version(spec[1:])
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# >=1.2.3
|
|
139
|
+
if spec.startswith('>='):
|
|
140
|
+
self.operator = ">="
|
|
141
|
+
self.version = Version(spec[2:])
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# <=1.2.3
|
|
145
|
+
if spec.startswith('<='):
|
|
146
|
+
self.operator = "<="
|
|
147
|
+
self.version = Version(spec[2:])
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
# >1.2.3
|
|
151
|
+
if spec.startswith('>'):
|
|
152
|
+
self.operator = ">"
|
|
153
|
+
self.version = Version(spec[1:])
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# <1.2.3
|
|
157
|
+
if spec.startswith('<'):
|
|
158
|
+
self.operator = "<"
|
|
159
|
+
self.version = Version(spec[1:])
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# =1.2.3 或 1.2.3 (精确版本)
|
|
163
|
+
if spec.startswith('='):
|
|
164
|
+
spec = spec[1:]
|
|
165
|
+
self.operator = "="
|
|
166
|
+
self.version = Version(spec)
|
|
167
|
+
|
|
168
|
+
def match(self, version: Version) -> bool:
|
|
169
|
+
"""检查版本是否匹配规范"""
|
|
170
|
+
if not isinstance(version, Version):
|
|
171
|
+
version = Version(str(version))
|
|
172
|
+
|
|
173
|
+
if self.operator == "latest":
|
|
174
|
+
return True # latest 匹配任何版本,由调用者选择最新
|
|
175
|
+
|
|
176
|
+
if self.operator == "=":
|
|
177
|
+
return version == self.version
|
|
178
|
+
|
|
179
|
+
if self.operator == ">":
|
|
180
|
+
return version > self.version
|
|
181
|
+
|
|
182
|
+
if self.operator == ">=":
|
|
183
|
+
return version >= self.version
|
|
184
|
+
|
|
185
|
+
if self.operator == "<":
|
|
186
|
+
return version < self.version
|
|
187
|
+
|
|
188
|
+
if self.operator == "<=":
|
|
189
|
+
return version <= self.version
|
|
190
|
+
|
|
191
|
+
if self.operator == "^":
|
|
192
|
+
# ^1.2.3 匹配 >=1.2.3 且 <2.0.0
|
|
193
|
+
if self.version.major == 0:
|
|
194
|
+
# ^0.x.y 匹配 >=0.x.y 且 <0.(x+1).0
|
|
195
|
+
return version >= self.version and \
|
|
196
|
+
version.major == 0 and \
|
|
197
|
+
version.minor == self.version.minor
|
|
198
|
+
else:
|
|
199
|
+
return version >= self.version and \
|
|
200
|
+
version.major == self.version.major
|
|
201
|
+
|
|
202
|
+
if self.operator == "~":
|
|
203
|
+
# ~1.2.3 匹配 >=1.2.3 且 <1.3.0
|
|
204
|
+
return version >= self.version and \
|
|
205
|
+
version.major == self.version.major and \
|
|
206
|
+
version.minor == self.version.minor
|
|
207
|
+
|
|
208
|
+
if self.operator == "range":
|
|
209
|
+
return self.range_start <= version <= self.range_end
|
|
210
|
+
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parse_version_spec(spec: str) -> Tuple[Optional[str], Optional[str]]:
|
|
215
|
+
"""解析版本规范字符串
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
spec: 版本规范,如 "pkg@1.2.0", "pkg@^1.0.0", "pkg@latest", "pkg"
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
(package_name, version_spec) 元组
|
|
222
|
+
"""
|
|
223
|
+
if '@' not in spec:
|
|
224
|
+
return spec, None
|
|
225
|
+
|
|
226
|
+
parts = spec.rsplit('@', 1)
|
|
227
|
+
if len(parts) == 2:
|
|
228
|
+
return parts[0], parts[1]
|
|
229
|
+
|
|
230
|
+
return spec, None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def compare_versions(v1: str, v2: str) -> int:
|
|
234
|
+
"""比较两个版本号
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
v1: 版本号 1
|
|
238
|
+
v2: 版本号 2
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
-1 if v1 < v2
|
|
242
|
+
0 if v1 == v2
|
|
243
|
+
1 if v1 > v2
|
|
244
|
+
"""
|
|
245
|
+
version1 = Version(v1)
|
|
246
|
+
version2 = Version(v2)
|
|
247
|
+
|
|
248
|
+
if version1 < version2:
|
|
249
|
+
return -1
|
|
250
|
+
elif version1 > version2:
|
|
251
|
+
return 1
|
|
252
|
+
else:
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def match_version(version: str, spec: str) -> bool:
|
|
257
|
+
"""检查版本是否匹配规范
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
version: 版本号,如 "1.2.3"
|
|
261
|
+
spec: 版本规范,如 "^1.0.0", ">=1.2.0", "1.2.3"
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
是否匹配
|
|
265
|
+
"""
|
|
266
|
+
v = Version(version)
|
|
267
|
+
s = VersionSpec(spec)
|
|
268
|
+
return s.match(v)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_latest_version(versions: List[str]) -> Optional[str]:
|
|
272
|
+
"""从版本列表中获取最新版本
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
versions: 版本号列表
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
最新版本号,如果列表为空则返回 None
|
|
279
|
+
"""
|
|
280
|
+
if not versions:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
version_objects = [Version(v) for v in versions]
|
|
284
|
+
latest = max(version_objects)
|
|
285
|
+
return str(latest)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def filter_versions(versions: List[str], spec: str) -> List[str]:
|
|
289
|
+
"""根据版本规范过滤版本列表
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
versions: 版本号列表
|
|
293
|
+
spec: 版本规范
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
匹配的版本列表,按版本号降序排列
|
|
297
|
+
"""
|
|
298
|
+
version_spec = VersionSpec(spec)
|
|
299
|
+
matched = []
|
|
300
|
+
|
|
301
|
+
for v in versions:
|
|
302
|
+
version = Version(v)
|
|
303
|
+
if version_spec.match(version):
|
|
304
|
+
matched.append(v)
|
|
305
|
+
|
|
306
|
+
# 按版本号降序排列
|
|
307
|
+
matched.sort(key=lambda x: Version(x), reverse=True)
|
|
308
|
+
return matched
|