inkcre-cli 0.1.0__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.
- inkcre_cli/__init__.py +1 -0
- inkcre_cli/command.py +187 -0
- inkcre_cli/commands/__init__.py +1 -0
- inkcre_cli/commands/agent.py +121 -0
- inkcre_cli/commands/config.py +95 -0
- inkcre_cli/commands/connection.py +109 -0
- inkcre_cli/commands/extension.py +125 -0
- inkcre_cli/commands/info.py +245 -0
- inkcre_cli/commands/jobs.py +279 -0
- inkcre_cli/commands/peer.py +67 -0
- inkcre_cli/commands/source.py +147 -0
- inkcre_cli/connection.py +64 -0
- inkcre_cli/errors.py +17 -0
- inkcre_cli/http.py +107 -0
- inkcre_cli/main.py +75 -0
- inkcre_cli/output.py +92 -0
- inkcre_cli/schema.py +71 -0
- inkcre_cli-0.1.0.dist-info/METADATA +77 -0
- inkcre_cli-0.1.0.dist-info/RECORD +21 -0
- inkcre_cli-0.1.0.dist-info/WHEEL +4 -0
- inkcre_cli-0.1.0.dist-info/entry_points.txt +5 -0
inkcre_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Independent InKCre REST consumer; no Core runtime imports."""
|
inkcre_cli/command.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Small shared Click options and one invocation's resources."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from pydantic import BaseModel, TypeAdapter
|
|
10
|
+
|
|
11
|
+
from . import connection
|
|
12
|
+
from .errors import CommandError
|
|
13
|
+
from .http import CoreRESTClient
|
|
14
|
+
from .output import Output
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(click.Command):
|
|
18
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
19
|
+
original = args.copy()
|
|
20
|
+
try:
|
|
21
|
+
return super().parse_args(ctx, args)
|
|
22
|
+
except click.UsageError:
|
|
23
|
+
# Click rejects unknown options before eager callbacks run. Its native
|
|
24
|
+
# tolerant parse can still find presentation options without scanning
|
|
25
|
+
# raw argv (which would mistake an --input-json value for an option).
|
|
26
|
+
with self.make_context(
|
|
27
|
+
ctx.info_name,
|
|
28
|
+
original,
|
|
29
|
+
parent=ctx.parent,
|
|
30
|
+
obj=ctx.obj,
|
|
31
|
+
resilient_parsing=True,
|
|
32
|
+
ignore_unknown_options=True,
|
|
33
|
+
):
|
|
34
|
+
pass
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Group(Command, click.Group):
|
|
39
|
+
command_class = Command
|
|
40
|
+
group_class = type
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Entity(BaseModel):
|
|
44
|
+
type: Literal["block", "relation"]
|
|
45
|
+
id: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def entity(value: str, *, block_only: bool = False) -> Entity:
|
|
49
|
+
kind, separator, identity = value.partition(":")
|
|
50
|
+
if not separator:
|
|
51
|
+
raise click.BadParameter("使用 block:<id> 或 relation:<id>")
|
|
52
|
+
result = Entity.model_validate({"type": kind, "id": identity})
|
|
53
|
+
if block_only and result.type != "block":
|
|
54
|
+
raise click.BadParameter("此操作需要 block:<id>")
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def segment(value: str | int) -> str:
|
|
59
|
+
return quote(str(value), safe="")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def duration(value: str) -> float:
|
|
63
|
+
suffix = value[-1:] if value[-1:].isalpha() else ""
|
|
64
|
+
scale = {"": 1, "s": 1, "m": 60, "h": 3600}
|
|
65
|
+
try:
|
|
66
|
+
result = float(value[:-1] if suffix else value) * scale[suffix]
|
|
67
|
+
if not 0 < result < float("inf"):
|
|
68
|
+
raise ValueError
|
|
69
|
+
return result
|
|
70
|
+
except (ValueError, KeyError) as error:
|
|
71
|
+
raise click.BadParameter("使用正数秒,或 30s / 2m / 1h") from error
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Invocation:
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self.connection_name: str | None = None
|
|
77
|
+
self.connections_file: Path | None = None
|
|
78
|
+
self.output = Output()
|
|
79
|
+
self._client: CoreRESTClient | None = None
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def local_file(self) -> Path:
|
|
83
|
+
return connection.file_path(self.connections_file)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def client(self) -> CoreRESTClient:
|
|
87
|
+
if self._client is None:
|
|
88
|
+
_, selected = connection.select(connection.load(self.local_file), self.connection_name)
|
|
89
|
+
self._client = CoreRESTClient(selected)
|
|
90
|
+
return self._client
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
if self._client:
|
|
94
|
+
self._client.close()
|
|
95
|
+
|
|
96
|
+
def send(
|
|
97
|
+
self,
|
|
98
|
+
method: str,
|
|
99
|
+
path: str,
|
|
100
|
+
*,
|
|
101
|
+
params: dict | None = None,
|
|
102
|
+
body: Any = None,
|
|
103
|
+
strip: tuple[str, ...] = (),
|
|
104
|
+
selectors: dict[str, str] | None = None,
|
|
105
|
+
) -> Any:
|
|
106
|
+
try:
|
|
107
|
+
return self.client.request(method, path, params=params, body=body)
|
|
108
|
+
except CommandError as error:
|
|
109
|
+
if error.status == 422 and isinstance(error.detail, list):
|
|
110
|
+
detail = []
|
|
111
|
+
for item in error.detail:
|
|
112
|
+
loc = list(item.get("loc", ()))
|
|
113
|
+
if loc[:1] == ["body"]:
|
|
114
|
+
loc = loc[1:]
|
|
115
|
+
if loc[: len(strip)] == list(strip):
|
|
116
|
+
loc = loc[len(strip) :]
|
|
117
|
+
if loc and selectors and loc[0] in selectors:
|
|
118
|
+
loc[0] = selectors[loc[0]]
|
|
119
|
+
detail.append({**item, "loc": loc})
|
|
120
|
+
raise CommandError(detail, status=error.status) from error
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def show(self, value: Any, *, no_content: bool = False, partial_failure: bool = False) -> None:
|
|
124
|
+
self.output.emit(value, no_content=no_content)
|
|
125
|
+
if partial_failure:
|
|
126
|
+
raise CommandError("结果中存在逐项错误;成功项已保留")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _option(ctx: click.Context, parameter: click.Parameter, value: Any) -> Any:
|
|
130
|
+
invocation = ctx.ensure_object(Invocation)
|
|
131
|
+
if value is not None:
|
|
132
|
+
if parameter.name == "json_mode":
|
|
133
|
+
invocation.output.json_mode = value
|
|
134
|
+
elif parameter.name == "output_dir":
|
|
135
|
+
invocation.output.directory = value
|
|
136
|
+
else:
|
|
137
|
+
setattr(invocation, parameter.name, value)
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def common(function):
|
|
142
|
+
options: list[tuple[tuple[str, ...], dict[str, Any]]] = [
|
|
143
|
+
(("--connection", "connection_name"), {"help": "本次使用的本机连接名"}),
|
|
144
|
+
(("--connections-file",), {"type": click.Path(path_type=Path), "help": "替换连接配置文件"}),
|
|
145
|
+
(
|
|
146
|
+
("--output-dir",),
|
|
147
|
+
{"type": click.Path(path_type=Path), "help": "保存完整结果,返回入口文件"},
|
|
148
|
+
),
|
|
149
|
+
(
|
|
150
|
+
("--json", "json_mode"),
|
|
151
|
+
{"is_flag": True, "default": None, "is_eager": True, "help": "完整紧凑 JSON"},
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
for args, kwargs in options:
|
|
155
|
+
function = click.option(*args, callback=_option, expose_value=False, **kwargs)(function)
|
|
156
|
+
return function
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def input_options(function):
|
|
160
|
+
function = click.option("--schema", is_flag=True, help="只查询输入 JSON Schema,不执行操作")(
|
|
161
|
+
function
|
|
162
|
+
)
|
|
163
|
+
function = click.option("--input-json", help="直接提供 JSON 对象")(function)
|
|
164
|
+
return click.option(
|
|
165
|
+
"--input", "input_file", type=click.Path(path_type=Path), help="JSON 文件,- 表示 stdin"
|
|
166
|
+
)(function)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def paging(function):
|
|
170
|
+
function = click.option("--cursor", help="前一页返回的 next_cursor;保留相同过滤条件")(function)
|
|
171
|
+
return click.option(
|
|
172
|
+
"--limit", type=click.IntRange(min=1), help="本次最多返回多少项;不自动翻页"
|
|
173
|
+
)(function)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_input(input_file: Path | None, input_json: str | None) -> dict[str, Any]:
|
|
177
|
+
if input_file is not None and input_json is not None:
|
|
178
|
+
raise click.UsageError("--input 与 --input-json 不能同时使用")
|
|
179
|
+
if input_file is not None:
|
|
180
|
+
text = (
|
|
181
|
+
sys.stdin.read() if str(input_file) == "-" else input_file.read_text(encoding="utf-8")
|
|
182
|
+
)
|
|
183
|
+
elif input_json is not None:
|
|
184
|
+
text = input_json
|
|
185
|
+
else:
|
|
186
|
+
return {}
|
|
187
|
+
return TypeAdapter(dict[str, Any]).validate_json(text)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ordinary command groups, not an Extension command registry."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Agent definitions and read-only AI/profile discovery."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..command import Group, Invocation, common, input_options, load_input, paging, segment
|
|
6
|
+
from ..schema import request_schema
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(cls=Group)
|
|
10
|
+
def agent():
|
|
11
|
+
"""管理 Agent definition;不启动对话或本机 Agent runtime。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@agent.command("list")
|
|
15
|
+
@paging
|
|
16
|
+
@common
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def list_agents(inv: Invocation, limit, cursor):
|
|
19
|
+
"""列出 Agent definition。"""
|
|
20
|
+
inv.show(inv.send("GET", "/agents", params={"limit": limit, "cursor": cursor}))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@agent.command()
|
|
24
|
+
@click.argument("agent_id", type=int)
|
|
25
|
+
@common
|
|
26
|
+
@click.pass_obj
|
|
27
|
+
def get(inv: Invocation, agent_id):
|
|
28
|
+
"""读取一个 Agent definition。"""
|
|
29
|
+
inv.show(inv.send("GET", f"/agents/{agent_id}"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@agent.command()
|
|
33
|
+
@input_options
|
|
34
|
+
@common
|
|
35
|
+
@click.pass_obj
|
|
36
|
+
def create(inv: Invocation, schema, input_file, input_json):
|
|
37
|
+
"""创建 Agent definition;模型用 ai models、工具用 agent tools 发现。"""
|
|
38
|
+
if schema:
|
|
39
|
+
return inv.show(request_schema(inv.client, "/agents", "POST"))
|
|
40
|
+
inv.show(inv.send("POST", "/agents", body=load_input(input_file, input_json)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@agent.command()
|
|
44
|
+
@click.argument("agent_id", type=int)
|
|
45
|
+
@input_options
|
|
46
|
+
@common
|
|
47
|
+
@click.pass_obj
|
|
48
|
+
def update(inv: Invocation, agent_id, schema, input_file, input_json):
|
|
49
|
+
"""修改提交的 Agent definition 字段。"""
|
|
50
|
+
if schema:
|
|
51
|
+
return inv.show(request_schema(inv.client, "/agents/{agent_id}", "PATCH"))
|
|
52
|
+
inv.show(inv.send("PATCH", f"/agents/{agent_id}", body=load_input(input_file, input_json)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@agent.command()
|
|
56
|
+
@click.argument("agent_id", type=int)
|
|
57
|
+
@common
|
|
58
|
+
@click.pass_obj
|
|
59
|
+
def delete(inv: Invocation, agent_id):
|
|
60
|
+
"""删除 Agent definition。"""
|
|
61
|
+
inv.show(inv.send("DELETE", f"/agents/{agent_id}"), no_content=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@agent.command()
|
|
65
|
+
@click.argument("tool_id", required=False)
|
|
66
|
+
@paging
|
|
67
|
+
@common
|
|
68
|
+
@click.pass_obj
|
|
69
|
+
def tools(inv: Invocation, tool_id, limit, cursor):
|
|
70
|
+
"""列出本机已注册 Agent Tool;给定 ID 时取得完整合同,不执行工具。"""
|
|
71
|
+
inv.show(
|
|
72
|
+
inv.send(
|
|
73
|
+
"GET",
|
|
74
|
+
"/agent-tools" + ("/" + segment(tool_id) if tool_id else ""),
|
|
75
|
+
params={"limit": limit, "cursor": cursor},
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@click.group(cls=Group)
|
|
81
|
+
def ai():
|
|
82
|
+
"""发现 AI 模型,不混入 Agent definition 的管理范围。"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@ai.command()
|
|
86
|
+
@click.argument("model_id", type=int, required=False)
|
|
87
|
+
@paging
|
|
88
|
+
@common
|
|
89
|
+
@click.pass_obj
|
|
90
|
+
def models(inv: Invocation, model_id, limit, cursor):
|
|
91
|
+
"""列出 AIModel,或读取指定 ID;不返回 Provider 的 secret config。"""
|
|
92
|
+
inv.show(
|
|
93
|
+
inv.send(
|
|
94
|
+
"GET",
|
|
95
|
+
"/ai/models" + (f"/{model_id}" if model_id is not None else ""),
|
|
96
|
+
params={"limit": limit, "cursor": cursor},
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.group("embedding-profile", cls=Group)
|
|
102
|
+
def embedding_profile():
|
|
103
|
+
"""只读发现语义检索可选的 embedding profile。"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@embedding_profile.command("list")
|
|
107
|
+
@paging
|
|
108
|
+
@common
|
|
109
|
+
@click.pass_obj
|
|
110
|
+
def list_profiles(inv: Invocation, limit, cursor):
|
|
111
|
+
"""列出 EmbeddingProfile。"""
|
|
112
|
+
inv.show(inv.send("GET", "/embedding-profiles", params={"limit": limit, "cursor": cursor}))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@embedding_profile.command("get")
|
|
116
|
+
@click.argument("profile_id", type=int)
|
|
117
|
+
@common
|
|
118
|
+
@click.pass_obj
|
|
119
|
+
def get_profile(inv: Invocation, profile_id):
|
|
120
|
+
"""读取 EmbeddingProfile。"""
|
|
121
|
+
inv.show(inv.send("GET", f"/embedding-profiles/{profile_id}"))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Deployment config values and their runtime-owned input schemas."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..command import Group, Invocation, common, input_options, load_input, paging, segment
|
|
6
|
+
from ..schema import partial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group(cls=Group)
|
|
10
|
+
def config():
|
|
11
|
+
"""远端 deployment 配置,与本机 connection 文件分开。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@config.command("list")
|
|
15
|
+
@paging
|
|
16
|
+
@common
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def list_configs(inv: Invocation, limit, cursor):
|
|
19
|
+
"""列出保存的 deployment config,不要求 schema 已加载。"""
|
|
20
|
+
inv.show(inv.send("GET", "/configs", params={"limit": limit, "cursor": cursor}))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@config.command()
|
|
24
|
+
@click.argument("key")
|
|
25
|
+
@common
|
|
26
|
+
@click.pass_obj
|
|
27
|
+
def get(inv: Invocation, key):
|
|
28
|
+
"""读取配置的实际 schema/value。"""
|
|
29
|
+
inv.show(inv.send("GET", "/configs/" + segment(key)))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@config.command()
|
|
33
|
+
@click.argument("schema_id", required=False)
|
|
34
|
+
@paging
|
|
35
|
+
@common
|
|
36
|
+
@click.pass_obj
|
|
37
|
+
def schemas(inv: Invocation, schema_id, limit, cursor):
|
|
38
|
+
"""发现接入 Peer 已加载的 schema;给出 ID 时读取输入合同。"""
|
|
39
|
+
inv.show(
|
|
40
|
+
inv.send(
|
|
41
|
+
"GET",
|
|
42
|
+
"/config-schemas" + ("/" + segment(schema_id) if schema_id else ""),
|
|
43
|
+
params={"limit": limit, "cursor": cursor},
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@config.command()
|
|
49
|
+
@click.argument("key")
|
|
50
|
+
@click.option("--schema-id", required=True)
|
|
51
|
+
@input_options
|
|
52
|
+
@common
|
|
53
|
+
@click.pass_obj
|
|
54
|
+
def replace(inv: Invocation, key, schema_id, schema, input_file, input_json):
|
|
55
|
+
"""创建或完整替换;JSON 是 value 本身,不再包装 value。"""
|
|
56
|
+
if schema:
|
|
57
|
+
return inv.show(inv.send("GET", "/config-schemas/" + segment(schema_id))["input_schema"])
|
|
58
|
+
inv.show(
|
|
59
|
+
inv.send(
|
|
60
|
+
"PUT",
|
|
61
|
+
"/configs/" + segment(key),
|
|
62
|
+
body={
|
|
63
|
+
"schema": schema_id,
|
|
64
|
+
"value": load_input(input_file, input_json),
|
|
65
|
+
},
|
|
66
|
+
strip=("value",),
|
|
67
|
+
selectors={"schema": "--schema-id"},
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@config.command()
|
|
73
|
+
@click.argument("key")
|
|
74
|
+
@input_options
|
|
75
|
+
@common
|
|
76
|
+
@click.pass_obj
|
|
77
|
+
def update(inv: Invocation, key, schema, input_file, input_json):
|
|
78
|
+
"""更新 value 的提交字段;嵌套值按 owner 合同处理。"""
|
|
79
|
+
if schema:
|
|
80
|
+
existing = inv.send("GET", "/configs/" + segment(key))
|
|
81
|
+
return inv.show(
|
|
82
|
+
partial(
|
|
83
|
+
inv.send("GET", "/config-schemas/" + segment(existing["schema"]))["input_schema"]
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
inv.show(inv.send("PATCH", "/configs/" + segment(key), body=load_input(input_file, input_json)))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@config.command()
|
|
90
|
+
@click.argument("key")
|
|
91
|
+
@common
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
def delete(inv: Invocation, key):
|
|
94
|
+
"""删除保存的配置值。"""
|
|
95
|
+
inv.show(inv.send("DELETE", "/configs/" + segment(key)), no_content=True)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Offline connection editing and explicit connectivity observation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .. import connection as store
|
|
9
|
+
from ..command import Group, Invocation, common, input_options, load_input
|
|
10
|
+
from ..errors import CommandError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(cls=Group)
|
|
14
|
+
def connection():
|
|
15
|
+
"""本机命名连接,不是远端 deployment config 或 Peer 注册。"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@connection.command("list")
|
|
19
|
+
@common
|
|
20
|
+
@click.pass_obj
|
|
21
|
+
def list_connections(inv: Invocation):
|
|
22
|
+
"""离线列出名称、地址与保存的默认名。"""
|
|
23
|
+
data = store.load(inv.local_file)
|
|
24
|
+
inv.show(
|
|
25
|
+
{
|
|
26
|
+
"default": data.default,
|
|
27
|
+
"connections": {
|
|
28
|
+
name: {"base_url": str(value.base_url)} for name, value in data.connections.items()
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@connection.command()
|
|
35
|
+
@click.argument("name")
|
|
36
|
+
@common
|
|
37
|
+
@click.pass_obj
|
|
38
|
+
def get(inv: Invocation, name):
|
|
39
|
+
"""离线读取指定连接,包括保存的 JWT secret。"""
|
|
40
|
+
_, value = store.select(store.load(inv.local_file), name)
|
|
41
|
+
inv.show(value.model_dump(mode="json"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@connection.command("set")
|
|
45
|
+
@click.argument("name")
|
|
46
|
+
@input_options
|
|
47
|
+
@common
|
|
48
|
+
@click.pass_obj
|
|
49
|
+
def set_connection(inv: Invocation, name, schema, input_file, input_json):
|
|
50
|
+
"""创建或完整替换命名连接;输入 base_url 与 jwt_secret。"""
|
|
51
|
+
if schema:
|
|
52
|
+
return inv.show(store.Connection.model_json_schema())
|
|
53
|
+
value = store.Connection.model_validate(load_input(input_file, input_json))
|
|
54
|
+
data = store.load(inv.local_file)
|
|
55
|
+
data.connections[name] = value
|
|
56
|
+
if data.default is None:
|
|
57
|
+
data.default = name
|
|
58
|
+
store.save(inv.local_file, data)
|
|
59
|
+
inv.show({"name": name, "base_url": str(value.base_url), "default": data.default})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@connection.command()
|
|
63
|
+
@click.argument("name")
|
|
64
|
+
@common
|
|
65
|
+
@click.pass_obj
|
|
66
|
+
def use(inv: Invocation, name):
|
|
67
|
+
"""修改本机默认连接。"""
|
|
68
|
+
data = store.load(inv.local_file)
|
|
69
|
+
store.select(data, name)
|
|
70
|
+
data.default = name
|
|
71
|
+
store.save(inv.local_file, data)
|
|
72
|
+
inv.show({"default": name})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@connection.command()
|
|
76
|
+
@click.argument("name")
|
|
77
|
+
@common
|
|
78
|
+
@click.pass_obj
|
|
79
|
+
def delete(inv: Invocation, name):
|
|
80
|
+
"""删除本机连接;不操作远端部署。"""
|
|
81
|
+
data = store.load(inv.local_file)
|
|
82
|
+
store.select(data, name)
|
|
83
|
+
del data.connections[name]
|
|
84
|
+
if data.default == name:
|
|
85
|
+
data.default = None
|
|
86
|
+
store.save(inv.local_file, data)
|
|
87
|
+
inv.show(None, no_content=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@connection.command()
|
|
91
|
+
@common
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
def check(inv: Invocation):
|
|
94
|
+
"""分别检查公共 readyz 与受保护读取,不把就绪误当成认证成功。"""
|
|
95
|
+
result: dict[str, Any] = {"endpoint": inv.client.base_url}
|
|
96
|
+
failed = False
|
|
97
|
+
for key, path, authenticated in (
|
|
98
|
+
("readyz", "/readyz", False),
|
|
99
|
+
("authenticated_read", "/peers/self", True),
|
|
100
|
+
):
|
|
101
|
+
try:
|
|
102
|
+
result[key] = inv.client.request("GET", path, authenticated=authenticated)
|
|
103
|
+
except CommandError as error:
|
|
104
|
+
result[key] = {"error": error.as_dict()}
|
|
105
|
+
failed = True
|
|
106
|
+
except httpx.RequestError as error:
|
|
107
|
+
result[key] = {"error": {"detail": str(error)}}
|
|
108
|
+
failed = True
|
|
109
|
+
inv.show(result, partial_failure=failed)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Extension Host REST management; no CLI-specific Extension adapters."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..command import Group, Invocation, common, input_options, load_input, paging, segment
|
|
6
|
+
from ..errors import CommandError
|
|
7
|
+
from ..schema import partial
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def route(coordinate: str) -> str:
|
|
11
|
+
namespace, separator, name = coordinate.partition("/")
|
|
12
|
+
if not separator or not namespace or not name or "/" in name:
|
|
13
|
+
raise click.BadParameter("Extension 使用 namespace/name coordinate")
|
|
14
|
+
return "/extensions/" + segment(namespace) + "/" + segment(name)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group(cls=Group)
|
|
18
|
+
def extension():
|
|
19
|
+
"""安装、配置和启停 Extension;发布仍由 Extension 的交付流程负责。"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@extension.command("list")
|
|
23
|
+
@paging
|
|
24
|
+
@common
|
|
25
|
+
@click.pass_obj
|
|
26
|
+
def list_extensions(inv: Invocation, limit, cursor):
|
|
27
|
+
"""列出 Extension 的安装、启用意图与运行状态。"""
|
|
28
|
+
inv.show(inv.send("GET", "/extensions", params={"limit": limit, "cursor": cursor}))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@extension.command()
|
|
32
|
+
@click.argument("coordinate")
|
|
33
|
+
@common
|
|
34
|
+
@click.pass_obj
|
|
35
|
+
def get(inv: Invocation, coordinate):
|
|
36
|
+
"""读取一个已安装 Extension。"""
|
|
37
|
+
inv.show(inv.send("GET", route(coordinate)))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@extension.command()
|
|
41
|
+
@click.argument("coordinate")
|
|
42
|
+
@click.option("--version", required=True, help="准确的已发布版本,不解析 latest")
|
|
43
|
+
@common
|
|
44
|
+
@click.pass_obj
|
|
45
|
+
def install(inv: Invocation, coordinate, version):
|
|
46
|
+
"""安装准确版本;不隐式启用。"""
|
|
47
|
+
inv.show(inv.send("POST", route(coordinate), params={"version": version}))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@extension.command()
|
|
51
|
+
@click.argument("coordinate")
|
|
52
|
+
@common
|
|
53
|
+
@click.pass_obj
|
|
54
|
+
def uninstall(inv: Invocation, coordinate):
|
|
55
|
+
"""卸载 Extension。"""
|
|
56
|
+
inv.show(inv.send("DELETE", route(coordinate)), no_content=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@extension.command()
|
|
60
|
+
@click.argument("coordinate")
|
|
61
|
+
@click.option("--peer", help="启用意图的目标 Peer;省略时为接入 Peer")
|
|
62
|
+
@common
|
|
63
|
+
@click.pass_obj
|
|
64
|
+
def enable(inv: Invocation, coordinate, peer):
|
|
65
|
+
"""在指定或当前 Peer 启用 Extension。"""
|
|
66
|
+
inv.show(inv.send("POST", route(coordinate) + "/enable", params={"route_to_peer": peer}))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@extension.command()
|
|
70
|
+
@click.argument("coordinate")
|
|
71
|
+
@click.option("--peer", help="禁用意图的目标 Peer;省略时为接入 Peer")
|
|
72
|
+
@common
|
|
73
|
+
@click.pass_obj
|
|
74
|
+
def disable(inv: Invocation, coordinate, peer):
|
|
75
|
+
"""在指定或当前 Peer 禁用 Extension。"""
|
|
76
|
+
inv.show(inv.send("POST", route(coordinate) + "/disable", params={"route_to_peer": peer}))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@extension.group()
|
|
80
|
+
def config():
|
|
81
|
+
"""读写 Extension 的持久配置,不要求先启用。"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@config.command("get")
|
|
85
|
+
@click.argument("coordinate")
|
|
86
|
+
@common
|
|
87
|
+
@click.pass_obj
|
|
88
|
+
def get_config(inv: Invocation, coordinate):
|
|
89
|
+
"""从安装记录读取 config;不调用业务 schema 验证。"""
|
|
90
|
+
inv.show(inv.send("GET", route(coordinate))["config"])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def config_schema(inv: Invocation, coordinate: str) -> dict:
|
|
94
|
+
contract = inv.send("GET", route(coordinate))["config_schema"]
|
|
95
|
+
if contract is None:
|
|
96
|
+
raise CommandError("该 Extension 尚未发布配置 schema;可在启用后重新发现")
|
|
97
|
+
return contract
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@config.command("replace")
|
|
101
|
+
@click.argument("coordinate")
|
|
102
|
+
@input_options
|
|
103
|
+
@common
|
|
104
|
+
@click.pass_obj
|
|
105
|
+
def replace_config(inv: Invocation, coordinate, schema, input_file, input_json):
|
|
106
|
+
"""完整替换 Extension config。"""
|
|
107
|
+
if schema:
|
|
108
|
+
return inv.show(config_schema(inv, coordinate))
|
|
109
|
+
inv.show(
|
|
110
|
+
inv.send("PUT", route(coordinate) + "/config", body=load_input(input_file, input_json))
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@config.command("update")
|
|
115
|
+
@click.argument("coordinate")
|
|
116
|
+
@input_options
|
|
117
|
+
@common
|
|
118
|
+
@click.pass_obj
|
|
119
|
+
def update_config(inv: Invocation, coordinate, schema, input_file, input_json):
|
|
120
|
+
"""更新提交的 config 字段。"""
|
|
121
|
+
if schema:
|
|
122
|
+
return inv.show(partial(config_schema(inv, coordinate)))
|
|
123
|
+
inv.show(
|
|
124
|
+
inv.send("PATCH", route(coordinate) + "/config", body=load_input(input_file, input_json))
|
|
125
|
+
)
|