aukeys-opscli 0.0.6__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.
- aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
- aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
- aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
- aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
- opscli/__init__.py +9 -0
- opscli/amazon/__init__.py +13 -0
- opscli/amazon/cli.py +5 -0
- opscli/amazon/client.py +5 -0
- opscli/amazon/commands/__init__.py +1 -0
- opscli/amazon/commands/cli.py +200 -0
- opscli/amazon/domain/__init__.py +25 -0
- opscli/amazon/domain/exceptions.py +73 -0
- opscli/amazon/domain/models.py +74 -0
- opscli/amazon/exceptions.py +21 -0
- opscli/amazon/manager.py +5 -0
- opscli/amazon/models.py +5 -0
- opscli/amazon/parser.py +5 -0
- opscli/amazon/scraper.py +5 -0
- opscli/amazon/scraping/__init__.py +12 -0
- opscli/amazon/scraping/parser.py +66 -0
- opscli/amazon/scraping/scraper.py +315 -0
- opscli/amazon/services/__init__.py +5 -0
- opscli/amazon/services/manager.py +105 -0
- opscli/amazon/transport/__init__.py +5 -0
- opscli/amazon/transport/client.py +77 -0
- opscli/auth/__init__.py +79 -0
- opscli/auth/cli.py +255 -0
- opscli/auth/commands/__init__.py +1 -0
- opscli/auth/commands/cli.py +5 -0
- opscli/auth/config.py +55 -0
- opscli/auth/core/__init__.py +1 -0
- opscli/auth/core/device_flow.py +80 -0
- opscli/auth/core/system_registry.py +89 -0
- opscli/auth/core/token_manager.py +167 -0
- opscli/auth/domain/__init__.py +23 -0
- opscli/auth/domain/exceptions.py +33 -0
- opscli/auth/exceptions.py +23 -0
- opscli/auth/storage/__init__.py +1 -0
- opscli/auth/storage/credential_store.py +163 -0
- opscli/auth/storage/crypto.py +67 -0
- opscli/cli.py +36 -0
- opscli/config.py +3 -0
- opscli/query/__init__.py +6 -0
- opscli/query/cli.py +5 -0
- opscli/query/client.py +5 -0
- opscli/query/commands/__init__.py +1 -0
- opscli/query/commands/cli.py +140 -0
- opscli/query/domain/__init__.py +23 -0
- opscli/query/domain/exceptions.py +73 -0
- opscli/query/domain/models.py +21 -0
- opscli/query/exceptions.py +21 -0
- opscli/query/manager.py +5 -0
- opscli/query/models.py +5 -0
- opscli/query/services/__init__.py +5 -0
- opscli/query/services/manager.py +373 -0
- opscli/query/transport/__init__.py +5 -0
- opscli/query/transport/client.py +57 -0
- opscli/skills/__init__.py +37 -0
- opscli/skills/cli.py +5 -0
- opscli/skills/commands/__init__.py +1 -0
- opscli/skills/commands/cli.py +389 -0
- opscli/skills/detector.py +5 -0
- opscli/skills/discovery/__init__.py +5 -0
- opscli/skills/discovery/detector.py +225 -0
- opscli/skills/domain/__init__.py +23 -0
- opscli/skills/domain/exceptions.py +51 -0
- opscli/skills/domain/models.py +144 -0
- opscli/skills/exceptions.py +5 -0
- opscli/skills/manager.py +5 -0
- opscli/skills/models.py +19 -0
- opscli/skills/services/__init__.py +5 -0
- opscli/skills/services/manager.py +276 -0
- opscli/skills/sync/__init__.py +5 -0
- opscli/skills/sync/updater.py +274 -0
- opscli/skills/templates/ops-amazon/SKILL.md +181 -0
- opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
- opscli/skills/templates/ops-auth/SKILL.md +466 -0
- opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
- opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
- opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
- opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
- opscli/skills/templates/ops-dataset-query/references//346/225/260/346/215/256/346/237/245/350/257/242/346/234/215/345/212/241/345/274/200/345/217/221/350/257/264/346/230/216/346/226/207/346/241/243.md +1126 -0
- opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
- opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
- opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
- opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
- opscli/skills/templates/ops-skills/SKILL.md +494 -0
- opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
- opscli/skills/updater.py +5 -0
- opscli/version.py +19 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""skills 模块异常定义。
|
|
2
|
+
|
|
3
|
+
统一封装对外错误对象,保证 CLI JSON 输出结构稳定。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SkillsError(Exception):
|
|
10
|
+
"""skills 模块统一异常基类。"""
|
|
11
|
+
|
|
12
|
+
def to_dict(self) -> dict[str, object]:
|
|
13
|
+
"""转换为稳定的错误对象,便于 CLI 直接输出 JSON。"""
|
|
14
|
+
return {
|
|
15
|
+
"type": self.__class__.__name__,
|
|
16
|
+
"message": str(self),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SkillRemoteError(SkillsError):
|
|
21
|
+
"""远端 Skill 接口异常。"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
message: str,
|
|
26
|
+
*,
|
|
27
|
+
endpoint: str | None = None,
|
|
28
|
+
status_code: int | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.endpoint = endpoint
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, object]:
|
|
35
|
+
"""输出附带远端定位信息的错误对象。"""
|
|
36
|
+
payload = super().to_dict()
|
|
37
|
+
if self.endpoint is not None:
|
|
38
|
+
payload["endpoint"] = self.endpoint
|
|
39
|
+
if self.status_code is not None:
|
|
40
|
+
payload["status_code"] = self.status_code
|
|
41
|
+
return payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def error_to_dict(exc: Exception) -> dict[str, object]:
|
|
45
|
+
"""将任意异常转换为稳定的错误对象。"""
|
|
46
|
+
if isinstance(exc, SkillsError):
|
|
47
|
+
return exc.to_dict()
|
|
48
|
+
return {
|
|
49
|
+
"type": exc.__class__.__name__,
|
|
50
|
+
"message": str(exc),
|
|
51
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Skills 模块的数据模型定义。
|
|
2
|
+
|
|
3
|
+
包含 Skill 生命周期中各阶段的数据结构:发现记录、安装结果、升级结果。
|
|
4
|
+
所有 dataclass 均提供 to_dict() 方法用于 JSON 序列化输出。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def runtime_to_tool_name(runtime: str) -> str:
|
|
14
|
+
"""将内部 runtime 标识转换为对外稳定的工具名。"""
|
|
15
|
+
mapping = {
|
|
16
|
+
"claude": "claude-code",
|
|
17
|
+
"openclaw": "openclaw",
|
|
18
|
+
}
|
|
19
|
+
return mapping.get(runtime, runtime)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SkillRecord:
|
|
24
|
+
"""已发现的 Skill 记录。
|
|
25
|
+
|
|
26
|
+
由 SkillDetector 扫描磁盘目录后生成,描述一个已安装 Skill 的基本信息。
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str # Skill 名称,对应目录名
|
|
30
|
+
version: str # 当前版本号,读取自 data/VERSION.json
|
|
31
|
+
runtime: str # 运行时类型:claude 或 openclaw
|
|
32
|
+
root: Path # Skill 根目录绝对路径
|
|
33
|
+
version_file: Path # VERSION.json 文件绝对路径
|
|
34
|
+
|
|
35
|
+
def to_dict(self) -> dict:
|
|
36
|
+
"""转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
|
|
37
|
+
data = asdict(self)
|
|
38
|
+
data["runtime"] = runtime_to_tool_name(self.runtime)
|
|
39
|
+
data["root"] = str(self.root)
|
|
40
|
+
data["version_file"] = str(self.version_file)
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SkillInstallResult:
|
|
46
|
+
"""Skill 安装操作的结果。
|
|
47
|
+
|
|
48
|
+
由 SkillsManager.install() 返回,记录安装目标路径及是否覆盖了已有安装。
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str # Skill 名称
|
|
52
|
+
runtime: str # 目标运行时类型
|
|
53
|
+
target_dir: Path # 安装目标目录
|
|
54
|
+
version: str # 安装后的版本号
|
|
55
|
+
replaced: bool # 是否覆盖了已有安装
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
"""转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
|
|
59
|
+
data = asdict(self)
|
|
60
|
+
data["runtime"] = runtime_to_tool_name(self.runtime)
|
|
61
|
+
data["target_dir"] = str(self.target_dir)
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class SkillBatchInstallResult:
|
|
67
|
+
"""批量安装结果。"""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
installs: list[SkillInstallResult]
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
return {
|
|
74
|
+
"name": self.name,
|
|
75
|
+
"version": self.installs[0].version if self.installs else "unknown",
|
|
76
|
+
"installed_paths": [
|
|
77
|
+
{
|
|
78
|
+
"tool": runtime_to_tool_name(item.runtime),
|
|
79
|
+
"path": str(item.target_dir),
|
|
80
|
+
"replaced": item.replaced,
|
|
81
|
+
}
|
|
82
|
+
for item in self.installs
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class SkillUpgradeResult:
|
|
89
|
+
"""Skill 升级操作的结果。
|
|
90
|
+
|
|
91
|
+
由 SkillsUpdater.upgrade_dataset_fields() 返回,记录版本变化和是否实际更新。
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
name: str # Skill 名称
|
|
95
|
+
from_version: str # 升级前版本号
|
|
96
|
+
to_version: str # 升级后版本号(未更新时与 from_version 相同)
|
|
97
|
+
runtime: str # 所属运行时
|
|
98
|
+
target_dir: Path # Skill 根目录
|
|
99
|
+
updated: bool # 是否实际执行了更新
|
|
100
|
+
field_count: int = 0 # 当前用户可见字段数
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict:
|
|
103
|
+
"""转换为可 JSON 序列化的字典,Path 字段转为字符串。"""
|
|
104
|
+
data = asdict(self)
|
|
105
|
+
data["runtime"] = runtime_to_tool_name(self.runtime)
|
|
106
|
+
data["target_dir"] = str(self.target_dir)
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class SkillBatchUpgradeResult:
|
|
112
|
+
"""批量升级结果。"""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
results: list[SkillUpgradeResult]
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict:
|
|
118
|
+
updated = self._group_results(updated=True)
|
|
119
|
+
already_latest = self._group_results(updated=False)
|
|
120
|
+
return {
|
|
121
|
+
"updated": updated,
|
|
122
|
+
"already_latest": already_latest,
|
|
123
|
+
"failed": [],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def _group_results(self, *, updated: bool) -> list[dict]:
|
|
127
|
+
grouped: dict[tuple[str, str, str, int], dict] = {}
|
|
128
|
+
for item in self.results:
|
|
129
|
+
if item.updated is not updated:
|
|
130
|
+
continue
|
|
131
|
+
key = (item.name, item.from_version, item.to_version, item.field_count)
|
|
132
|
+
group = grouped.setdefault(
|
|
133
|
+
key,
|
|
134
|
+
{
|
|
135
|
+
"name": item.name,
|
|
136
|
+
"from_version": item.from_version,
|
|
137
|
+
"to_version": item.to_version,
|
|
138
|
+
"field_count": item.field_count,
|
|
139
|
+
"tools": [],
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
group["tools"].append(runtime_to_tool_name(item.runtime))
|
|
143
|
+
|
|
144
|
+
return list(grouped.values())
|
opscli/skills/manager.py
ADDED
opscli/skills/models.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""skills models 兼容导出。"""
|
|
2
|
+
|
|
3
|
+
from opscli.skills.domain.models import (
|
|
4
|
+
SkillBatchInstallResult,
|
|
5
|
+
SkillBatchUpgradeResult,
|
|
6
|
+
SkillInstallResult,
|
|
7
|
+
SkillRecord,
|
|
8
|
+
SkillUpgradeResult,
|
|
9
|
+
runtime_to_tool_name,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"runtime_to_tool_name",
|
|
14
|
+
"SkillRecord",
|
|
15
|
+
"SkillInstallResult",
|
|
16
|
+
"SkillBatchInstallResult",
|
|
17
|
+
"SkillUpgradeResult",
|
|
18
|
+
"SkillBatchUpgradeResult",
|
|
19
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Skill 生命周期管理器。
|
|
2
|
+
|
|
3
|
+
提供 Skill 的安装、列表查询、状态检查、升级等核心业务逻辑。
|
|
4
|
+
是 CLI 层(cli.py)和底层组件(detector/updater)之间的中间协调层。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from opscli.skills.discovery.detector import SkillDetector
|
|
14
|
+
from opscli.skills.domain.exceptions import error_to_dict
|
|
15
|
+
from opscli.skills.domain.models import (
|
|
16
|
+
SkillBatchInstallResult,
|
|
17
|
+
SkillBatchUpgradeResult,
|
|
18
|
+
SkillInstallResult,
|
|
19
|
+
SkillRecord,
|
|
20
|
+
SkillUpgradeResult,
|
|
21
|
+
runtime_to_tool_name,
|
|
22
|
+
)
|
|
23
|
+
from opscli.skills.sync.updater import SkillsUpdater
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SkillsManager:
|
|
27
|
+
"""Skill 管理器,协调检测器和更新器完成 Skill 生命周期管理。"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.detector = SkillDetector() # 负责 Skill 发现
|
|
31
|
+
self.updater = SkillsUpdater() # 负责远端数据拉取和升级
|
|
32
|
+
self.templates_dir = Path(__file__).parent.parent / "templates" # 内置 Skill 模板目录
|
|
33
|
+
|
|
34
|
+
def list_templates(self) -> list[dict]:
|
|
35
|
+
"""扫描内置模板目录,返回可安装的 Skill 列表。
|
|
36
|
+
|
|
37
|
+
每项包含 name、version、description(从 SKILL.md 首段提取)。
|
|
38
|
+
"""
|
|
39
|
+
templates: list[dict] = []
|
|
40
|
+
if not self.templates_dir.exists():
|
|
41
|
+
return templates
|
|
42
|
+
for skill_dir in sorted(p for p in self.templates_dir.iterdir() if p.is_dir()):
|
|
43
|
+
version_file = skill_dir / "data" / "VERSION.json"
|
|
44
|
+
if not version_file.exists():
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
payload = json.loads(version_file.read_text(encoding="utf-8"))
|
|
48
|
+
except Exception:
|
|
49
|
+
continue
|
|
50
|
+
version = str(payload.get("version", "unknown"))
|
|
51
|
+
description = self._extract_skill_description(skill_dir)
|
|
52
|
+
templates.append({"name": skill_dir.name, "version": version, "description": description})
|
|
53
|
+
return templates
|
|
54
|
+
|
|
55
|
+
def _extract_skill_description(self, skill_dir: Path) -> str:
|
|
56
|
+
"""从 SKILL.md 提取 Skill 简介(标题后第一个非空行)。
|
|
57
|
+
|
|
58
|
+
自动跳过 YAML frontmatter(--- 包裹区域)。
|
|
59
|
+
"""
|
|
60
|
+
skill_md = skill_dir / "SKILL.md"
|
|
61
|
+
if not skill_md.exists():
|
|
62
|
+
return ""
|
|
63
|
+
try:
|
|
64
|
+
lines = skill_md.read_text(encoding="utf-8").splitlines()
|
|
65
|
+
except Exception:
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
# 跳过 YAML frontmatter(以 --- 开头和结尾的区域)
|
|
69
|
+
in_frontmatter = False
|
|
70
|
+
content_lines: list[str] = []
|
|
71
|
+
for line in lines:
|
|
72
|
+
stripped = line.strip()
|
|
73
|
+
if stripped == "---" and not content_lines:
|
|
74
|
+
in_frontmatter = not in_frontmatter
|
|
75
|
+
continue
|
|
76
|
+
if in_frontmatter:
|
|
77
|
+
continue
|
|
78
|
+
content_lines.append(line)
|
|
79
|
+
|
|
80
|
+
# 从正文内容中提取:跳过标题行(# 开头),取第一个非空的普通文本行
|
|
81
|
+
skip_title = True
|
|
82
|
+
for line in content_lines:
|
|
83
|
+
stripped = line.strip()
|
|
84
|
+
if skip_title and stripped.startswith("#"):
|
|
85
|
+
skip_title = False
|
|
86
|
+
continue
|
|
87
|
+
if stripped and not stripped.startswith("#") and not stripped.startswith(">"):
|
|
88
|
+
return stripped[:30] + ("…" if len(stripped) > 30 else "")
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def list_skills(self, skills_dir: str | None = None, cwd: Path | None = None) -> list[SkillRecord]:
|
|
92
|
+
"""列出所有已安装的 Skill,委托给 SkillDetector.discover()。"""
|
|
93
|
+
return self.detector.discover(skills_dir=skills_dir, cwd=cwd)
|
|
94
|
+
|
|
95
|
+
def install(
|
|
96
|
+
self,
|
|
97
|
+
skill_name: str,
|
|
98
|
+
*,
|
|
99
|
+
skills_dir: str | None = None,
|
|
100
|
+
cwd: Path | None = None,
|
|
101
|
+
runtime: str | list[str] | None = None,
|
|
102
|
+
force: bool = False,
|
|
103
|
+
) -> SkillBatchInstallResult:
|
|
104
|
+
"""从内置模板安装 Skill 到目标目录。
|
|
105
|
+
|
|
106
|
+
流程:
|
|
107
|
+
1. 检查模板是否存在(只支持内置 Skill)
|
|
108
|
+
2. 确定安装目标路径(显式指定 或 自动检测)
|
|
109
|
+
3. 若目标已存在且未指定 force,则报错
|
|
110
|
+
4. 将模板目录整体复制到目标位置
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
skill_name: 要安装的 Skill 名称(如 "ops-dataset-query")
|
|
114
|
+
skills_dir: 显式指定安装根目录
|
|
115
|
+
cwd: 当前工作目录,用于自动检测安装目标
|
|
116
|
+
runtime: 运行时类型(claude/openclaw)
|
|
117
|
+
force: 是否覆盖已存在的安装
|
|
118
|
+
"""
|
|
119
|
+
template_dir = self.templates_dir / skill_name
|
|
120
|
+
if not template_dir.exists():
|
|
121
|
+
raise ValueError(f"不支持的内置 Skill: {skill_name}")
|
|
122
|
+
|
|
123
|
+
current = cwd or Path.cwd()
|
|
124
|
+
if skills_dir:
|
|
125
|
+
targets = [(str(runtime or "custom"), Path(skills_dir).expanduser())]
|
|
126
|
+
else:
|
|
127
|
+
runtime_list = self._normalize_runtime_arg(runtime)
|
|
128
|
+
targets = self.detector.detect_install_targets(
|
|
129
|
+
cwd=current,
|
|
130
|
+
preferred_runtimes=runtime_list,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
installs: list[SkillInstallResult] = []
|
|
134
|
+
for target_runtime, target_root in targets:
|
|
135
|
+
target_root.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
target_dir = target_root / skill_name
|
|
137
|
+
|
|
138
|
+
replaced = False
|
|
139
|
+
if target_dir.exists():
|
|
140
|
+
if not force:
|
|
141
|
+
raise ValueError(f"Skill 已存在: {target_dir}")
|
|
142
|
+
shutil.rmtree(target_dir)
|
|
143
|
+
replaced = True
|
|
144
|
+
|
|
145
|
+
shutil.copytree(template_dir, target_dir)
|
|
146
|
+
installs.append(
|
|
147
|
+
SkillInstallResult(
|
|
148
|
+
name=skill_name,
|
|
149
|
+
runtime=target_runtime,
|
|
150
|
+
target_dir=target_dir,
|
|
151
|
+
version=self._read_version(target_dir),
|
|
152
|
+
replaced=replaced,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return SkillBatchInstallResult(name=skill_name, installs=installs)
|
|
157
|
+
|
|
158
|
+
def status(self, skills_dir: str | None = None, cwd: Path | None = None) -> dict:
|
|
159
|
+
"""查询 Skill 安装状态,包含本地信息和远端版本对比。
|
|
160
|
+
|
|
161
|
+
返回结构:
|
|
162
|
+
- installed: 本地已安装 Skill 列表(附带远端版本和是否有更新)
|
|
163
|
+
- remote_manifest: 远端 manifest 数据(可能为 None)
|
|
164
|
+
- remote_error: 远端请求失败时的错误信息
|
|
165
|
+
"""
|
|
166
|
+
records = self.list_skills(skills_dir=skills_dir, cwd=cwd)
|
|
167
|
+
|
|
168
|
+
# 获取远端 manifest(仅支持 ops-dataset-query)
|
|
169
|
+
remote_manifest = None
|
|
170
|
+
remote_summary = None
|
|
171
|
+
remote_error = None
|
|
172
|
+
try:
|
|
173
|
+
remote_summary = self.updater.build_remote_summary("ops-dataset-query")
|
|
174
|
+
remote_manifest = remote_summary.get("manifest")
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
remote_error = error_to_dict(exc)
|
|
177
|
+
|
|
178
|
+
remote_version = None
|
|
179
|
+
if remote_manifest:
|
|
180
|
+
remote_version = str(remote_manifest.get("version", "v0.0.0"))
|
|
181
|
+
|
|
182
|
+
# 为每个已安装 Skill 附带远端版本信息
|
|
183
|
+
enriched: list[dict] = []
|
|
184
|
+
for item in records:
|
|
185
|
+
row = item.to_dict()
|
|
186
|
+
if item.name == "ops-dataset-query" and remote_version:
|
|
187
|
+
row["remote_version"] = remote_version
|
|
188
|
+
row["has_update"] = self.updater.compare_versions(item.version, remote_version) < 0
|
|
189
|
+
else:
|
|
190
|
+
row["remote_version"] = None
|
|
191
|
+
row["has_update"] = False
|
|
192
|
+
enriched.append(row)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"skills": self._summarize_installed_skills(enriched),
|
|
196
|
+
"installed": enriched,
|
|
197
|
+
"remote_manifest": remote_manifest,
|
|
198
|
+
"remote_summary": remote_summary,
|
|
199
|
+
"remote_error": remote_error,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def upgrade(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
name: str = "ops-dataset-query",
|
|
206
|
+
skills_dir: str | None = None,
|
|
207
|
+
cwd: Path | None = None,
|
|
208
|
+
force: bool = False,
|
|
209
|
+
) -> SkillBatchUpgradeResult:
|
|
210
|
+
"""升级指定 Skill 到最新版本。
|
|
211
|
+
|
|
212
|
+
当前仅支持 ops-dataset-query 的升级。会先查找本地已安装的记录,
|
|
213
|
+
再委托给 SkillsUpdater 执行实际的远端数据拉取和文件替换。
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name: 要升级的 Skill 名称
|
|
217
|
+
skills_dir: 指定扫描目录
|
|
218
|
+
cwd: 当前工作目录
|
|
219
|
+
force: 是否强制升级(即使版本号相同也重新拉取)
|
|
220
|
+
"""
|
|
221
|
+
records = self.list_skills(skills_dir=skills_dir, cwd=cwd)
|
|
222
|
+
targets = [item for item in records if item.name == name]
|
|
223
|
+
if not targets:
|
|
224
|
+
raise ValueError(f"未找到已安装 Skill: {name}")
|
|
225
|
+
if name != "ops-dataset-query":
|
|
226
|
+
raise ValueError(f"暂不支持升级 Skill: {name}")
|
|
227
|
+
|
|
228
|
+
results = [
|
|
229
|
+
self.updater.upgrade_ops_dataset_query(target, force=force)
|
|
230
|
+
for target in targets
|
|
231
|
+
]
|
|
232
|
+
return SkillBatchUpgradeResult(name=name, results=results)
|
|
233
|
+
|
|
234
|
+
def _normalize_runtime_arg(self, runtime: str | list[str] | None) -> list[str] | None:
|
|
235
|
+
"""统一解析 runtime 参数。"""
|
|
236
|
+
if runtime is None:
|
|
237
|
+
return None
|
|
238
|
+
if isinstance(runtime, list):
|
|
239
|
+
return runtime
|
|
240
|
+
parts = [item.strip() for item in runtime.split(",") if item.strip()]
|
|
241
|
+
return parts or None
|
|
242
|
+
|
|
243
|
+
def _read_version(self, target_dir: Path) -> str:
|
|
244
|
+
"""从安装目录的 data/VERSION.json 中读取版本号。"""
|
|
245
|
+
version_file = target_dir / "data" / "VERSION.json"
|
|
246
|
+
payload = json.loads(version_file.read_text(encoding="utf-8"))
|
|
247
|
+
return str(payload.get("version", "unknown"))
|
|
248
|
+
|
|
249
|
+
def _summarize_installed_skills(self, records: list[dict]) -> list[dict]:
|
|
250
|
+
"""按 skill 名称聚合安装记录,生成稳定的对外 JSON 结构。"""
|
|
251
|
+
grouped: dict[str, dict] = {}
|
|
252
|
+
for item in records:
|
|
253
|
+
group = grouped.setdefault(
|
|
254
|
+
item["name"],
|
|
255
|
+
{
|
|
256
|
+
"name": item["name"],
|
|
257
|
+
"local_version": item["version"],
|
|
258
|
+
"remote_version": item["remote_version"],
|
|
259
|
+
"has_update": item["has_update"],
|
|
260
|
+
"installed_paths": [],
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if item["version"] != group["local_version"]:
|
|
265
|
+
group["local_version"] = max(str(group["local_version"]), str(item["version"]))
|
|
266
|
+
if item["remote_version"] and not group["remote_version"]:
|
|
267
|
+
group["remote_version"] = item["remote_version"]
|
|
268
|
+
group["has_update"] = group["has_update"] or item["has_update"]
|
|
269
|
+
group["installed_paths"].append(
|
|
270
|
+
{
|
|
271
|
+
"tool": runtime_to_tool_name(item["runtime"]),
|
|
272
|
+
"path": item["root"],
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return list(grouped.values())
|