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.
Files changed (92) hide show
  1. aukeys_opscli-0.0.6.dist-info/METADATA +15 -0
  2. aukeys_opscli-0.0.6.dist-info/RECORD +92 -0
  3. aukeys_opscli-0.0.6.dist-info/WHEEL +4 -0
  4. aukeys_opscli-0.0.6.dist-info/entry_points.txt +2 -0
  5. opscli/__init__.py +9 -0
  6. opscli/amazon/__init__.py +13 -0
  7. opscli/amazon/cli.py +5 -0
  8. opscli/amazon/client.py +5 -0
  9. opscli/amazon/commands/__init__.py +1 -0
  10. opscli/amazon/commands/cli.py +200 -0
  11. opscli/amazon/domain/__init__.py +25 -0
  12. opscli/amazon/domain/exceptions.py +73 -0
  13. opscli/amazon/domain/models.py +74 -0
  14. opscli/amazon/exceptions.py +21 -0
  15. opscli/amazon/manager.py +5 -0
  16. opscli/amazon/models.py +5 -0
  17. opscli/amazon/parser.py +5 -0
  18. opscli/amazon/scraper.py +5 -0
  19. opscli/amazon/scraping/__init__.py +12 -0
  20. opscli/amazon/scraping/parser.py +66 -0
  21. opscli/amazon/scraping/scraper.py +315 -0
  22. opscli/amazon/services/__init__.py +5 -0
  23. opscli/amazon/services/manager.py +105 -0
  24. opscli/amazon/transport/__init__.py +5 -0
  25. opscli/amazon/transport/client.py +77 -0
  26. opscli/auth/__init__.py +79 -0
  27. opscli/auth/cli.py +255 -0
  28. opscli/auth/commands/__init__.py +1 -0
  29. opscli/auth/commands/cli.py +5 -0
  30. opscli/auth/config.py +55 -0
  31. opscli/auth/core/__init__.py +1 -0
  32. opscli/auth/core/device_flow.py +80 -0
  33. opscli/auth/core/system_registry.py +89 -0
  34. opscli/auth/core/token_manager.py +167 -0
  35. opscli/auth/domain/__init__.py +23 -0
  36. opscli/auth/domain/exceptions.py +33 -0
  37. opscli/auth/exceptions.py +23 -0
  38. opscli/auth/storage/__init__.py +1 -0
  39. opscli/auth/storage/credential_store.py +163 -0
  40. opscli/auth/storage/crypto.py +67 -0
  41. opscli/cli.py +36 -0
  42. opscli/config.py +3 -0
  43. opscli/query/__init__.py +6 -0
  44. opscli/query/cli.py +5 -0
  45. opscli/query/client.py +5 -0
  46. opscli/query/commands/__init__.py +1 -0
  47. opscli/query/commands/cli.py +140 -0
  48. opscli/query/domain/__init__.py +23 -0
  49. opscli/query/domain/exceptions.py +73 -0
  50. opscli/query/domain/models.py +21 -0
  51. opscli/query/exceptions.py +21 -0
  52. opscli/query/manager.py +5 -0
  53. opscli/query/models.py +5 -0
  54. opscli/query/services/__init__.py +5 -0
  55. opscli/query/services/manager.py +373 -0
  56. opscli/query/transport/__init__.py +5 -0
  57. opscli/query/transport/client.py +57 -0
  58. opscli/skills/__init__.py +37 -0
  59. opscli/skills/cli.py +5 -0
  60. opscli/skills/commands/__init__.py +1 -0
  61. opscli/skills/commands/cli.py +389 -0
  62. opscli/skills/detector.py +5 -0
  63. opscli/skills/discovery/__init__.py +5 -0
  64. opscli/skills/discovery/detector.py +225 -0
  65. opscli/skills/domain/__init__.py +23 -0
  66. opscli/skills/domain/exceptions.py +51 -0
  67. opscli/skills/domain/models.py +144 -0
  68. opscli/skills/exceptions.py +5 -0
  69. opscli/skills/manager.py +5 -0
  70. opscli/skills/models.py +19 -0
  71. opscli/skills/services/__init__.py +5 -0
  72. opscli/skills/services/manager.py +276 -0
  73. opscli/skills/sync/__init__.py +5 -0
  74. opscli/skills/sync/updater.py +274 -0
  75. opscli/skills/templates/ops-amazon/SKILL.md +181 -0
  76. opscli/skills/templates/ops-amazon/data/VERSION.json +4 -0
  77. opscli/skills/templates/ops-auth/SKILL.md +466 -0
  78. opscli/skills/templates/ops-auth/data/VERSION.json +4 -0
  79. opscli/skills/templates/ops-dataset-query/SKILL.md +691 -0
  80. opscli/skills/templates/ops-dataset-query/data/VERSION.json +4 -0
  81. opscli/skills/templates/ops-dataset-query/data/dataset_fields.csv +1 -0
  82. opscli/skills/templates/ops-dataset-query/data/datasets.csv +1 -0
  83. opscli/skills/templates/ops-dataset-query/data/query_metadata.json +4 -0
  84. 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
  85. opscli/skills/templates/ops-dataset-query/scripts/core.py +140 -0
  86. opscli/skills/templates/ops-dataset-query/scripts/query.py +145 -0
  87. opscli/skills/templates/ops-dataset-query/scripts/search.py +36 -0
  88. opscli/skills/templates/ops-dataset-query/scripts/updater.py +106 -0
  89. opscli/skills/templates/ops-skills/SKILL.md +494 -0
  90. opscli/skills/templates/ops-skills/data/VERSION.json +4 -0
  91. opscli/skills/updater.py +5 -0
  92. opscli/version.py +19 -0
@@ -0,0 +1,373 @@
1
+ """query 模块业务编排层。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from opscli.query.domain.exceptions import DatasetNotFoundError, InvalidPayloadError, QueryMetadataNotReadyError
10
+ from opscli.query.domain.models import QueryMetadataResult
11
+ from opscli.query.transport.client import QueryClient
12
+ from opscli.skills.discovery.detector import SkillDetector
13
+
14
+
15
+ @dataclass
16
+ class _FieldSpec:
17
+ field_name: str
18
+ alias: str
19
+ aggregation: str | None = None
20
+
21
+
22
+ class QueryManager:
23
+ """协调本地 metadata 与远端 query 执行。"""
24
+
25
+ def __init__(self) -> None:
26
+ self.detector = SkillDetector()
27
+ self.client = QueryClient()
28
+ self.template_dir = Path(__file__).resolve().parent.parent.parent / "skills" / "templates" / "ops-dataset-query" / "data"
29
+
30
+ def metadata(
31
+ self,
32
+ *,
33
+ dataset_alias: str | None = None,
34
+ table_id: int | None = None,
35
+ skills_dir: str | None = None,
36
+ cwd: Path | None = None,
37
+ ) -> QueryMetadataResult:
38
+ """按数据集别名或 table_id 查询本地 query metadata。"""
39
+ if not dataset_alias and table_id is None:
40
+ raise InvalidPayloadError("必须提供 --dataset 或 --table-id")
41
+
42
+ payload = self._load_query_metadata(skills_dir=skills_dir, cwd=cwd)
43
+ datasets = payload.get("datasets") or []
44
+ fields = payload.get("fields") or []
45
+
46
+ matched = None
47
+ if dataset_alias:
48
+ matched = next((item for item in datasets if item.get("dataset_alias") == dataset_alias), None)
49
+ elif table_id is not None:
50
+ matched = next((item for item in datasets if int(item.get("table_id", -1)) == int(table_id)), None)
51
+
52
+ if matched is None:
53
+ needle = dataset_alias if dataset_alias else str(table_id)
54
+ raise DatasetNotFoundError(f"未找到目标数据集: {needle}")
55
+
56
+ matched_fields = [
57
+ item for item in fields
58
+ if int(item.get("table_id", -1)) == int(matched.get("table_id", -1))
59
+ ]
60
+ return QueryMetadataResult(dataset=matched, fields=matched_fields, source="local")
61
+
62
+ def run(self, *, payload_path: str) -> dict:
63
+ """读取本地 payload 文件并转发执行查询。"""
64
+ payload_file = Path(payload_path).expanduser()
65
+ if not payload_file.exists():
66
+ raise InvalidPayloadError(f"payload 文件不存在: {payload_file}")
67
+
68
+ try:
69
+ payload = json.loads(payload_file.read_text(encoding="utf-8"))
70
+ except Exception as exc:
71
+ raise InvalidPayloadError(f"payload 不是合法 JSON: {payload_file}") from exc
72
+
73
+ self._validate_payload(payload)
74
+ return self.client.cli_query(payload)
75
+
76
+ def build(
77
+ self,
78
+ *,
79
+ dataset_alias: str | None = None,
80
+ table_id: int | None = None,
81
+ dimensions: list[str] | None = None,
82
+ metrics: list[str] | None = None,
83
+ where_conditions: list[str] | None = None,
84
+ where_json: str | None = None,
85
+ where_file: str | None = None,
86
+ having_conditions: list[str] | None = None,
87
+ order_by: list[str] | None = None,
88
+ limit: int = 20,
89
+ offset: int = 0,
90
+ dry_run: bool = False,
91
+ skills_dir: str | None = None,
92
+ cwd: Path | None = None,
93
+ output_path: str | None = None,
94
+ data_comparison: str | None = None,
95
+ ) -> dict:
96
+ """基于简化参数构造标准 query payload。"""
97
+ if not dimensions and not metrics:
98
+ raise InvalidPayloadError("至少需要提供一个 --dimension 或 --metric")
99
+ where_source_count = sum(1 for item in (where_conditions, where_json, where_file) if item)
100
+ if where_source_count > 1:
101
+ raise InvalidPayloadError("--where、--where-json 和 --where-file 只能使用一种")
102
+
103
+ metadata = self.metadata(
104
+ dataset_alias=dataset_alias,
105
+ table_id=table_id,
106
+ skills_dir=skills_dir,
107
+ cwd=cwd,
108
+ )
109
+ dataset = metadata.dataset
110
+ field_index = {
111
+ str(item.get("field_name")): item
112
+ for item in metadata.fields
113
+ if item.get("field_name")
114
+ }
115
+
116
+ select_items: list[dict] = []
117
+ group_by: list[str] = []
118
+
119
+ for spec in dimensions or []:
120
+ item = self._parse_dimension_spec(spec)
121
+ self._ensure_field_exists(field_index, item.field_name)
122
+ select_items.append(
123
+ {
124
+ "expr": f"{dataset['dataset_alias']}.{item.field_name}",
125
+ "alias": item.alias,
126
+ }
127
+ )
128
+ group_by.append(item.alias)
129
+
130
+ for spec in metrics or []:
131
+ item = self._parse_metric_spec(spec)
132
+ self._ensure_field_exists(field_index, item.field_name)
133
+ select_row = {
134
+ "expr": f"{dataset['dataset_alias']}.{item.field_name}",
135
+ "alias": item.alias,
136
+ }
137
+ if item.aggregation:
138
+ select_row["aggregation"] = item.aggregation
139
+ select_items.append(select_row)
140
+
141
+ payload = {
142
+ "tableId": int(dataset["table_id"]),
143
+ "query": {
144
+ "select": select_items,
145
+ "groupBy": group_by,
146
+ "orderBy": self._build_order_by(order_by or []),
147
+ "limit": limit,
148
+ "offset": offset,
149
+ },
150
+ }
151
+ if dry_run:
152
+ payload["dryRun"] = True
153
+
154
+ where_payload = self._load_where_clause(
155
+ where_conditions=where_conditions or [],
156
+ where_json=where_json,
157
+ where_file=where_file,
158
+ dataset_alias=str(dataset["dataset_alias"]),
159
+ )
160
+ if where_payload is not None:
161
+ payload["query"]["where"] = where_payload
162
+
163
+ having_payload = self._build_having_clause(having_conditions or [])
164
+ if having_payload:
165
+ payload["query"]["having"] = having_payload
166
+
167
+ # 构造 dataComparison(数据对比)
168
+ if data_comparison:
169
+ payload["dataComparison"] = self._build_data_comparison(
170
+ data_comparison, dataset_alias=str(dataset["dataset_alias"]),
171
+ )
172
+
173
+ self._validate_payload(payload)
174
+ if output_path:
175
+ output_file = Path(output_path).expanduser()
176
+ output_file.parent.mkdir(parents=True, exist_ok=True)
177
+ output_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
178
+
179
+ return {
180
+ "dataset": dataset,
181
+ "payload": payload,
182
+ "output": str(Path(output_path).expanduser()) if output_path else None,
183
+ }
184
+
185
+ def build_and_run(self, **kwargs) -> dict:
186
+ """先构造 payload,再立即执行查询。"""
187
+ build_result = self.build(**kwargs)
188
+ query_result = self.client.cli_query(build_result["payload"])
189
+ return {
190
+ **build_result,
191
+ "result": query_result,
192
+ }
193
+
194
+ def _build_data_comparison(self, raw: str, *, dataset_alias: str) -> dict:
195
+ """解析 dataComparison 定义:field,start_date,end_date。
196
+
197
+ 格式示例:date_id,2026-03-01,2026-03-22
198
+ field 可以是纯字段名(自动加前缀)或含前缀的全名。
199
+ """
200
+ parts = [item.strip() for item in raw.split(",")]
201
+ if len(parts) != 3 or not all(parts):
202
+ raise InvalidPayloadError(
203
+ "--data-comparison 格式: field,start_date,end_date"
204
+ "(例: date_id,2026-03-01,2026-03-22)"
205
+ )
206
+ field, start_date, end_date = parts
207
+ # 自动补全数据集别名前缀
208
+ if "." not in field:
209
+ field = f"{dataset_alias}.{field}"
210
+ return {
211
+ "switch": True,
212
+ "field": field,
213
+ "startDate": start_date,
214
+ "endDate": end_date,
215
+ }
216
+
217
+ def _validate_payload(self, payload: dict) -> None:
218
+ """校验最小请求结构。"""
219
+ if not isinstance(payload, dict):
220
+ raise InvalidPayloadError("payload 顶层必须是 JSON 对象")
221
+ if "tableId" not in payload:
222
+ raise InvalidPayloadError("payload 缺少 tableId")
223
+ if "query" not in payload or not isinstance(payload["query"], dict):
224
+ raise InvalidPayloadError("payload 缺少 query 对象")
225
+
226
+ def _load_query_metadata(self, *, skills_dir: str | None, cwd: Path | None) -> dict:
227
+ """从已安装 Skill 或内置模板中读取 query_metadata.json。"""
228
+ metadata_file = self._resolve_metadata_file(skills_dir=skills_dir, cwd=cwd)
229
+ try:
230
+ payload = json.loads(metadata_file.read_text(encoding="utf-8"))
231
+ except Exception as exc:
232
+ raise QueryMetadataNotReadyError(f"query metadata 读取失败: {metadata_file}") from exc
233
+
234
+ if not isinstance(payload, dict):
235
+ raise QueryMetadataNotReadyError(f"query metadata 结构非法: {metadata_file}")
236
+ return payload
237
+
238
+ def _resolve_metadata_file(self, *, skills_dir: str | None, cwd: Path | None) -> Path:
239
+ """优先使用已安装 Skill 的 metadata 文件,否则回退到内置模板。"""
240
+ records = self.detector.discover(skills_dir=skills_dir, cwd=cwd)
241
+ for record in records:
242
+ if record.name != "ops-dataset-query":
243
+ continue
244
+ metadata_file = record.root / "data" / "query_metadata.json"
245
+ if metadata_file.exists():
246
+ return metadata_file
247
+
248
+ fallback = self.template_dir / "query_metadata.json"
249
+ if fallback.exists():
250
+ return fallback
251
+ raise QueryMetadataNotReadyError("本地未找到 query_metadata.json,请先安装并升级 ops-dataset-query")
252
+
253
+ def _parse_dimension_spec(self, raw: str) -> _FieldSpec:
254
+ """解析维度定义:field_name[:alias]。"""
255
+ parts = [item.strip() for item in raw.split(":")]
256
+ if len(parts) == 1 and parts[0]:
257
+ return _FieldSpec(field_name=parts[0], alias=parts[0])
258
+ if len(parts) == 2 and parts[0] and parts[1]:
259
+ return _FieldSpec(field_name=parts[0], alias=parts[1])
260
+ raise InvalidPayloadError(f"无效的 --dimension 定义: {raw}")
261
+
262
+ def _parse_metric_spec(self, raw: str) -> _FieldSpec:
263
+ """解析指标定义:field_name:aggregation[:alias]。"""
264
+ parts = [item.strip() for item in raw.split(":")]
265
+ if len(parts) == 2 and parts[0] and parts[1]:
266
+ return _FieldSpec(field_name=parts[0], aggregation=parts[1].upper(), alias=parts[0])
267
+ if len(parts) == 3 and parts[0] and parts[1] and parts[2]:
268
+ return _FieldSpec(field_name=parts[0], aggregation=parts[1].upper(), alias=parts[2])
269
+ raise InvalidPayloadError(f"无效的 --metric 定义: {raw}")
270
+
271
+ def _build_order_by(self, items: list[str]) -> list[dict]:
272
+ """解析排序定义:expr[:asc|desc]。"""
273
+ result: list[dict] = []
274
+ for raw in items:
275
+ parts = [item.strip() for item in raw.split(":")]
276
+ if len(parts) == 1 and parts[0]:
277
+ result.append({"expr": parts[0], "desc": False})
278
+ continue
279
+ if len(parts) == 2 and parts[0] and parts[1]:
280
+ direction = parts[1].lower()
281
+ if direction not in ("asc", "desc"):
282
+ raise InvalidPayloadError(f"无效的 --order-by 排序方向: {raw}")
283
+ result.append({"expr": parts[0], "desc": direction == "desc"})
284
+ continue
285
+ raise InvalidPayloadError(f"无效的 --order-by 定义: {raw}")
286
+ return result
287
+
288
+ def _load_where_clause(
289
+ self,
290
+ *,
291
+ where_conditions: list[str],
292
+ where_json: str | None,
293
+ where_file: str | None,
294
+ dataset_alias: str,
295
+ ) -> dict | None:
296
+ """从 JSON 字符串或文件中读取 where 结构。"""
297
+ if where_conditions:
298
+ payload = {
299
+ "operator": "AND",
300
+ "conditions": [self._parse_where_condition(item, dataset_alias=dataset_alias) for item in where_conditions],
301
+ }
302
+ elif where_json:
303
+ try:
304
+ payload = json.loads(where_json)
305
+ except Exception as exc:
306
+ raise InvalidPayloadError("--where-json 不是合法 JSON") from exc
307
+ elif where_file:
308
+ file_path = Path(where_file).expanduser()
309
+ if not file_path.exists():
310
+ raise InvalidPayloadError(f"where 文件不存在: {file_path}")
311
+ try:
312
+ payload = json.loads(file_path.read_text(encoding="utf-8"))
313
+ except Exception as exc:
314
+ raise InvalidPayloadError(f"where 文件不是合法 JSON: {file_path}") from exc
315
+ else:
316
+ return None
317
+
318
+ if not isinstance(payload, dict):
319
+ raise InvalidPayloadError("where 必须是 JSON 对象")
320
+ return payload
321
+
322
+ def _parse_where_condition(self, raw: str, *, dataset_alias: str) -> dict:
323
+ """解析 where 简写条件:field|operator|value_json。"""
324
+ parts = raw.split("|", 2)
325
+ if len(parts) != 3:
326
+ raise InvalidPayloadError(f"无效的 --where 定义: {raw}")
327
+
328
+ field, operator, value_raw = (item.strip() for item in parts)
329
+ if not field or not operator or not value_raw:
330
+ raise InvalidPayloadError(f"无效的 --where 定义: {raw}")
331
+
332
+ expr = field if "." in field else f"{dataset_alias}.{field}"
333
+ try:
334
+ value = json.loads(value_raw)
335
+ except Exception:
336
+ value = value_raw
337
+
338
+ return {
339
+ "field": expr,
340
+ "operator": operator,
341
+ "value": value,
342
+ }
343
+
344
+ def _build_having_clause(self, items: list[str]) -> list[dict]:
345
+ """解析 having 简写条件:expr|operator|value_json。"""
346
+ result: list[dict] = []
347
+ for raw in items:
348
+ parts = raw.split("|", 2)
349
+ if len(parts) != 3:
350
+ raise InvalidPayloadError(f"无效的 --having 定义: {raw}")
351
+
352
+ expr, operator, value_raw = (item.strip() for item in parts)
353
+ if not expr or not operator or not value_raw:
354
+ raise InvalidPayloadError(f"无效的 --having 定义: {raw}")
355
+
356
+ try:
357
+ value = json.loads(value_raw)
358
+ except Exception:
359
+ value = value_raw
360
+
361
+ result.append(
362
+ {
363
+ "field": expr,
364
+ "operator": operator,
365
+ "value": value,
366
+ }
367
+ )
368
+ return result
369
+
370
+ def _ensure_field_exists(self, field_index: dict[str, dict], field_name: str) -> None:
371
+ """校验字段存在于 metadata 中。"""
372
+ if field_name not in field_index:
373
+ raise InvalidPayloadError(f"字段不存在于当前数据集 metadata 中: {field_name}")
@@ -0,0 +1,5 @@
1
+ """query 远端传输层。"""
2
+
3
+ from opscli.query.transport.client import QueryClient
4
+
5
+ __all__ = ["QueryClient"]
@@ -0,0 +1,57 @@
1
+ """query 模块远端请求客户端。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from opscli.auth import AuthClient, OPS_URL
8
+ from opscli.query.domain.exceptions import BadRemoteJsonError, RemoteBusinessError, RemoteHttpError
9
+
10
+
11
+ class QueryClient:
12
+ """统一封装 query 模块的远端请求。"""
13
+
14
+ def __init__(self, auth_client: AuthClient | None = None) -> None:
15
+ self.auth_client = auth_client or AuthClient()
16
+ self.ops_url = OPS_URL.rstrip("/")
17
+
18
+ def cli_query(self, payload: dict) -> dict:
19
+ """转发查询请求到 auto-scheduler 的 cli-query 接口。"""
20
+ headers, cookies = self.auth_client.build_request_auth("ops")
21
+ response = httpx.post(
22
+ f"{self.ops_url}/v1/data-metrics/cli-query",
23
+ json=payload,
24
+ headers=headers,
25
+ cookies=cookies,
26
+ timeout=30,
27
+ )
28
+ return self._parse_response(response)
29
+
30
+ def _parse_response(self, response: httpx.Response) -> dict:
31
+ """统一解析 HTTP 响应,识别业务层错误。"""
32
+ try:
33
+ payload = response.json()
34
+ except Exception as exc:
35
+ raise BadRemoteJsonError("远端返回了无法解析的 JSON") from exc
36
+
37
+ if response.status_code >= 400:
38
+ message = self._extract_message(payload) or f"远端请求失败,HTTP {response.status_code}"
39
+ raise RemoteHttpError(response.status_code, message)
40
+
41
+ if isinstance(payload, dict):
42
+ business_code = payload.get("code")
43
+ if business_code not in (None, 0, 200):
44
+ message = self._extract_message(payload) or "远端业务执行失败"
45
+ raise RemoteBusinessError(business_code, message)
46
+
47
+ if not isinstance(payload, dict):
48
+ raise BadRemoteJsonError("远端返回结构不是 JSON 对象")
49
+ return payload
50
+
51
+ def _extract_message(self, payload: dict) -> str | None:
52
+ """从远端返回中提取最有价值的错误信息。"""
53
+ for key in ("msg", "message", "error"):
54
+ value = payload.get(key)
55
+ if isinstance(value, str) and value.strip():
56
+ return value.strip()
57
+ return None
@@ -0,0 +1,37 @@
1
+ """opscli skills 模块。
2
+
3
+ 提供 Claude Code / OpenClaw 的 Skill 生命周期管理能力,包括:
4
+ - 发现:扫描本地已安装的 Skill
5
+ - 安装:从内置模板安装到指定目录
6
+ - 状态:查询本地和远端版本对比
7
+ - 升级:从运营系统后端拉取最新数据
8
+
9
+ 典型工作流程:
10
+ 1. 用户执行 `opscli skills install ops-dataset-query` 安装 Skill
11
+ 2. Skill 被部署到 .claude/skills 或 .openclaw/skills
12
+ 3. 用户执行 `opscli skills upgrade` 拉取远端最新数据
13
+
14
+ CLI 入口通过 cli.py 注册到 opscli 顶级命令:
15
+ - opscli skills list # 列出已安装的 Skill
16
+ - opscli skills install # 安装 Skill
17
+ - opscli skills status # 查看安装状态
18
+ - opscli skills upgrade # 升级到远端最新版本
19
+ """
20
+
21
+ from opscli.skills.domain.models import (
22
+ SkillBatchInstallResult,
23
+ SkillBatchUpgradeResult,
24
+ SkillInstallResult,
25
+ SkillRecord,
26
+ SkillUpgradeResult,
27
+ )
28
+ from opscli.skills.services.manager import SkillsManager
29
+
30
+ __all__ = [
31
+ "SkillRecord",
32
+ "SkillInstallResult",
33
+ "SkillBatchInstallResult",
34
+ "SkillUpgradeResult",
35
+ "SkillBatchUpgradeResult",
36
+ "SkillsManager",
37
+ ]
opscli/skills/cli.py ADDED
@@ -0,0 +1,5 @@
1
+ """skills CLI 兼容导出。"""
2
+
3
+ from opscli.skills.commands.cli import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1 @@
1
+ """skills 命令层。"""