inkcre-cli 0.1.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.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: inkcre-cli
3
+ Version: 0.1.0
4
+ Summary: An agent-friendly REST interface to InKCre
5
+ License-Expression: GPL-3.0-only
6
+ Project-URL: Repository, https://github.com/InKCre/core-py
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: click<9,>=8.1
9
+ Requires-Dist: pydantic<3,>=2.10
10
+ Requires-Dist: httpx<0.29,>=0.28.1
11
+ Requires-Dist: PyJWT<3,>=2.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # inkcre-cli
15
+
16
+ InKCre 的独立命令行界面,通过 Core REST API 检索信息、读取 Resolver 内容和管理部署。
17
+ 它不安装 Core、不访问数据库,也不是执行 Job 的 Peer。
18
+
19
+ ```sh
20
+ python -m pip install inkcre-cli
21
+ inkcre-cli --help
22
+ inkcre-cli connection set personal --input connection.json
23
+ inkcre-cli recall 'Agent 工具' --mode lexical --json
24
+ inkcre-cli get block:42 relation:8 --json
25
+ inkcre-cli resolver invoke block:42 --method get_solved_content
26
+ ```
27
+
28
+ connection.json 包含 `base_url` 与 `jwt_secret`。默认连接文件是
29
+ `~/.inkcre/cli/connections.json`;可用 `--connections-file` 指定隔离文件。
30
+ 这里的地址是 Core REST 入口,不是 PostgREST 或 MCP endpoint。
31
+ 首个连接自动成为默认连接;`connection use personal` 修改默认值,`--connection NAME` 只选择本次接入。
32
+
33
+ `--help` 离线可用;有结构化输入的命令提供 `--schema`,从当前 Core 发现准确合同。
34
+ 使用 `--input FILE`、`--input -` 或 `--input-json JSON` 提供输入。
35
+ `--json` 输出完整紧凑 JSON;二进制内容保存为文件,不做 Base64 编码。
36
+ `--output-dir DIR` 显式保存完整结果,stdout 返回入口文件路径。
37
+
38
+ ## 发现与管理
39
+
40
+ 先发现 exact type、method 或 schema,再提交它所描述的输入。例如:
41
+
42
+ ```sh
43
+ inkcre-cli source types --json
44
+ inkcre-cli source create --type SOURCE_TYPE --schema
45
+ inkcre-cli source create --type SOURCE_TYPE --input source.json
46
+ inkcre-cli source collect 42 --input-json '{}'
47
+ inkcre-cli job wait 17 --for 10s --json
48
+ inkcre-cli job abort 17
49
+ inkcre-cli cron create --job-type core.source.collect.v1 --schema
50
+ inkcre-cli agent tools --json
51
+ inkcre-cli ai models --json
52
+ inkcre-cli config schemas --json
53
+ ```
54
+
55
+ 这里的 ID 仅为示例,应使用实际创建回执或查询返回的 ID。`source collect`、`source backfill` 和
56
+ `organization ruminate` 创建 Job,而不是在 CLI 或接入 Core 中同步执行。`job wait --for 10s` 最多观察
57
+ 十秒,可能返回仍为 pending/running 的记录;停止观察不取消 Job。`job abort` 请求 best-effort 停止,
58
+ `abort_requested: true` 不代表执行端已经完成清理。只有后续记录才能说明实际结果。
59
+
60
+ `connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理,
61
+ 模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。
62
+
63
+ ## 输出与续读
64
+
65
+ 结果写 stdout,命令错误写 stderr。退出码 0 表示命令成功,1 表示远端/IO/部分结果失败,2 表示本机输入
66
+ 错误,130 表示观察被中断。读取一个状态为 failed 的 Job 仍是成功读取,脚本应检查 Job 的实际 status。
67
+ 批量读取与混合 recall 保留各项结果,不因一项失败丢弃其它成功项。
68
+
69
+ 支持分页的目录使用 `--limit`、`--cursor` 和返回的 `next_cursor`;CLI 不自动翻页。
70
+ 检索仍保留各模式自身的 limit 和结果结构,混合 recall 不融合排名。
71
+
72
+ 人类可读输出过长时显示摘要和完整文件路径;`--json` 不截断、不 pretty-print。
73
+ 二进制自动保存为文件;显式 `--output-dir DIR` 导出的入口 JSON 使用相对文件引用,整个子目录可以搬移。
74
+ 这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。
75
+
76
+ 开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。
77
+ 0.0.0 是未发布的开发版本;首次发行由仓库的 Release PR 准备。
@@ -0,0 +1,64 @@
1
+ # inkcre-cli
2
+
3
+ InKCre 的独立命令行界面,通过 Core REST API 检索信息、读取 Resolver 内容和管理部署。
4
+ 它不安装 Core、不访问数据库,也不是执行 Job 的 Peer。
5
+
6
+ ```sh
7
+ python -m pip install inkcre-cli
8
+ inkcre-cli --help
9
+ inkcre-cli connection set personal --input connection.json
10
+ inkcre-cli recall 'Agent 工具' --mode lexical --json
11
+ inkcre-cli get block:42 relation:8 --json
12
+ inkcre-cli resolver invoke block:42 --method get_solved_content
13
+ ```
14
+
15
+ connection.json 包含 `base_url` 与 `jwt_secret`。默认连接文件是
16
+ `~/.inkcre/cli/connections.json`;可用 `--connections-file` 指定隔离文件。
17
+ 这里的地址是 Core REST 入口,不是 PostgREST 或 MCP endpoint。
18
+ 首个连接自动成为默认连接;`connection use personal` 修改默认值,`--connection NAME` 只选择本次接入。
19
+
20
+ `--help` 离线可用;有结构化输入的命令提供 `--schema`,从当前 Core 发现准确合同。
21
+ 使用 `--input FILE`、`--input -` 或 `--input-json JSON` 提供输入。
22
+ `--json` 输出完整紧凑 JSON;二进制内容保存为文件,不做 Base64 编码。
23
+ `--output-dir DIR` 显式保存完整结果,stdout 返回入口文件路径。
24
+
25
+ ## 发现与管理
26
+
27
+ 先发现 exact type、method 或 schema,再提交它所描述的输入。例如:
28
+
29
+ ```sh
30
+ inkcre-cli source types --json
31
+ inkcre-cli source create --type SOURCE_TYPE --schema
32
+ inkcre-cli source create --type SOURCE_TYPE --input source.json
33
+ inkcre-cli source collect 42 --input-json '{}'
34
+ inkcre-cli job wait 17 --for 10s --json
35
+ inkcre-cli job abort 17
36
+ inkcre-cli cron create --job-type core.source.collect.v1 --schema
37
+ inkcre-cli agent tools --json
38
+ inkcre-cli ai models --json
39
+ inkcre-cli config schemas --json
40
+ ```
41
+
42
+ 这里的 ID 仅为示例,应使用实际创建回执或查询返回的 ID。`source collect`、`source backfill` 和
43
+ `organization ruminate` 创建 Job,而不是在 CLI 或接入 Core 中同步执行。`job wait --for 10s` 最多观察
44
+ 十秒,可能返回仍为 pending/running 的记录;停止观察不取消 Job。`job abort` 请求 best-effort 停止,
45
+ `abort_requested: true` 不代表执行端已经完成清理。只有后续记录才能说明实际结果。
46
+
47
+ `connection` 只管理本机连接,`config` 管理远端 deployment config;Agent definition 由 `agent` 管理,
48
+ 模型目录由 `ai` 发现。Extension 配置和运行目标使用 `extension`,不混入 connection。
49
+
50
+ ## 输出与续读
51
+
52
+ 结果写 stdout,命令错误写 stderr。退出码 0 表示命令成功,1 表示远端/IO/部分结果失败,2 表示本机输入
53
+ 错误,130 表示观察被中断。读取一个状态为 failed 的 Job 仍是成功读取,脚本应检查 Job 的实际 status。
54
+ 批量读取与混合 recall 保留各项结果,不因一项失败丢弃其它成功项。
55
+
56
+ 支持分页的目录使用 `--limit`、`--cursor` 和返回的 `next_cursor`;CLI 不自动翻页。
57
+ 检索仍保留各模式自身的 limit 和结果结构,混合 recall 不融合排名。
58
+
59
+ 人类可读输出过长时显示摘要和完整文件路径;`--json` 不截断、不 pretty-print。
60
+ 二进制自动保存为文件;显式 `--output-dir DIR` 导出的入口 JSON 使用相对文件引用,整个子目录可以搬移。
61
+ 这些是同一次 HTTP 响应的本地文件,不是新下载会话,也不会再次调用 Resolver。
62
+
63
+ 开发使用独立 PDM 项目:`pdm install -p cli`、`pdm run -p cli check`、`pdm build -p cli`。
64
+ 0.0.0 是未发布的开发版本;首次发行由仓库的 Release PR 准备。
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "inkcre-cli"
3
+ version = "0.1.0"
4
+ description = "An agent-friendly REST interface to InKCre"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "GPL-3.0-only"
8
+ dependencies = [
9
+ "click>=8.1,<9",
10
+ "pydantic>=2.10,<3",
11
+ "httpx>=0.28.1,<0.29",
12
+ "PyJWT>=2.10,<3",
13
+ ]
14
+
15
+ [project.scripts]
16
+ inkcre-cli = "inkcre_cli.main:main"
17
+
18
+ [project.urls]
19
+ Repository = "https://github.com/InKCre/core-py"
20
+
21
+ [build-system]
22
+ requires = [
23
+ "pdm-backend",
24
+ ]
25
+ build-backend = "pdm.backend"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "ruff>=0.15,<0.16",
30
+ "pyrefly>=0.56,<0.57",
31
+ ]
32
+
33
+ [tool.pdm]
34
+ distribution = true
35
+
36
+ [tool.pdm.scripts]
37
+ lint = "ruff check src"
38
+ format = "ruff format src"
39
+ "format:check" = "ruff format --check src"
40
+ typecheck = "pyrefly check"
41
+
42
+ [tool.pdm.scripts.check]
43
+ composite = [
44
+ "lint",
45
+ "format:check",
46
+ "typecheck",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ target-version = "py312"
52
+
53
+ [tool.ruff.lint]
54
+ select = [
55
+ "E",
56
+ "F",
57
+ "I",
58
+ "UP",
59
+ "B",
60
+ ]
61
+
62
+ [tool.pyrefly]
63
+ project-includes = [
64
+ "src",
65
+ ]
@@ -0,0 +1 @@
1
+ """Independent InKCre REST consumer; no Core runtime imports."""
@@ -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)