rathflow-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.
- rathflow_cli/__init__.py +6 -0
- rathflow_cli/__main__.py +6 -0
- rathflow_cli/cli.py +161 -0
- rathflow_cli/commands/__init__.py +0 -0
- rathflow_cli/commands/_common.py +78 -0
- rathflow_cli/commands/admin.py +83 -0
- rathflow_cli/commands/agent.py +288 -0
- rathflow_cli/commands/api_cmd.py +75 -0
- rathflow_cli/commands/asset.py +449 -0
- rathflow_cli/commands/auth_cmd.py +160 -0
- rathflow_cli/commands/billing.py +71 -0
- rathflow_cli/commands/config_cmd.py +98 -0
- rathflow_cli/commands/memory.py +275 -0
- rathflow_cli/commands/org.py +70 -0
- rathflow_cli/commands/project.py +223 -0
- rathflow_cli/commands/sandbox.py +282 -0
- rathflow_cli/commands/session.py +237 -0
- rathflow_cli/commands/workflow.py +81 -0
- rathflow_cli/config.py +69 -0
- rathflow_cli/endpoints.py +207 -0
- rathflow_cli/errors.py +70 -0
- rathflow_cli/http.py +294 -0
- rathflow_cli/output.py +95 -0
- rathflow_cli/selftest.py +120 -0
- rathflow_cli/state.py +129 -0
- rathflow_cli-0.1.0.dist-info/METADATA +164 -0
- rathflow_cli-0.1.0.dist-info/RECORD +31 -0
- rathflow_cli-0.1.0.dist-info/WHEEL +5 -0
- rathflow_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rathflow_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- rathflow_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""session:会话、block、事件流与 block 分享。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .. import output
|
|
8
|
+
from ..commands._common import (
|
|
9
|
+
BODY_FILE_OPT,
|
|
10
|
+
BODY_OPT,
|
|
11
|
+
PAGE_SIZE_OPT,
|
|
12
|
+
PAGE_TOKEN_OPT,
|
|
13
|
+
body_of,
|
|
14
|
+
pagination,
|
|
15
|
+
)
|
|
16
|
+
from ..errors import UsageError
|
|
17
|
+
from ..state import current
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(no_args_is_help=True, help="会话 / block / 事件流")
|
|
20
|
+
|
|
21
|
+
SESSION_COLUMNS = [
|
|
22
|
+
("SESSION ID", "sessionId"),
|
|
23
|
+
("状态", "status"),
|
|
24
|
+
("标题", "title"),
|
|
25
|
+
("AGENT", "agentDefId"),
|
|
26
|
+
("创建时间", "createdAt"),
|
|
27
|
+
]
|
|
28
|
+
BLOCK_COLUMNS = [
|
|
29
|
+
("BLOCK ID", "blockId"),
|
|
30
|
+
("标题", "title"),
|
|
31
|
+
("事件区间", "fromSeq"),
|
|
32
|
+
("到", "toSeq"),
|
|
33
|
+
("创建时间", "createdAt"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------ 会话
|
|
38
|
+
|
|
39
|
+
@app.command("list")
|
|
40
|
+
def list_(
|
|
41
|
+
page_size: int = PAGE_SIZE_OPT,
|
|
42
|
+
page_token: str = PAGE_TOKEN_OPT,
|
|
43
|
+
status: str = typer.Option(None, "--status", help="按状态过滤"),
|
|
44
|
+
) -> None:
|
|
45
|
+
"""列出本项目会话。"""
|
|
46
|
+
st = current()
|
|
47
|
+
payload = st.client().call(
|
|
48
|
+
"session.ListSessions", query=pagination(page_size or 50, page_token, {"status": status})
|
|
49
|
+
)
|
|
50
|
+
output.emit_items(payload, items_key="sessions", columns=SESSION_COLUMNS, json_out=st.json_out)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("get")
|
|
54
|
+
def get(session_id: str = typer.Argument(...)) -> None:
|
|
55
|
+
"""会话详情。"""
|
|
56
|
+
st = current()
|
|
57
|
+
payload = st.client().call("session.GetSession", path_params={"session_id": session_id})
|
|
58
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("create")
|
|
62
|
+
def create(
|
|
63
|
+
body: str = BODY_OPT,
|
|
64
|
+
body_file: str = BODY_FILE_OPT,
|
|
65
|
+
title: str = typer.Option(None, "--title", help="标题(等价于 --body 里给 title)"),
|
|
66
|
+
) -> None:
|
|
67
|
+
"""建会话。"""
|
|
68
|
+
st = current()
|
|
69
|
+
payload_body = body_of(body, body_file)
|
|
70
|
+
if payload_body is None:
|
|
71
|
+
payload_body = {"title": title} if title else {}
|
|
72
|
+
payload = st.client().call("session.CreateSession", body=payload_body)
|
|
73
|
+
if st.json_out:
|
|
74
|
+
output.emit_json(payload)
|
|
75
|
+
return
|
|
76
|
+
typer.echo(payload.get("sessionId") or "")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("delete")
|
|
80
|
+
def delete(
|
|
81
|
+
session_id: str = typer.Argument(...),
|
|
82
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""删会话。"""
|
|
85
|
+
st = current()
|
|
86
|
+
if not yes:
|
|
87
|
+
typer.confirm(f"确认删除会话 {session_id}?", abort=True)
|
|
88
|
+
st.client().call("session.DeleteSession", path_params={"session_id": session_id})
|
|
89
|
+
typer.echo(f"已删除 {session_id}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command("archive")
|
|
93
|
+
def archive(session_id: str = typer.Argument(...)) -> None:
|
|
94
|
+
"""归档会话。"""
|
|
95
|
+
st = current()
|
|
96
|
+
payload = st.client().call("session.ArchiveSession", path_params={"session_id": session_id})
|
|
97
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command("lineage")
|
|
101
|
+
def lineage(session_id: str = typer.Argument(...)) -> None:
|
|
102
|
+
"""血统(fork/派生关系)。"""
|
|
103
|
+
st = current()
|
|
104
|
+
payload = st.client().call("session.GetSessionLineage", path_params={"session_id": session_id})
|
|
105
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("interrupt")
|
|
109
|
+
def interrupt(session_id: str = typer.Argument(...)) -> None:
|
|
110
|
+
"""打断正在跑的 run(agent.Interrupt)。"""
|
|
111
|
+
st = current()
|
|
112
|
+
payload = st.client().call("agent.Interrupt", path_params={"session_id": session_id}, body={})
|
|
113
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------ block
|
|
117
|
+
|
|
118
|
+
@app.command("blocks")
|
|
119
|
+
def blocks(
|
|
120
|
+
session_id: str = typer.Argument(...),
|
|
121
|
+
page_size: int = PAGE_SIZE_OPT,
|
|
122
|
+
page_token: str = PAGE_TOKEN_OPT,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""列会话下的 block。"""
|
|
125
|
+
st = current()
|
|
126
|
+
payload = st.client().call(
|
|
127
|
+
"session.ListBlocks",
|
|
128
|
+
path_params={"session_id": session_id},
|
|
129
|
+
query=pagination(page_size or 50, page_token),
|
|
130
|
+
)
|
|
131
|
+
output.emit_items(payload, items_key="blocks", columns=BLOCK_COLUMNS, json_out=st.json_out)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command("block")
|
|
135
|
+
def block_get(block_id: str = typer.Argument(...)) -> None:
|
|
136
|
+
"""看单个 block。"""
|
|
137
|
+
st = current()
|
|
138
|
+
payload = st.client().call("session.GetBlock", path_params={"block_id": block_id})
|
|
139
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("block-create")
|
|
143
|
+
def block_create(
|
|
144
|
+
session_id: str = typer.Argument(...),
|
|
145
|
+
body: str = BODY_OPT,
|
|
146
|
+
body_file: str = BODY_FILE_OPT,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""给会话加 block(内容用 --body/--body-file 传)。"""
|
|
149
|
+
st = current()
|
|
150
|
+
payload_body = body_of(body, body_file)
|
|
151
|
+
if payload_body is None:
|
|
152
|
+
raise UsageError('需要 --body \'{"type":...,...}\' 或 --body-file <路径|->')
|
|
153
|
+
payload = st.client().call(
|
|
154
|
+
"session.CreateBlock", path_params={"session_id": session_id}, body=payload_body
|
|
155
|
+
)
|
|
156
|
+
if st.json_out:
|
|
157
|
+
output.emit_json(payload)
|
|
158
|
+
return
|
|
159
|
+
typer.echo(payload.get("blockId") or "")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.command("block-delete")
|
|
163
|
+
def block_delete(
|
|
164
|
+
block_id: str = typer.Argument(...),
|
|
165
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
166
|
+
) -> None:
|
|
167
|
+
"""删 block。"""
|
|
168
|
+
st = current()
|
|
169
|
+
if not yes:
|
|
170
|
+
typer.confirm(f"确认删除 block {block_id}?", abort=True)
|
|
171
|
+
st.client().call("session.DeleteBlock", path_params={"block_id": block_id})
|
|
172
|
+
typer.echo(f"已删除 {block_id}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("share")
|
|
176
|
+
def share(
|
|
177
|
+
block_id: str = typer.Argument(...),
|
|
178
|
+
body: str = BODY_OPT,
|
|
179
|
+
body_file: str = BODY_FILE_OPT,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""把 block 分享给别人(scope 用 --body 传)。"""
|
|
182
|
+
st = current()
|
|
183
|
+
payload = st.client().call(
|
|
184
|
+
"session.ShareBlock", path_params={"block_id": block_id}, body=body_of(body, body_file) or {}
|
|
185
|
+
)
|
|
186
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command("unshare")
|
|
190
|
+
def unshare(share_id: str = typer.Argument(...)) -> None:
|
|
191
|
+
"""撤销 block 分享。"""
|
|
192
|
+
st = current()
|
|
193
|
+
st.client().call("session.RevokeBlockShare", path_params={"share_id": share_id})
|
|
194
|
+
typer.echo(f"已撤销 {share_id}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------ 事件
|
|
198
|
+
|
|
199
|
+
@app.command("events")
|
|
200
|
+
def events(
|
|
201
|
+
session_id: str = typer.Argument(...),
|
|
202
|
+
page_size: int = PAGE_SIZE_OPT,
|
|
203
|
+
page_token: str = PAGE_TOKEN_OPT,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""拉历史事件(一次性)。"""
|
|
206
|
+
st = current()
|
|
207
|
+
payload = st.client().call(
|
|
208
|
+
"session.GetSessionEvents",
|
|
209
|
+
path_params={"session_id": session_id},
|
|
210
|
+
query=pagination(page_size or 100, page_token),
|
|
211
|
+
)
|
|
212
|
+
output.emit_items(
|
|
213
|
+
payload,
|
|
214
|
+
items_key="events",
|
|
215
|
+
columns=[("SEQ", "seq"), ("数据", "payloadJson"), ("时间", "createdAt")],
|
|
216
|
+
json_out=st.json_out,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command("watch")
|
|
221
|
+
def watch(
|
|
222
|
+
session_id: str = typer.Argument(...),
|
|
223
|
+
raw: bool = typer.Option(False, "--raw", help="逐帧原样打印 JSON,不渲染"),
|
|
224
|
+
from_seq: int = typer.Option(None, "--from-seq", help="从该 seq 续传(断线重连用)"),
|
|
225
|
+
) -> None:
|
|
226
|
+
"""跟事件流(:stream,长连,Ctrl-C 退出)。"""
|
|
227
|
+
st = current()
|
|
228
|
+
query = {"from_seq": from_seq} if from_seq is not None else None
|
|
229
|
+
for frame in st.client().stream(
|
|
230
|
+
"session.StreamSessionEvents", path_params={"session_id": session_id}, query=query
|
|
231
|
+
):
|
|
232
|
+
if st.json_out or raw:
|
|
233
|
+
output.emit_json(frame)
|
|
234
|
+
continue
|
|
235
|
+
seq = frame.get("seq")
|
|
236
|
+
prefix = f"{str(seq).rjust(6)} " if seq is not None else ""
|
|
237
|
+
typer.echo(prefix + output._cell(frame.get("payloadJson") or frame))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""workflow:工作流定义与删除。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .. import output
|
|
8
|
+
from ..commands._common import (
|
|
9
|
+
BODY_FILE_OPT,
|
|
10
|
+
BODY_OPT,
|
|
11
|
+
PAGE_SIZE_OPT,
|
|
12
|
+
PAGE_TOKEN_OPT,
|
|
13
|
+
body_of,
|
|
14
|
+
pagination,
|
|
15
|
+
)
|
|
16
|
+
from ..state import current
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(no_args_is_help=True, help="工作流")
|
|
19
|
+
|
|
20
|
+
COLUMNS = [
|
|
21
|
+
("WORKFLOW ID", "workflowId"),
|
|
22
|
+
("标题", "title"),
|
|
23
|
+
("状态", "status"),
|
|
24
|
+
("更新时间", "updatedAt"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("list")
|
|
29
|
+
def list_(
|
|
30
|
+
page_size: int = PAGE_SIZE_OPT,
|
|
31
|
+
page_token: str = PAGE_TOKEN_OPT,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""列工作流。"""
|
|
34
|
+
st = current()
|
|
35
|
+
payload = st.client().call("workflow.ListWorkflows", query=pagination(page_size or 50, page_token))
|
|
36
|
+
output.emit_items(payload, items_key="workflows", columns=COLUMNS, json_out=st.json_out)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("get")
|
|
40
|
+
def get(workflow_id: str = typer.Argument(...)) -> None:
|
|
41
|
+
"""工作流详情。"""
|
|
42
|
+
st = current()
|
|
43
|
+
payload = st.client().call("workflow.GetWorkflow", path_params={"workflow_id": workflow_id})
|
|
44
|
+
output.emit_object(payload, json_out=st.json_out)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("create")
|
|
48
|
+
def create(
|
|
49
|
+
title: str = typer.Option(None, "--title", "-t"),
|
|
50
|
+
body: str = BODY_OPT,
|
|
51
|
+
body_file: str = BODY_FILE_OPT,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""建工作流(图定义用 --body 传)。"""
|
|
54
|
+
st = current()
|
|
55
|
+
payload_body = body_of(body, body_file) or {}
|
|
56
|
+
if title:
|
|
57
|
+
payload_body.setdefault("title", title)
|
|
58
|
+
payload = st.client().call("workflow.CreateWorkflow", body=payload_body)
|
|
59
|
+
if st.json_out:
|
|
60
|
+
output.emit_json(payload)
|
|
61
|
+
return
|
|
62
|
+
typer.echo(payload.get("workflowId") or "")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("delete")
|
|
66
|
+
def delete(
|
|
67
|
+
workflow_id: str = typer.Argument(...),
|
|
68
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""删工作流(连同其会话与沙箱;响应会报删了多少)。"""
|
|
71
|
+
st = current()
|
|
72
|
+
if not yes:
|
|
73
|
+
typer.confirm(f"确认删除工作流 {workflow_id}?其下会话与沙箱一并清理", abort=True)
|
|
74
|
+
payload = st.client().call("workflow.DeleteWorkflow", path_params={"workflow_id": workflow_id})
|
|
75
|
+
if st.json_out:
|
|
76
|
+
output.emit_json(payload)
|
|
77
|
+
return
|
|
78
|
+
typer.echo(
|
|
79
|
+
f"已删除;清理会话 {payload.get('sessionsDeleted', 0)} 个、"
|
|
80
|
+
f"沙箱 {payload.get('sandboxesTerminated', 0)} 个"
|
|
81
|
+
)
|
rathflow_cli/config.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""本地配置:~/.config/rathflow/config.json(多 profile)。
|
|
2
|
+
|
|
3
|
+
选 JSON 而非 TOML:tomllib 只读,写 TOML 需额外依赖(违反 P4)。
|
|
4
|
+
|
|
5
|
+
优先级(由调用方在 cli.py 统一合并):flag > 环境变量 > profile > 内建默认。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# 面向公开发布的内建默认:托管网关。本地开发用 --base-url 或 RATHFLOW_BASE_URL
|
|
16
|
+
# 指回 http://127.0.0.1:8080。
|
|
17
|
+
DEFAULT_BASE_URL = "https://rathflow.lynwe.com"
|
|
18
|
+
|
|
19
|
+
# 环境变量名与 Get Started 页 curl 片段逐字同名,页面片段可直接喂 CLI。
|
|
20
|
+
ENV_BASE_URL = "RATHFLOW_BASE_URL"
|
|
21
|
+
ENV_PROJECT = "RATHFLOW_PROJECT"
|
|
22
|
+
ENV_TOKEN = "RATHFLOW_TOKEN"
|
|
23
|
+
ENV_CONFIG_DIR = "RATHFLOW_CONFIG_DIR"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def config_dir() -> Path:
|
|
27
|
+
return Path(os.environ.get(ENV_CONFIG_DIR) or (Path.home() / ".config" / "rathflow"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def config_path() -> Path:
|
|
31
|
+
return config_dir() / "config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load() -> dict:
|
|
35
|
+
p = config_path()
|
|
36
|
+
if not p.exists():
|
|
37
|
+
return {"current": "default", "profiles": {}}
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
40
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
41
|
+
raise SystemExit(f"配置文件损坏:{p}({exc})")
|
|
42
|
+
data.setdefault("current", "default")
|
|
43
|
+
data.setdefault("profiles", {})
|
|
44
|
+
return data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save(data: dict) -> Path:
|
|
48
|
+
"""落盘并收紧权限到 0600(内含令牌,等同口令)。"""
|
|
49
|
+
p = config_path()
|
|
50
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
tmp = p.with_suffix(".json.tmp")
|
|
52
|
+
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
53
|
+
tmp.replace(p)
|
|
54
|
+
try:
|
|
55
|
+
p.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass # 非 POSIX 文件系统:尽力而为
|
|
58
|
+
return p
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def profile(data: dict, name: str | None = None) -> dict:
|
|
62
|
+
return data["profiles"].setdefault(name or data["current"], {})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def mask(secret: str | None) -> str:
|
|
66
|
+
"""令牌/密钥打码显示:保留前 8 位。"""
|
|
67
|
+
if not secret:
|
|
68
|
+
return "(未设置)"
|
|
69
|
+
return secret[:8] + "…" if len(secret) > 8 else "…"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""REST 端点表 —— 全 CLI 的路径唯一事实源(**禁止在命令实现里写 URL 字面量**)。
|
|
2
|
+
|
|
3
|
+
内容与 web/lib/api/generated/endpoints.ts 同源(proto → grpc-gateway 生成)。
|
|
4
|
+
契约变了改本文件;后续可改为从 proto 生成(设计文档 §3.5)。
|
|
5
|
+
|
|
6
|
+
值 = (HTTP 方法, 路径模板)。模板里的 `{name}` 是 proto 字段名(snake_case)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
from .errors import UsageError
|
|
15
|
+
|
|
16
|
+
# key → (method, path)
|
|
17
|
+
ENDPOINTS: dict[str, tuple[str, str]] = {
|
|
18
|
+
# ---- auth ----
|
|
19
|
+
"auth.IssueAPIKey": ("POST", "/api/v1/api-keys"),
|
|
20
|
+
"auth.ListAPIKeys": ("GET", "/api/v1/api-keys"),
|
|
21
|
+
"auth.Login": ("POST", "/api/v1/auth/login"),
|
|
22
|
+
"auth.Logout": ("POST", "/api/v1/auth/logout"),
|
|
23
|
+
"auth.Refresh": ("POST", "/api/v1/auth/refresh"),
|
|
24
|
+
"auth.Register": ("POST", "/api/v1/auth/register"),
|
|
25
|
+
"auth.RevokeAPIKey": ("DELETE", "/api/v1/api-keys/{key_id}"),
|
|
26
|
+
# ---- tenant:分享链接(能力凭证,明文只在创建/轮换时出现一次)----
|
|
27
|
+
"tenant.JoinSharedProject": ("POST", "/api/v1/shared-projects"),
|
|
28
|
+
"tenant.ListSharedProjects": ("GET", "/api/v1/shared-projects"),
|
|
29
|
+
"tenant.RevokeProjectShare": ("DELETE", "/api/v1/projects/{project_id}/share"),
|
|
30
|
+
"tenant.UpdateProjectShare": ("PATCH", "/api/v1/projects/{project_id}/share"),
|
|
31
|
+
# ---- tenant:org 成员身份 ----
|
|
32
|
+
"tenant.AcceptOrgInvitation": ("POST", "/api/v1/orgs/join"),
|
|
33
|
+
"tenant.CreateOrgInvitation": ("POST", "/api/v1/orgs/{org_id}/invitations"),
|
|
34
|
+
"tenant.ListMyOrgs": ("GET", "/api/v1/orgs"),
|
|
35
|
+
# ---- tenant:project ----
|
|
36
|
+
"tenant.CreateProject": ("POST", "/api/v1/projects"),
|
|
37
|
+
"tenant.DeleteProject": ("DELETE", "/api/v1/projects/{project_id}"),
|
|
38
|
+
"tenant.GetProject": ("GET", "/api/v1/projects/{project_id}"),
|
|
39
|
+
"tenant.GetProjectConfig": ("GET", "/api/v1/projects/{project_id}/config"),
|
|
40
|
+
"tenant.ListProjects": ("GET", "/api/v1/projects"),
|
|
41
|
+
"tenant.UpdateProject": ("PATCH", "/api/v1/projects/{project_id}"),
|
|
42
|
+
"tenant.UpdateProjectConfig": ("PATCH", "/api/v1/projects/{project_id}/config"),
|
|
43
|
+
# ---- session ----
|
|
44
|
+
"session.ArchiveSession": ("POST", "/api/v1/sessions/{session_id}:archive"),
|
|
45
|
+
"session.CreateBlock": ("POST", "/api/v1/sessions/{session_id}/blocks"),
|
|
46
|
+
"session.CreateSession": ("POST", "/api/v1/sessions"),
|
|
47
|
+
"session.DeleteBlock": ("DELETE", "/api/v1/blocks/{block_id}"),
|
|
48
|
+
"session.DeleteSession": ("DELETE", "/api/v1/sessions/{session_id}"),
|
|
49
|
+
"session.GetBlock": ("GET", "/api/v1/blocks/{block_id}"),
|
|
50
|
+
"session.GetSession": ("GET", "/api/v1/sessions/{session_id}"),
|
|
51
|
+
"session.GetSessionEvents": ("GET", "/api/v1/sessions/{session_id}/events"),
|
|
52
|
+
"session.GetSessionLineage": ("GET", "/api/v1/sessions/{session_id}/lineage"),
|
|
53
|
+
"session.ListBlocks": ("GET", "/api/v1/sessions/{session_id}/blocks"),
|
|
54
|
+
"session.ListSessions": ("GET", "/api/v1/sessions"),
|
|
55
|
+
"session.RevokeBlockShare": ("DELETE", "/api/v1/block-shares/{share_id}"),
|
|
56
|
+
"session.ShareBlock": ("POST", "/api/v1/blocks/{block_id}/shares"),
|
|
57
|
+
"session.StreamSessionEvents": ("GET", "/api/v1/sessions/{session_id}/events:stream"),
|
|
58
|
+
# ---- workflow ----
|
|
59
|
+
"workflow.CreateWorkflow": ("POST", "/api/v1/workflows"),
|
|
60
|
+
"workflow.DeleteWorkflow": ("DELETE", "/api/v1/workflows/{workflow_id}"),
|
|
61
|
+
"workflow.GetWorkflow": ("GET", "/api/v1/workflows/{workflow_id}"),
|
|
62
|
+
"workflow.ListWorkflows": ("GET", "/api/v1/workflows"),
|
|
63
|
+
"workflow.UpdateWorkflow": ("PATCH", "/api/v1/workflows/{workflow_id}"),
|
|
64
|
+
# ---- agent ----
|
|
65
|
+
"agent.CreateAgentDef": ("POST", "/api/v1/agents"),
|
|
66
|
+
"agent.DeleteAgentDef": ("DELETE", "/api/v1/agents/{agent_def_id}"),
|
|
67
|
+
"agent.GetAgentDef": ("GET", "/api/v1/agents/{agent_def_id}"),
|
|
68
|
+
"agent.GetAgentDefVersion": ("GET", "/api/v1/agents/{agent_def_id}/versions/{version}"),
|
|
69
|
+
"agent.GetRun": ("GET", "/api/v1/runs/{run_id}"),
|
|
70
|
+
"agent.Interrupt": ("POST", "/api/v1/sessions/{session_id}:interrupt"),
|
|
71
|
+
"agent.ListAgentDefs": ("GET", "/api/v1/agents"),
|
|
72
|
+
"agent.ListAgentDefVersions": ("GET", "/api/v1/agents/{agent_def_id}/versions"),
|
|
73
|
+
"agent.ListAgentDirectory": ("GET", "/api/v1/agent-directory"),
|
|
74
|
+
"agent.ListAgentMessages": ("GET", "/api/v1/agent-directory/{agent_id}/messages"),
|
|
75
|
+
"agent.ListRuns": ("GET", "/api/v1/sessions/{session_id}/runs"),
|
|
76
|
+
"agent.Prompt": ("POST", "/api/v1/sessions/{session_id}:prompt"),
|
|
77
|
+
"agent.ResolveAgentAddress": ("GET", "/api/v1/agent-directory/{agent_id}"),
|
|
78
|
+
"agent.UpdateAgentDef": ("PATCH", "/api/v1/agents/{agent_def_id}"),
|
|
79
|
+
# ---- memory ----
|
|
80
|
+
"memory.CommitSession": ("POST", "/api/v1/memories:commit"),
|
|
81
|
+
"memory.Delete": ("DELETE", "/api/v1/memories/{memory_path}"),
|
|
82
|
+
"memory.GetTask": ("GET", "/api/v1/memory-tasks/{task_id}"),
|
|
83
|
+
"memory.List": ("GET", "/api/v1/memories"),
|
|
84
|
+
"memory.ListTasks": ("GET", "/api/v1/memory-tasks"),
|
|
85
|
+
"memory.Read": ("GET", "/api/v1/memories/{memory_path}"),
|
|
86
|
+
"memory.Search": ("POST", "/api/v1/memories:search"),
|
|
87
|
+
"memory.Tree": ("POST", "/api/v1/memories:tree"),
|
|
88
|
+
"memory.WatchTask": ("GET", "/api/v1/memory-tasks/{task_id}:watch"),
|
|
89
|
+
"memory.Write": ("PUT", "/api/v1/memories/{memory_path}"),
|
|
90
|
+
# ---- sandbox ----
|
|
91
|
+
"sandbox.Create": ("POST", "/api/v1/sandboxes"),
|
|
92
|
+
"sandbox.ExposePort": ("POST", "/api/v1/sandboxes/{sandbox_id}/ports"),
|
|
93
|
+
"sandbox.Get": ("GET", "/api/v1/sandboxes/{sandbox_id}"),
|
|
94
|
+
"sandbox.List": ("GET", "/api/v1/sandboxes"),
|
|
95
|
+
"sandbox.ListDir": ("GET", "/api/v1/sandboxes/{sandbox_id}/directory"),
|
|
96
|
+
"sandbox.ReadFile": ("GET", "/api/v1/sandboxes/{sandbox_id}/files/{path}"),
|
|
97
|
+
"sandbox.Renew": ("POST", "/api/v1/sandboxes/{sandbox_id}:renew"),
|
|
98
|
+
"sandbox.RunCode": ("POST", "/api/v1/sandboxes/{sandbox_id}/code"),
|
|
99
|
+
"sandbox.RunCommand": ("POST", "/api/v1/sandboxes/{sandbox_id}/exec"),
|
|
100
|
+
"sandbox.Stat": ("GET", "/api/v1/sandboxes/{sandbox_id}/stat"),
|
|
101
|
+
"sandbox.StreamLogs": ("GET", "/api/v1/sandboxes/{sandbox_id}/logs"),
|
|
102
|
+
"sandbox.Terminate": ("DELETE", "/api/v1/sandboxes/{sandbox_id}"),
|
|
103
|
+
# ---- assets ----
|
|
104
|
+
"assets.CreateAsset": ("POST", "/admin/api/v1/assets"),
|
|
105
|
+
"assets.CreateAssetVersion": ("POST", "/admin/api/v1/assets/{asset_id}/versions"),
|
|
106
|
+
"assets.GetAssemblyArtifact": (
|
|
107
|
+
"GET",
|
|
108
|
+
"/admin/api/v1/assembly-artifacts/{project_id}/revisions/{revision}",
|
|
109
|
+
),
|
|
110
|
+
"assets.GetAssemblyStatus": ("GET", "/api/v1/assembly-status"),
|
|
111
|
+
"assets.GetAsset": ("GET", "/api/v1/assets/{asset_id}"),
|
|
112
|
+
"assets.ListAssemblyRevisions": ("GET", "/admin/api/v1/assembly-revisions"),
|
|
113
|
+
"assets.ListAssets": ("GET", "/api/v1/assets"),
|
|
114
|
+
"assets.ListEnabledAssets": ("GET", "/api/v1/assets:enabled"),
|
|
115
|
+
"assets.PinAssetVersion": ("POST", "/api/v1/assets/{asset_id}:pin-version"),
|
|
116
|
+
"assets.PublishVersion": (
|
|
117
|
+
"POST",
|
|
118
|
+
"/admin/api/v1/assets/{asset_id}/versions/{version_id}:publish",
|
|
119
|
+
),
|
|
120
|
+
"assets.PutAssetContent": (
|
|
121
|
+
"PUT",
|
|
122
|
+
"/admin/api/v1/assets/{asset_id}/versions/{version_id}/content",
|
|
123
|
+
),
|
|
124
|
+
"assets.SetAssetConfig": ("POST", "/api/v1/assets/{asset_id}:set-config"),
|
|
125
|
+
"assets.SetAssetEnabled": ("POST", "/api/v1/assets/{asset_id}:set-enabled"),
|
|
126
|
+
"assets.SetAssetLifecycle": ("POST", "/admin/api/v1/assets/{asset_id}:set-lifecycle"),
|
|
127
|
+
"assets.SetCredential": ("POST", "/api/v1/assets/{asset_id}:set-credential"),
|
|
128
|
+
"assets.SetVisibility": ("POST", "/admin/api/v1/assets/{asset_id}:set-visibility"),
|
|
129
|
+
"assets.SyncCoreAssets": ("POST", "/admin/api/v1/assets:sync-core"),
|
|
130
|
+
"assets.TriggerAssemblyDispatch": ("POST", "/admin/api/v1/assembly-dispatch:trigger"),
|
|
131
|
+
"assets.UpdateAsset": ("PATCH", "/admin/api/v1/assets/{asset_id}"),
|
|
132
|
+
"assets.YankVersion": ("POST", "/admin/api/v1/assets/{asset_id}/versions/{version_id}:yank"),
|
|
133
|
+
# ---- billing ----
|
|
134
|
+
"billing.GetSubscription": ("GET", "/api/v1/billing/subscription"),
|
|
135
|
+
"billing.GetUsage": ("GET", "/api/v1/billing/usage"),
|
|
136
|
+
"billing.ListInvoices": ("GET", "/api/v1/billing/invoices"),
|
|
137
|
+
# ---- admin ----
|
|
138
|
+
"admin.ListUsers": ("GET", "/admin/api/v1/users"),
|
|
139
|
+
"admin.PromoteUser": ("POST", "/admin/api/v1/users/{user_id}:promote"),
|
|
140
|
+
"admin.RotateContextTokenKey": ("POST", "/admin/api/v1/context-token-keys:rotate"),
|
|
141
|
+
"admin.SetUserStatus": ("PUT", "/admin/api/v1/users/{user_id}/status"),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# 多段通配参数(模板里写作 {name=**}):值含 `/`,须**逐段 encode 后以 `/` 连接**,
|
|
145
|
+
# 不可整体 encodeURIComponent(否则 `notes/a.md` 会被压成单个 segment)。
|
|
146
|
+
MULTI_PARAMS: dict[str, frozenset[str]] = {
|
|
147
|
+
"memory.Delete": frozenset({"memory_path"}),
|
|
148
|
+
"memory.Read": frozenset({"memory_path"}),
|
|
149
|
+
"memory.Write": frozenset({"memory_path"}),
|
|
150
|
+
"sandbox.ReadFile": frozenset({"path"}),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# 响应为流的端点(唯一事实源:contracts/gen/go-gateway 里 forward_*_0 =
|
|
154
|
+
# runtime.ForwardResponseStream 的那几条)。流式端点必须走 Client.stream():
|
|
155
|
+
# 走 call() 会拿到逐行 JSON 的未解析正文。
|
|
156
|
+
STREAMING: frozenset[str] = frozenset(
|
|
157
|
+
{
|
|
158
|
+
"sandbox.RunCommand",
|
|
159
|
+
"sandbox.RunCode",
|
|
160
|
+
"sandbox.ReadFile",
|
|
161
|
+
"sandbox.StreamLogs",
|
|
162
|
+
"session.StreamSessionEvents",
|
|
163
|
+
"memory.Search",
|
|
164
|
+
"memory.WatchTask",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def is_streaming(key: str) -> bool:
|
|
170
|
+
return key in STREAMING
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_TEMPLATE_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)(=\*\*)?\}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def path_params(key: str) -> list[str]:
|
|
177
|
+
"""路径模板需要的参数名。"""
|
|
178
|
+
return [m.group(1) for m in _TEMPLATE_RE.finditer(_spec(key)[1])]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _spec(key: str) -> tuple[str, str]:
|
|
182
|
+
try:
|
|
183
|
+
return ENDPOINTS[key]
|
|
184
|
+
except KeyError:
|
|
185
|
+
raise UsageError(f"未知端点 {key!r}(用 `rathflow api --list` 查看全部)") from None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def render_path(key: str, params: dict) -> str:
|
|
189
|
+
"""把模板渲染成真实路径,逐参数 encode。缺参数 → UsageError。"""
|
|
190
|
+
_method, template = _spec(key)
|
|
191
|
+
multi = MULTI_PARAMS.get(key, frozenset())
|
|
192
|
+
|
|
193
|
+
def repl(m: re.Match) -> str:
|
|
194
|
+
name = m.group(1)
|
|
195
|
+
value = params.get(name)
|
|
196
|
+
if value is None or value == "":
|
|
197
|
+
raise UsageError(f"{key} 需要路径参数 --path {name}=<值>")
|
|
198
|
+
text = str(value)
|
|
199
|
+
if name in multi:
|
|
200
|
+
return "/".join(quote(seg, safe="") for seg in text.split("/"))
|
|
201
|
+
return quote(text, safe="")
|
|
202
|
+
|
|
203
|
+
return _TEMPLATE_RE.sub(repl, template)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def method_of(key: str) -> str:
|
|
207
|
+
return _spec(key)[0]
|
rathflow_cli/errors.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""错误信封与退出码(contracts/docs/error-envelope.md)。
|
|
2
|
+
|
|
3
|
+
信封形状:{"error": {"code","message","details"}}。终端只显示 message,code 供程序判定。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
# 退出码约定(docs:CLI 设计 §3.8)
|
|
9
|
+
EXIT_OK = 0
|
|
10
|
+
EXIT_ERROR = 1 # 未分类
|
|
11
|
+
EXIT_USAGE = 2 # 参数/缺 project/未登录
|
|
12
|
+
EXIT_AUTH = 3 # 401(刷新也无效)
|
|
13
|
+
EXIT_FORBIDDEN = 4 # 403
|
|
14
|
+
EXIT_NOT_FOUND = 5 # 404
|
|
15
|
+
EXIT_INVALID = 6 # 400 INVALID_ARGUMENT / FAILED_PRECONDITION / OUT_OF_RANGE
|
|
16
|
+
EXIT_CONFLICT = 7 # 409
|
|
17
|
+
EXIT_RATE_LIMIT = 8 # 429
|
|
18
|
+
EXIT_SERVER = 9 # 5xx
|
|
19
|
+
|
|
20
|
+
_BY_STATUS = {
|
|
21
|
+
400: EXIT_INVALID,
|
|
22
|
+
401: EXIT_AUTH,
|
|
23
|
+
403: EXIT_FORBIDDEN,
|
|
24
|
+
404: EXIT_NOT_FOUND,
|
|
25
|
+
409: EXIT_CONFLICT,
|
|
26
|
+
429: EXIT_RATE_LIMIT,
|
|
27
|
+
499: EXIT_ERROR,
|
|
28
|
+
500: EXIT_SERVER,
|
|
29
|
+
501: EXIT_SERVER,
|
|
30
|
+
503: EXIT_SERVER,
|
|
31
|
+
504: EXIT_SERVER,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ApiError(Exception):
|
|
36
|
+
"""一次失败的 API 调用(信封已解出,或传输层失败)。"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, status: int, code: str, message: str, details=None):
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.status = status
|
|
41
|
+
self.code = code
|
|
42
|
+
self.message = message
|
|
43
|
+
self.details = list(details or [])
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def exit_code(self) -> int:
|
|
47
|
+
return _BY_STATUS.get(self.status, EXIT_ERROR)
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
if self.code and self.code not in ("UNKNOWN", ""):
|
|
51
|
+
return f"{self.message} [{self.code}]"
|
|
52
|
+
return self.message
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class UsageError(Exception):
|
|
56
|
+
"""本地用法错误(未登录、缺 project、参数不合法)。退出码 2。"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def from_response(status: int, payload) -> ApiError:
|
|
60
|
+
"""从响应体解出信封;形状不符时退回通用文案(不外泄内部细节)。"""
|
|
61
|
+
body = payload if isinstance(payload, dict) else {}
|
|
62
|
+
err = body.get("error")
|
|
63
|
+
if not isinstance(err, dict):
|
|
64
|
+
return ApiError(status, "UNKNOWN", f"请求失败(HTTP {status})")
|
|
65
|
+
return ApiError(
|
|
66
|
+
status,
|
|
67
|
+
str(err.get("code") or "UNKNOWN"),
|
|
68
|
+
str(err.get("message") or f"请求失败(HTTP {status})"),
|
|
69
|
+
err.get("details"),
|
|
70
|
+
)
|