hw-cloudrobo-core 0.2.0__tar.gz
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.
- hw_cloudrobo_core-0.2.0/PKG-INFO +28 -0
- hw_cloudrobo_core-0.2.0/README.md +18 -0
- hw_cloudrobo_core-0.2.0/pyproject.toml +48 -0
- hw_cloudrobo_core-0.2.0/setup.cfg +4 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/__init__.py +0 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/__init__.py +0 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/cli_utils.py +15 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/config_cmd.py +173 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/config_utils.py +81 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/self_cmd.py +113 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/setup_user_config.py +36 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/skill_cmd.py +137 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli/skill_loader.py +17 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/cli_main.py +219 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/config.yaml +18 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/models.py +7 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/plugins/__init__.py +49 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/__init__.py +4 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/apig_sdk_auth.py +134 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/auth.py +11 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/base_client.py +12 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/config.py +263 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/crypto.py +66 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/exceptions.py +46 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/http_client.py +433 -0
- hw_cloudrobo_core-0.2.0/src/cloudrobo_core/sdk/obs_client.py +502 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/PKG-INFO +28 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/SOURCES.txt +31 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/dependency_links.txt +1 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/entry_points.txt +2 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/requires.txt +11 -0
- hw_cloudrobo_core-0.2.0/src/hw_cloudrobo_core.egg-info/top_level.txt +1 -0
- hw_cloudrobo_core-0.2.0/tests/test_sdk.py +64 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hw-cloudrobo-core
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CloudRobo Core SDK and CLI Framework
|
|
5
|
+
Author-email: Huawei Cloud CloudRobo Team <hwcloudrobo@huawei.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Requires-Dist: click>=8.0
|
|
20
|
+
Requires-Dist: pyyaml>=5.0
|
|
21
|
+
Requires-Dist: requests>=2.25
|
|
22
|
+
Requires-Dist: rich>=10.0
|
|
23
|
+
Requires-Dist: cryptography>=41.0.0
|
|
24
|
+
Requires-Dist: esdk-obs-python
|
|
25
|
+
Requires-Dist: hw-cloudrobo-workspace>=0.1.0
|
|
26
|
+
Requires-Dist: hw-cloudrobo-resource>=0.1.0
|
|
27
|
+
Provides-Extra: obs
|
|
28
|
+
Requires-Dist: pycryptodome==3.10.1; extra == "obs"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# cloudrobo-core
|
|
2
|
+
|
|
3
|
+
核心 SDK 与 CLI 框架,提供 Config、HttpClient、BaseClient 和 CLI 插件加载机制。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e packages/cloudrobo-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 文档
|
|
12
|
+
|
|
13
|
+
- [模块概览](docs/index.md)
|
|
14
|
+
- [CLI 命令](docs/commands.md)
|
|
15
|
+
- [使用示例](docs/examples.md)
|
|
16
|
+
- [开发指南](docs/development.md)
|
|
17
|
+
- [安装指南](../../docs/installation.md)
|
|
18
|
+
- [架构文档](../../docs/architecture.md)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==83.0.0", "packaging==26.2", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hw-cloudrobo-core"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "CloudRobo Core SDK and CLI Framework"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
authors = [{name = "Huawei Cloud CloudRobo Team", email = "hwcloudrobo@huawei.com"}]
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"click>=8.0",
|
|
27
|
+
"pyyaml>=5.0",
|
|
28
|
+
"requests>=2.25",
|
|
29
|
+
"rich>=10.0",
|
|
30
|
+
"cryptography>=41.0.0",
|
|
31
|
+
"esdk-obs-python",
|
|
32
|
+
"hw-cloudrobo-workspace>=0.1.0",
|
|
33
|
+
"hw-cloudrobo-resource>=0.1.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
obs = [
|
|
38
|
+
"pycryptodome==3.10.1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
cloudrobo = "cloudrobo_core.cli_main:main"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
cloudrobo_core = ["config.yaml"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import click
|
|
4
|
+
from cloudrobo_core.sdk import Config, HttpClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_client(ctx, client_class):
|
|
8
|
+
config_path = os.environ.get("CLOUDROBO_SERVICE_CONFIG")
|
|
9
|
+
config = Config(config_path or None)
|
|
10
|
+
http = HttpClient(config)
|
|
11
|
+
return client_class(http)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def out(result):
|
|
15
|
+
click.echo(json.dumps(result, ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from cloudrobo_core.cli.config_utils import USER_CONFIG_PATH
|
|
8
|
+
|
|
9
|
+
_SENSITIVE_KEYS = {"ak", "sk"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _mask(value: str) -> str:
|
|
13
|
+
if len(value) > 8:
|
|
14
|
+
return value[:4] + "****" + value[-4:]
|
|
15
|
+
return "****"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def config():
|
|
20
|
+
"""配置管理命令"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@config.command("set")
|
|
25
|
+
@click.argument("pairs", nargs=-1, required=True)
|
|
26
|
+
def set_config(pairs):
|
|
27
|
+
"""设置配置项(支持一次设置多个)
|
|
28
|
+
|
|
29
|
+
用法:
|
|
30
|
+
cloudrobo config set ak xxx sk yyy
|
|
31
|
+
cloudrobo config set ak xxx sk yyy region cn-southwest-2
|
|
32
|
+
|
|
33
|
+
支持的 key:
|
|
34
|
+
ak, sk, region
|
|
35
|
+
|
|
36
|
+
ak/sk 会自动加密存储(机器绑定),region 明文存储。
|
|
37
|
+
"""
|
|
38
|
+
if len(pairs) % 2 != 0:
|
|
39
|
+
click.echo("参数必须是 key-value 对,例如: config set ak xxx sk yyy", err=True)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
config_path = USER_CONFIG_PATH
|
|
43
|
+
|
|
44
|
+
data = {}
|
|
45
|
+
if config_path.exists():
|
|
46
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
47
|
+
data = yaml.safe_load(f) or {}
|
|
48
|
+
|
|
49
|
+
if "cloudrobo" not in data:
|
|
50
|
+
data["cloudrobo"] = {}
|
|
51
|
+
if "auth" not in data["cloudrobo"]:
|
|
52
|
+
data["cloudrobo"]["auth"] = {}
|
|
53
|
+
|
|
54
|
+
key_map = {
|
|
55
|
+
"ak": ("cloudrobo", "auth", "ak"),
|
|
56
|
+
"sk": ("cloudrobo", "auth", "sk"),
|
|
57
|
+
"region": ("cloudrobo", "region"),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for i in range(0, len(pairs), 2):
|
|
61
|
+
key, value = pairs[i], pairs[i + 1]
|
|
62
|
+
|
|
63
|
+
if key not in key_map:
|
|
64
|
+
click.echo(f"不支持的配置项: {key}", err=True)
|
|
65
|
+
click.echo(f"支持的配置项: {', '.join(key_map.keys())}", err=True)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if key in _SENSITIVE_KEYS:
|
|
69
|
+
from cloudrobo_core.sdk.crypto import encrypt
|
|
70
|
+
enc_value = encrypt(value)
|
|
71
|
+
data["cloudrobo"]["auth"][f"{key}_enc"] = enc_value
|
|
72
|
+
data["cloudrobo"]["auth"].pop(key, None)
|
|
73
|
+
click.echo(f"已设置 {key} (加密存储)")
|
|
74
|
+
else:
|
|
75
|
+
path = key_map[key]
|
|
76
|
+
obj = data
|
|
77
|
+
for p in path[:-1]:
|
|
78
|
+
if p not in obj:
|
|
79
|
+
obj[p] = {}
|
|
80
|
+
obj = obj[p]
|
|
81
|
+
obj[path[-1]] = value
|
|
82
|
+
click.echo(f"已设置 {key} = {value}")
|
|
83
|
+
|
|
84
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
86
|
+
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
|
|
87
|
+
try:
|
|
88
|
+
os.chmod(config_path, 0o600)
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@config.command("get")
|
|
94
|
+
@click.argument("key")
|
|
95
|
+
def get_config(key):
|
|
96
|
+
"""获取配置项
|
|
97
|
+
|
|
98
|
+
支持的 key:
|
|
99
|
+
ak, sk, region
|
|
100
|
+
|
|
101
|
+
ak/sk 解密后脱敏显示(仅显示前4后4位)。
|
|
102
|
+
"""
|
|
103
|
+
config_path = USER_CONFIG_PATH
|
|
104
|
+
|
|
105
|
+
if not config_path.exists():
|
|
106
|
+
click.echo(f"配置文件不存在: {config_path}", err=True)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
110
|
+
data = yaml.safe_load(f) or {}
|
|
111
|
+
|
|
112
|
+
cloudrobo = data.get("cloudrobo", {})
|
|
113
|
+
auth = cloudrobo.get("auth", {})
|
|
114
|
+
|
|
115
|
+
if key in _SENSITIVE_KEYS:
|
|
116
|
+
enc_field = f"{key}_enc"
|
|
117
|
+
enc_value = auth.get(enc_field)
|
|
118
|
+
if enc_value:
|
|
119
|
+
from cloudrobo_core.sdk.crypto import decrypt
|
|
120
|
+
try:
|
|
121
|
+
value = decrypt(enc_value)
|
|
122
|
+
click.echo(_mask(value))
|
|
123
|
+
except Exception:
|
|
124
|
+
click.echo("(解密失败)", err=True)
|
|
125
|
+
else:
|
|
126
|
+
plain = auth.get(key, "")
|
|
127
|
+
if plain:
|
|
128
|
+
click.echo(f"{_mask(plain)} (明文存储)")
|
|
129
|
+
else:
|
|
130
|
+
click.echo(f"{key} 未设置")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if key == "region":
|
|
134
|
+
value = cloudrobo.get("region", "")
|
|
135
|
+
click.echo(value if value else f"{key} 未设置")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
click.echo(f"不支持的配置项: {key}", err=True)
|
|
139
|
+
click.echo(f"支持的配置项: ak, sk, region", err=True)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@config.command("list")
|
|
143
|
+
def list_config():
|
|
144
|
+
"""列出所有配置"""
|
|
145
|
+
config_path = USER_CONFIG_PATH
|
|
146
|
+
|
|
147
|
+
if not config_path.exists():
|
|
148
|
+
click.echo(f"配置文件不存在: {config_path}", err=True)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
152
|
+
data = yaml.safe_load(f) or {}
|
|
153
|
+
|
|
154
|
+
cloudrobo = data.get("cloudrobo", {})
|
|
155
|
+
auth = cloudrobo.get("auth", {})
|
|
156
|
+
|
|
157
|
+
click.echo(f"配置文件: {config_path}")
|
|
158
|
+
click.echo()
|
|
159
|
+
click.echo("认证配置:")
|
|
160
|
+
|
|
161
|
+
for key in ("ak", "sk"):
|
|
162
|
+
enc_field = f"{key}_enc"
|
|
163
|
+
if auth.get(enc_field):
|
|
164
|
+
click.echo(f" {key}: 已加密存储 ✓")
|
|
165
|
+
elif auth.get(key):
|
|
166
|
+
click.echo(f" {key}: 明文存储 ⚠ (建议重新配置以启用加密存储)")
|
|
167
|
+
else:
|
|
168
|
+
click.echo(f" {key}: (未设置)")
|
|
169
|
+
|
|
170
|
+
click.echo()
|
|
171
|
+
click.echo("其他配置:")
|
|
172
|
+
region = cloudrobo.get("region", "")
|
|
173
|
+
click.echo(f" region: {region if region else '(未设置)'}")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
USER_CONFIG_DIR = Path.home() / ".cloudrobo"
|
|
12
|
+
USER_CONFIG_PATH = USER_CONFIG_DIR / "config.yaml"
|
|
13
|
+
|
|
14
|
+
_FIELDS_TO_CLEAR = [
|
|
15
|
+
("cloudrobo", "auth", "ak"),
|
|
16
|
+
("cloudrobo", "auth", "sk"),
|
|
17
|
+
("cloudrobo", "proxy", "http"),
|
|
18
|
+
("cloudrobo", "proxy", "https"),
|
|
19
|
+
("cloudrobo", "proxy", "no_proxy"),
|
|
20
|
+
("debug", "ca_bundle"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_project_config() -> Dict[str, Any]:
|
|
25
|
+
try:
|
|
26
|
+
config_file = files("cloudrobo_core").joinpath("config.yaml")
|
|
27
|
+
with open(str(config_file), "r", encoding="utf-8") as f:
|
|
28
|
+
return yaml.safe_load(f) or {}
|
|
29
|
+
except (FileNotFoundError, yaml.YAMLError) as e:
|
|
30
|
+
logger.debug("Failed to load project config via importlib: %s", e)
|
|
31
|
+
fallback = Path(__file__).resolve()
|
|
32
|
+
for _ in range(6):
|
|
33
|
+
fallback = fallback.parent
|
|
34
|
+
candidate = fallback / "config.yaml"
|
|
35
|
+
if candidate.exists():
|
|
36
|
+
try:
|
|
37
|
+
with open(candidate, "r", encoding="utf-8") as f:
|
|
38
|
+
return yaml.safe_load(f) or {}
|
|
39
|
+
except (FileNotFoundError, yaml.YAMLError) as e:
|
|
40
|
+
logger.debug("Failed to load fallback config %s: %s", candidate, e)
|
|
41
|
+
continue
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _generate_user_template() -> str:
|
|
46
|
+
data = _load_project_config()
|
|
47
|
+
for path in _FIELDS_TO_CLEAR:
|
|
48
|
+
node = data
|
|
49
|
+
for key in path[:-1]:
|
|
50
|
+
if isinstance(node, dict) and key in node:
|
|
51
|
+
node = node[key]
|
|
52
|
+
else:
|
|
53
|
+
node = None
|
|
54
|
+
break
|
|
55
|
+
if node is not None and isinstance(node, dict):
|
|
56
|
+
node[path[-1]] = ""
|
|
57
|
+
header = "# CloudRobo 用户配置(优先级高于工程目录 config/config.yaml)\n"
|
|
58
|
+
header += "# 在此填入你的 AK/SK,无需修改工程目录下的配置\n\n"
|
|
59
|
+
return header + yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_user_config() -> Path:
|
|
63
|
+
if not USER_CONFIG_PATH.exists():
|
|
64
|
+
USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
USER_CONFIG_PATH.write_text(_generate_user_template(), encoding="utf-8")
|
|
66
|
+
try:
|
|
67
|
+
os.chmod(USER_CONFIG_PATH, 0o600)
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
logger.info("Created user config template: %s", USER_CONFIG_PATH)
|
|
71
|
+
return USER_CONFIG_PATH
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
75
|
+
result = dict(base)
|
|
76
|
+
for k, v in override.items():
|
|
77
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
78
|
+
result[k] = deep_merge(result[k], v)
|
|
79
|
+
else:
|
|
80
|
+
result[k] = v
|
|
81
|
+
return result
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def self():
|
|
11
|
+
"""CloudRobo 自身管理命令组"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@self.command("uninstall")
|
|
16
|
+
@click.argument("packages", nargs=-1, required=False)
|
|
17
|
+
@click.option("--yes", "-y", is_flag=True, help="跳过确认提示")
|
|
18
|
+
@click.option("--all", "uninstall_all", is_flag=True, help="卸载所有 CloudRobo 包")
|
|
19
|
+
def uninstall(packages, yes, uninstall_all):
|
|
20
|
+
"""卸载 CloudRobo Client 安装包
|
|
21
|
+
|
|
22
|
+
不指定包名时卸载所有包。支持子包简写(如 asset, dataset)或完整包名。
|
|
23
|
+
|
|
24
|
+
示例:
|
|
25
|
+
|
|
26
|
+
cloudrobo self uninstall # 卸载所有包
|
|
27
|
+
|
|
28
|
+
cloudrobo self uninstall asset # 卸载 asset 子包
|
|
29
|
+
|
|
30
|
+
cloudrobo self uninstall hw-cloudrobo-asset # 卸载 asset 子包(完整名)
|
|
31
|
+
|
|
32
|
+
cloudrobo self uninstall asset dataset # 卸载多个子包
|
|
33
|
+
"""
|
|
34
|
+
# 获取已安装的包列表
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
[sys.executable, "-m", "pip", "list", "--format=columns"],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
)
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
click.echo(f"获取安装包列表失败: {result.stderr.strip()}", err=True)
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
# 扫描所有 cloudrobo 相关包(支持开发模式和发布模式)
|
|
45
|
+
installed_packages = []
|
|
46
|
+
for line in result.stdout.splitlines():
|
|
47
|
+
parts = line.split()
|
|
48
|
+
if parts and (
|
|
49
|
+
parts[0].startswith("cloudrobo-")
|
|
50
|
+
or parts[0].startswith("hw-cloudrobo-")
|
|
51
|
+
):
|
|
52
|
+
installed_packages.append(parts[0])
|
|
53
|
+
|
|
54
|
+
if not installed_packages:
|
|
55
|
+
click.echo("未找到任何 CloudRobo 安装包。")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# 确定要卸载的包
|
|
59
|
+
if uninstall_all or not packages:
|
|
60
|
+
# 卸载所有包
|
|
61
|
+
to_uninstall = installed_packages
|
|
62
|
+
else:
|
|
63
|
+
# 卸载指定包(支持简写和完整名)
|
|
64
|
+
to_uninstall = []
|
|
65
|
+
for pkg in packages:
|
|
66
|
+
# 如果已经是完整包名且已安装
|
|
67
|
+
if pkg in installed_packages:
|
|
68
|
+
to_uninstall.append(pkg)
|
|
69
|
+
else:
|
|
70
|
+
# 尝试匹配简写(asset → hw-cloudrobo-asset 或 cloudrobo-asset)
|
|
71
|
+
matched = [
|
|
72
|
+
p for p in installed_packages
|
|
73
|
+
if p.endswith(f"-{pkg}") or p == f"hw-cloudrobo-{pkg}" or p == f"cloudrobo-{pkg}"
|
|
74
|
+
]
|
|
75
|
+
if matched:
|
|
76
|
+
to_uninstall.extend(matched)
|
|
77
|
+
else:
|
|
78
|
+
click.echo(f"警告: 未找到包 '{pkg}',跳过。")
|
|
79
|
+
|
|
80
|
+
if not to_uninstall:
|
|
81
|
+
click.echo("没有需要卸载的包。")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
click.echo("即将卸载以下安装包:")
|
|
85
|
+
for pkg in to_uninstall:
|
|
86
|
+
click.echo(f" - {pkg}")
|
|
87
|
+
|
|
88
|
+
if not yes:
|
|
89
|
+
if not click.confirm("确认卸载?"):
|
|
90
|
+
click.echo("已取消。")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# 生成卸载脚本(避免自杀问题)
|
|
94
|
+
script = "import subprocess, sys\n"
|
|
95
|
+
script += f"pkgs = {to_uninstall!r}\n"
|
|
96
|
+
script += "cmd = [sys.executable, '-m', 'pip', 'uninstall', '-y'] + pkgs\n"
|
|
97
|
+
script += "r = subprocess.run(cmd)\n"
|
|
98
|
+
script += "sys.exit(r.returncode)\n"
|
|
99
|
+
|
|
100
|
+
script_path = os.path.join(tempfile.gettempdir(), "cloudrobo_uninstall.py")
|
|
101
|
+
with open(script_path, "w", encoding="utf-8") as f:
|
|
102
|
+
f.write(script)
|
|
103
|
+
|
|
104
|
+
click.echo("卸载脚本已生成,即将退出当前进程并执行卸载...")
|
|
105
|
+
popen_kwargs = {}
|
|
106
|
+
if sys.platform == "win32":
|
|
107
|
+
popen_kwargs["creationflags"] = (
|
|
108
|
+
subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
popen_kwargs["start_new_session"] = True
|
|
112
|
+
subprocess.Popen([sys.executable, script_path], **popen_kwargs)
|
|
113
|
+
sys.exit(0)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
USER_CLOUDROBO_DIR = Path.home() / ".cloudrobo"
|
|
8
|
+
USER_CONFIG_PATH = USER_CLOUDROBO_DIR / "config.yaml"
|
|
9
|
+
|
|
10
|
+
_DEFAULT_USER_CONFIG = """\
|
|
11
|
+
# CloudRobo 用户配置(优先级高于工程目录 config/config.yaml)
|
|
12
|
+
# 在此填入你的 AK/SK,无需修改工程目录下的配置
|
|
13
|
+
|
|
14
|
+
cloudrobo:
|
|
15
|
+
auth:
|
|
16
|
+
ak: ""
|
|
17
|
+
sk: ""
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ensure_user_config() -> Path:
|
|
22
|
+
USER_CLOUDROBO_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
if not USER_CONFIG_PATH.exists():
|
|
24
|
+
USER_CONFIG_PATH.write_text(_DEFAULT_USER_CONFIG, encoding="utf-8")
|
|
25
|
+
logger.info("Created user config: %s", USER_CONFIG_PATH)
|
|
26
|
+
return USER_CONFIG_PATH
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _post_install():
|
|
30
|
+
ensure_user_config()
|
|
31
|
+
secrets_dir = USER_CLOUDROBO_DIR / "conversations"
|
|
32
|
+
secrets_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
_post_install()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .skill_loader import _parse_frontmatter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TARGET_MAP = {
|
|
11
|
+
"claude-code": "~/.claude/skills",
|
|
12
|
+
"claude-code-project": ".claude/skills",
|
|
13
|
+
"jiuwenswarm": "~/.jiuwenswarm/agent/workspace/skills",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _resolve_target(target: str) -> Path:
|
|
18
|
+
if target.startswith("path:"):
|
|
19
|
+
return Path(target[5:]).expanduser().resolve()
|
|
20
|
+
|
|
21
|
+
if target not in TARGET_MAP:
|
|
22
|
+
click.echo(f"Unknown target: {target}", err=True)
|
|
23
|
+
click.echo(f"Supported targets: {', '.join(TARGET_MAP.keys())}, path:/custom/dir", err=True)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if target == "jiuwenswarm":
|
|
27
|
+
data_dir = os.environ.get("JIUWENSWARM_DATA_DIR")
|
|
28
|
+
if data_dir:
|
|
29
|
+
return Path(data_dir) / "agent" / "workspace" / "skills"
|
|
30
|
+
|
|
31
|
+
return Path(TARGET_MAP[target]).expanduser().resolve()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _list_skills_from(skills_root: Path) -> list:
|
|
35
|
+
skills = []
|
|
36
|
+
for skill_dir in sorted(skills_root.iterdir()):
|
|
37
|
+
if not skill_dir.is_dir():
|
|
38
|
+
continue
|
|
39
|
+
skill_md = skill_dir / "SKILL.md"
|
|
40
|
+
if not skill_md.exists():
|
|
41
|
+
continue
|
|
42
|
+
meta = _parse_frontmatter(skill_md)
|
|
43
|
+
skills.append({
|
|
44
|
+
"name": meta.get("name", skill_dir.name),
|
|
45
|
+
"description": meta.get("description", ""),
|
|
46
|
+
"dir": str(skill_dir),
|
|
47
|
+
})
|
|
48
|
+
return skills
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group(name="skill")
|
|
52
|
+
def skill():
|
|
53
|
+
"""Skill management commands"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@skill.command("install")
|
|
58
|
+
@click.option("--target", required=True,
|
|
59
|
+
help="Target platform (claude-code, claude-code-project, jiuwenswarm) or custom path (path:/dir)")
|
|
60
|
+
@click.option("--source", required=True,
|
|
61
|
+
help="Skill source directory")
|
|
62
|
+
@click.option("--skill-name", default=None,
|
|
63
|
+
help="Specific skills to install (comma-separated). Default: all skills")
|
|
64
|
+
def install(target, source, skill_name):
|
|
65
|
+
"""Install skills to agent platform directories"""
|
|
66
|
+
target_dir = _resolve_target(target)
|
|
67
|
+
if target_dir is None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
target_dir = Path(target_dir).expanduser().resolve()
|
|
71
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
skills_root = Path(source).expanduser().resolve()
|
|
74
|
+
if not skills_root.exists():
|
|
75
|
+
click.echo(f"Source directory not found: {skills_root}", err=True)
|
|
76
|
+
return
|
|
77
|
+
skills = _list_skills_from(skills_root)
|
|
78
|
+
|
|
79
|
+
if skill_name:
|
|
80
|
+
skill_names = [n.strip() for n in skill_name.split(",") if n.strip()]
|
|
81
|
+
skills = [s for s in skills if s["name"] in skill_names]
|
|
82
|
+
missing = set(skill_names) - {s["name"] for s in skills}
|
|
83
|
+
if missing:
|
|
84
|
+
click.echo(f"Skills not found: {', '.join(missing)}", err=True)
|
|
85
|
+
|
|
86
|
+
if not skills:
|
|
87
|
+
click.echo("No matching skills to install.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
installed = []
|
|
91
|
+
for s in skills:
|
|
92
|
+
src = Path(s["dir"])
|
|
93
|
+
dst = target_dir / src.name
|
|
94
|
+
if dst.exists():
|
|
95
|
+
shutil.rmtree(dst)
|
|
96
|
+
shutil.copytree(src, dst)
|
|
97
|
+
installed.append(s["name"])
|
|
98
|
+
|
|
99
|
+
click.echo(f"Installed {len(installed)} skill(s) to {target_dir}:")
|
|
100
|
+
for name in installed:
|
|
101
|
+
click.echo(f" {name}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@skill.command("uninstall")
|
|
105
|
+
@click.option("--target", required=True,
|
|
106
|
+
help="Target platform (claude-code, claude-code-project, jiuwenswarm) or custom path (path:/dir)")
|
|
107
|
+
@click.option("--skill-name", default=None,
|
|
108
|
+
help="Specific skills to uninstall (comma-separated). Default: all skills")
|
|
109
|
+
def uninstall(target, skill_name):
|
|
110
|
+
"""Uninstall skills from agent platform directories"""
|
|
111
|
+
target_dir = _resolve_target(target)
|
|
112
|
+
if target_dir is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
target_dir = Path(target_dir).expanduser().resolve()
|
|
116
|
+
if not target_dir.exists():
|
|
117
|
+
click.echo(f"Target directory does not exist: {target_dir}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if skill_name:
|
|
121
|
+
skills_to_remove = [n.strip() for n in skill_name.split(",") if n.strip()]
|
|
122
|
+
else:
|
|
123
|
+
skills_to_remove = [d.name for d in target_dir.iterdir() if d.is_dir()]
|
|
124
|
+
|
|
125
|
+
removed = []
|
|
126
|
+
for name in skills_to_remove:
|
|
127
|
+
skill_path = target_dir / name
|
|
128
|
+
if skill_path.exists():
|
|
129
|
+
shutil.rmtree(skill_path)
|
|
130
|
+
removed.append(name)
|
|
131
|
+
|
|
132
|
+
if removed:
|
|
133
|
+
click.echo(f"Uninstalled {len(removed)} skill(s) from {target_dir}:")
|
|
134
|
+
for name in removed:
|
|
135
|
+
click.echo(f" {name}")
|
|
136
|
+
else:
|
|
137
|
+
click.echo("No skills to uninstall.")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Skill loader: SKILL.md frontmatter parsing."""
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _parse_frontmatter(path: Path) -> Dict:
|
|
10
|
+
text = path.read_text(encoding="utf-8")
|
|
11
|
+
m = re.match(r"^---[ \t]*\r?\n(.*?)\r?\n---", text, re.DOTALL)
|
|
12
|
+
if not m:
|
|
13
|
+
return {}
|
|
14
|
+
try:
|
|
15
|
+
return yaml.safe_load(m.group(1)) or {}
|
|
16
|
+
except yaml.YAMLError:
|
|
17
|
+
return {}
|