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.
@@ -0,0 +1,6 @@
1
+ """RathFlow CLI —— RathFlow 北向 REST 的命令行客户端。
2
+
3
+ 安装:``uv tool install rathflow-cli``(或 ``pip install rathflow-cli``)。
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """支持 `python -m rathflow_cli`(与 `rathflow` 入口点等价)。"""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
rathflow_cli/cli.py ADDED
@@ -0,0 +1,161 @@
1
+ """typer 根应用与全局选项。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ import typer
8
+
9
+ from . import __version__, errors, state as state_mod
10
+ from .state import State
11
+
12
+ app = typer.Typer(
13
+ add_completion=False,
14
+ no_args_is_help=True,
15
+ help="RathFlow 命令行客户端(JWT 登录;命令覆盖北向 REST 全部端点)。",
16
+ )
17
+
18
+
19
+ def _print_version(value: bool) -> None:
20
+ if value:
21
+ typer.echo(f"rathflow {__version__}")
22
+ raise typer.Exit()
23
+
24
+
25
+ @app.callback()
26
+ def root(
27
+ ctx: typer.Context,
28
+ profile: str = typer.Option(None, "--profile", "-p", help="配置档名(缺省 default)"),
29
+ base_url: str = typer.Option(None, "--base-url", help="gateway 地址(env RATHFLOW_BASE_URL)"),
30
+ project: str = typer.Option(None, "--project", help="项目作用域(env RATHFLOW_PROJECT)"),
31
+ json_out: bool = typer.Option(False, "--json", help="以 JSON 输出(脚本消费的稳定形态)"),
32
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="只输出主键"),
33
+ version: bool = typer.Option(
34
+ False, "--version", callback=_print_version, is_eager=True, help="显示版本并退出"
35
+ ),
36
+ ) -> None:
37
+ ctx.obj = state_mod.install(
38
+ State(
39
+ profile_name=profile,
40
+ base_url=base_url,
41
+ project=project,
42
+ json_out=json_out,
43
+ quiet=quiet,
44
+ )
45
+ )
46
+
47
+
48
+ def _register() -> None:
49
+ from .commands import (
50
+ admin,
51
+ agent,
52
+ api_cmd,
53
+ asset,
54
+ auth_cmd,
55
+ billing,
56
+ config_cmd,
57
+ memory,
58
+ org,
59
+ project,
60
+ sandbox,
61
+ session,
62
+ workflow,
63
+ )
64
+
65
+ app.add_typer(auth_cmd.app, name="auth")
66
+ app.add_typer(config_cmd.app, name="config")
67
+ app.add_typer(org.app, name="org")
68
+ app.add_typer(project.app, name="project")
69
+ app.add_typer(session.app, name="session")
70
+ app.add_typer(memory.app, name="memory")
71
+ app.add_typer(sandbox.app, name="sandbox")
72
+ app.add_typer(agent.app, name="agent")
73
+ app.add_typer(asset.app, name="asset")
74
+ app.add_typer(billing.app, name="billing")
75
+ app.add_typer(workflow.app, name="workflow")
76
+ app.add_typer(admin.app, name="admin")
77
+
78
+ from .commands.auth_cmd import whoami
79
+
80
+ app.command("whoami", help="显示登录身份、作用域与可访问项目")(whoami)
81
+ # 逃生舱直接挂根:`rathflow api <Key>` 不必多打一层 `call`
82
+ app.command("api", help="任意端点调用(`rathflow api --list` 看全部)")(api_cmd.call)
83
+
84
+
85
+ _register()
86
+
87
+
88
+ # 可以挪到命令行最前的全局选项:前者是布尔旗标,后者各吃一个值。
89
+ _HOIST_FLAGS = frozenset({"--json", "--quiet"})
90
+ _HOIST_OPTS = frozenset({"--profile", "--base-url", "--project"})
91
+
92
+
93
+ def _value_opts() -> set[str]:
94
+ """全 app 里「吃下一个词」的选项名 —— 用来判断挪旗标安不安全。
95
+
96
+ 走 click 命令树而非 Typer 对象:Typer 只是注册表,click Group 是懒构建的。
97
+ """
98
+
99
+ def walk(cmd) -> set[str]:
100
+ found: set[str] = set()
101
+ for param in getattr(cmd, "params", []):
102
+ if getattr(param, "is_flag", False) or getattr(param, "count", False):
103
+ continue
104
+ found.update(getattr(param, "opts", []))
105
+ for sub in getattr(cmd, "commands", {}).values():
106
+ found |= walk(sub)
107
+ return found
108
+
109
+ return walk(typer.main.get_command(app))
110
+
111
+
112
+ def _hoist_globals(argv: list[str]) -> list[str]:
113
+ """把写在命令**后面**的全局选项挪到最前。
114
+
115
+ click 只认组级选项出现在子命令之前,故 `rathflow project list --json` 会被
116
+ 当成给 `list` 传参而报错。脚本里这个顺序太自然了,于是这里做一次改写:
117
+ 只挪白名单里的选项,且前一个词不是吃值的选项(否则那个词是被当值用的,
118
+ 例如 `--body --json` 里的 `--json` 是值,不能动)。
119
+ """
120
+ if len(argv) < 2:
121
+ return argv
122
+ value_opts = _value_opts()
123
+ head, tail = [argv[0]], []
124
+ hoisted: list[str] = []
125
+ idx = 1
126
+ while idx < len(argv):
127
+ tok = argv[idx]
128
+ prev = argv[idx - 1]
129
+ # 前一个词若是吃值的选项,本词就是它的值(click 也这么看),别动
130
+ safe = prev not in value_opts
131
+ if safe and tok in _HOIST_FLAGS:
132
+ hoisted.append(tok)
133
+ idx += 1
134
+ continue
135
+ if safe and tok in _HOIST_OPTS and idx + 1 < len(argv) and not argv[idx + 1].startswith("-"):
136
+ hoisted.extend([tok, argv[idx + 1]])
137
+ idx += 2
138
+ continue
139
+ tail.append(tok)
140
+ idx += 1
141
+ return head + hoisted + tail
142
+
143
+
144
+ def main() -> None:
145
+ sys.argv = _hoist_globals(sys.argv)
146
+ try:
147
+ app()
148
+ except errors.UsageError as exc:
149
+ print(f"错误:{exc}", file=sys.stderr)
150
+ sys.exit(errors.EXIT_USAGE)
151
+ except errors.ApiError as exc:
152
+ print(f"错误:{exc}", file=sys.stderr)
153
+ for detail in exc.details:
154
+ print(f" - {detail}", file=sys.stderr)
155
+ sys.exit(exc.exit_code)
156
+ except KeyboardInterrupt: # pragma: no cover
157
+ sys.exit(130)
158
+
159
+
160
+ if __name__ == "__main__": # pragma: no cover
161
+ main()
File without changes
@@ -0,0 +1,78 @@
1
+ """命令间共用的小工具(参数解析与请求体装配)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ import typer
9
+
10
+ from ..errors import UsageError
11
+
12
+ PATH_OPT = typer.Option(None, "--path", help="路径参数 k=v,可重复(如 --path session_id=abc)")
13
+ QUERY_OPT = typer.Option(None, "--query", help="查询参数 k=v,可重复")
14
+ BODY_OPT = typer.Option(None, "--body", help="请求体 JSON 字面量")
15
+ BODY_FILE_OPT = typer.Option(None, "--body-file", help="从文件读请求体 JSON(- 读 stdin)")
16
+ PAGE_SIZE_OPT = typer.Option(None, "--page-size", help="分页大小")
17
+ PAGE_TOKEN_OPT = typer.Option(None, "--page-token", help="分页游标")
18
+
19
+
20
+ def pairs(items: list[str] | None) -> dict:
21
+ """['k=v', ...] → {k: v};值里的 = 保留(只切第一个)。"""
22
+ out: dict[str, str] = {}
23
+ for item in items or []:
24
+ if "=" not in item:
25
+ raise UsageError(f"参数需形如 k=v,收到 {item!r}")
26
+ key, _, value = item.partition("=")
27
+ out[key.strip()] = value
28
+ return out
29
+
30
+
31
+ def body_of(body: str | None, body_file: str | None) -> dict | None:
32
+ """装配请求体:字面量或文件(`-` = stdin)。两者互斥。"""
33
+ if body and body_file:
34
+ raise UsageError("--body 与 --body-file 不能同时用")
35
+ raw: str | None = None
36
+ if body_file:
37
+ raw = sys.stdin.read() if body_file == "-" else _read(body_file)
38
+ elif body:
39
+ raw = body
40
+ else:
41
+ return None
42
+ raw = raw.strip()
43
+ if not raw:
44
+ return {}
45
+ try:
46
+ parsed = json.loads(raw)
47
+ except json.JSONDecodeError as exc:
48
+ raise UsageError(f"请求体不是合法 JSON:{exc}") from None
49
+ if not isinstance(parsed, dict):
50
+ raise UsageError("请求体必须是 JSON 对象")
51
+ return parsed
52
+
53
+
54
+ def _read(path: str) -> str:
55
+ try:
56
+ with open(path, encoding="utf-8") as fh:
57
+ return fh.read()
58
+ except OSError as exc:
59
+ raise UsageError(f"读取请求体文件失败:{exc}") from None
60
+
61
+
62
+ def pagination(page_size: int | None, page_token: str | None, extra: dict | None = None) -> dict:
63
+ """装配分页查询串(protojson 查询名用 proto 字段名 snake_case)。"""
64
+ query = dict(extra or {})
65
+ if page_size is not None:
66
+ query["page_size"] = page_size
67
+ if page_token:
68
+ query["page_token"] = page_token
69
+ return query
70
+
71
+
72
+ def id_of(payload: dict, *keys: str) -> str:
73
+ """从响应里挑主键用于 -q 输出。"""
74
+ for key in keys:
75
+ value = (payload or {}).get(key)
76
+ if value:
77
+ return str(value)
78
+ return ""
@@ -0,0 +1,83 @@
1
+ """admin:平台管理(用户、上下文令牌签名密钥)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .. import output
8
+ from ..commands._common import PAGE_SIZE_OPT, PAGE_TOKEN_OPT, pagination
9
+ from ..state import current
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="平台管理(需平台管理员)")
12
+
13
+
14
+ @app.command("users")
15
+ def users(
16
+ page_size: int = PAGE_SIZE_OPT,
17
+ page_token: str = PAGE_TOKEN_OPT,
18
+ ) -> None:
19
+ """列平台用户。"""
20
+ st = current()
21
+ payload = st.client().call("admin.ListUsers", query=pagination(page_size or 50, page_token))
22
+ output.emit_items(
23
+ payload,
24
+ items_key="users",
25
+ columns=[
26
+ ("USER ID", "userId"),
27
+ ("邮箱", "email"),
28
+ ("名称", "displayName"),
29
+ ("状态", "status"),
30
+ ("管理员", "isPlatformAdmin"),
31
+ ("注册时间", "createdAt"),
32
+ ],
33
+ json_out=st.json_out,
34
+ )
35
+
36
+
37
+ @app.command("promote")
38
+ def promote(
39
+ user_id: str = typer.Argument(...),
40
+ admin: bool = typer.Option(True, "--admin/--demote", help="授予或收回平台管理员"),
41
+ ) -> None:
42
+ """授予/收回平台管理员。"""
43
+ st = current()
44
+ body = {"isPlatformAdmin": admin}
45
+ payload = st.client().call(
46
+ "admin.PromoteUser", path_params={"user_id": user_id}, body=body
47
+ )
48
+ if st.json_out:
49
+ output.emit_json(payload)
50
+ return
51
+ typer.echo(f"{user_id} → {'平台管理员' if admin else '普通用户'}")
52
+
53
+
54
+ @app.command("set-status")
55
+ def set_status(
56
+ user_id: str = typer.Argument(...),
57
+ status: str = typer.Option(..., "--status", help="active | disabled"),
58
+ ) -> None:
59
+ """启用/停用账号。"""
60
+ st = current()
61
+ body = {"status": f"USER_STATUS_{status.upper()}"}
62
+ payload = st.client().call(
63
+ "admin.SetUserStatus", path_params={"user_id": user_id}, body=body
64
+ )
65
+ if st.json_out:
66
+ output.emit_json(payload)
67
+ return
68
+ typer.echo(f"{user_id} → {status}")
69
+
70
+
71
+ @app.command("rotate-context-key")
72
+ def rotate_context_key(
73
+ reason: str = typer.Option(None, "--reason", help="轮换原因(记入审计)"),
74
+ ) -> None:
75
+ """轮换上下文令牌签名密钥(reason 走 query,该接口无 body)。"""
76
+ st = current()
77
+ payload = st.client().call(
78
+ "admin.RotateContextTokenKey", query={"reason": reason} if reason else None
79
+ )
80
+ if st.json_out:
81
+ output.emit_json(payload)
82
+ return
83
+ typer.echo(f"新密钥 {payload.get('keyId')} 生效于 {payload.get('effectiveAt')}")
@@ -0,0 +1,288 @@
1
+ """agent:Agent 定义、版本、运行与 Agent 目录。"""
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="Agent 定义 / 版本 / 运行 / 目录")
20
+
21
+ DEF_COLUMNS = [
22
+ ("AGENT DEF ID", "agentDefId"),
23
+ ("名称", "name"),
24
+ ("版本", "version"),
25
+ ("状态", "status"),
26
+ ("更新时间", "updatedAt"),
27
+ ]
28
+ RUN_COLUMNS = [
29
+ ("RUN ID", "runId"),
30
+ ("状态", "status"),
31
+ ("提示词", "promptText"),
32
+ ("创建时间", "createdAt"),
33
+ ]
34
+
35
+
36
+ # ------------------------------------------------------------------ 定义
37
+
38
+ @app.command("defs")
39
+ def defs(
40
+ page_size: int = PAGE_SIZE_OPT,
41
+ page_token: str = PAGE_TOKEN_OPT,
42
+ ) -> None:
43
+ """列 Agent 定义。"""
44
+ st = current()
45
+ payload = st.client().call("agent.ListAgentDefs", query=pagination(page_size or 50, page_token))
46
+ output.emit_items(payload, items_key="agentDefs", columns=DEF_COLUMNS, json_out=st.json_out)
47
+
48
+
49
+ @app.command("def")
50
+ def def_get(agent_def_id: str = typer.Argument(...)) -> None:
51
+ """看定义详情。"""
52
+ st = current()
53
+ payload = st.client().call("agent.GetAgentDef", path_params={"agent_def_id": agent_def_id})
54
+ output.emit_object(payload, json_out=st.json_out)
55
+
56
+
57
+ @app.command("def-create")
58
+ def def_create(
59
+ name: str = typer.Option(..., "--name", "-n"),
60
+ description: str = typer.Option(None, "--description", "-d"),
61
+ config: str = typer.Option(None, "--config", help="config_json 字面量(自由 JSON 文本)"),
62
+ config_file: str = typer.Option(None, "--config-file", help="从文件读 config_json(- 读 stdin)"),
63
+ memory_binding: str = typer.Option(None, "--memory-binding", help="记忆绑定,如 memories/agents"),
64
+ body: str = BODY_OPT,
65
+ body_file: str = BODY_FILE_OPT,
66
+ ) -> None:
67
+ """建 Agent 定义。"""
68
+ st = current()
69
+ payload_body = body_of(body, body_file) or {}
70
+ payload_body.setdefault("name", name)
71
+ if description:
72
+ payload_body.setdefault("description", description)
73
+ if memory_binding:
74
+ payload_body.setdefault("memoryBinding", memory_binding)
75
+ text = _text_of(config, config_file)
76
+ if text is not None:
77
+ payload_body.setdefault("configJson", text)
78
+ payload = st.client().call("agent.CreateAgentDef", body=payload_body)
79
+ if st.json_out:
80
+ output.emit_json(payload)
81
+ return
82
+ typer.echo(payload.get("agentDefId") or "")
83
+
84
+
85
+ @app.command("def-update")
86
+ def def_update(
87
+ agent_def_id: str = typer.Argument(...),
88
+ name: str = typer.Option(None, "--name", "-n"),
89
+ description: str = typer.Option(None, "--description", "-d"),
90
+ config: str = typer.Option(None, "--config"),
91
+ config_file: str = typer.Option(None, "--config-file"),
92
+ memory_binding: str = typer.Option(None, "--memory-binding"),
93
+ status: str = typer.Option(None, "--status", help="AgentDefStatus 枚举"),
94
+ body: str = BODY_OPT,
95
+ body_file: str = BODY_FILE_OPT,
96
+ ) -> None:
97
+ """改定义(PATCH 语义;改完通常产生新版本)。"""
98
+ st = current()
99
+ payload_body = body_of(body, body_file) or {}
100
+ for key, value in (
101
+ ("name", name),
102
+ ("description", description),
103
+ ("memoryBinding", memory_binding),
104
+ ("status", status),
105
+ ):
106
+ if value:
107
+ payload_body.setdefault(key, value)
108
+ text = _text_of(config, config_file)
109
+ if text is not None:
110
+ payload_body.setdefault("configJson", text)
111
+ if not payload_body:
112
+ raise UsageError("至少给一项要改的字段(--name/--description/--config/--memory-binding/--status)")
113
+ payload = st.client().call(
114
+ "agent.UpdateAgentDef", path_params={"agent_def_id": agent_def_id}, body=payload_body
115
+ )
116
+ output.emit_object(payload, json_out=st.json_out)
117
+
118
+
119
+ @app.command("def-delete")
120
+ def def_delete(
121
+ agent_def_id: str = typer.Argument(...),
122
+ yes: bool = typer.Option(False, "--yes", "-y"),
123
+ ) -> None:
124
+ """删定义。"""
125
+ st = current()
126
+ if not yes:
127
+ typer.confirm(f"确认删除 Agent 定义 {agent_def_id}?", abort=True)
128
+ st.client().call("agent.DeleteAgentDef", path_params={"agent_def_id": agent_def_id})
129
+ typer.echo(f"已删除 {agent_def_id}")
130
+
131
+
132
+ @app.command("versions")
133
+ def versions(
134
+ agent_def_id: str = typer.Argument(...),
135
+ page_size: int = PAGE_SIZE_OPT,
136
+ page_token: str = PAGE_TOKEN_OPT,
137
+ ) -> None:
138
+ """列定义的版本。"""
139
+ st = current()
140
+ payload = st.client().call(
141
+ "agent.ListAgentDefVersions",
142
+ path_params={"agent_def_id": agent_def_id},
143
+ query=pagination(page_size or 50, page_token),
144
+ )
145
+ output.emit_items(
146
+ payload,
147
+ items_key="versions",
148
+ columns=[("版本", "version"), ("创建人", "createdBy"), ("创建时间", "createdAt")],
149
+ json_out=st.json_out,
150
+ )
151
+
152
+
153
+ @app.command("version")
154
+ def version(
155
+ agent_def_id: str = typer.Argument(...),
156
+ version: int = typer.Argument(...),
157
+ ) -> None:
158
+ """看某个版本。"""
159
+ st = current()
160
+ payload = st.client().call(
161
+ "agent.GetAgentDefVersion",
162
+ path_params={"agent_def_id": agent_def_id, "version": version},
163
+ )
164
+ output.emit_object(payload, json_out=st.json_out)
165
+
166
+
167
+ # ------------------------------------------------------------------ 运行
168
+
169
+ @app.command("prompt")
170
+ def prompt(
171
+ session_id: str = typer.Argument(...),
172
+ text: str = typer.Argument(None, help="提示词;缺省或 - 读 stdin"),
173
+ body: str = BODY_OPT,
174
+ body_file: str = BODY_FILE_OPT,
175
+ ) -> None:
176
+ """给会话发提示词,起一个 run(返回 runId;进度用 `session watch` 跟)。"""
177
+ st = current()
178
+ payload_body = body_of(body, body_file)
179
+ if payload_body is None:
180
+ if text in (None, "-"):
181
+ import sys
182
+
183
+ text = sys.stdin.read()
184
+ payload_body = {"text": text}
185
+ payload = st.client().call(
186
+ "agent.Prompt", path_params={"session_id": session_id}, body=payload_body
187
+ )
188
+ if st.json_out:
189
+ output.emit_json(payload)
190
+ return
191
+ typer.echo(payload.get("runId") or "")
192
+
193
+
194
+ @app.command("runs")
195
+ def runs(
196
+ session_id: str = typer.Argument(...),
197
+ page_size: int = PAGE_SIZE_OPT,
198
+ page_token: str = PAGE_TOKEN_OPT,
199
+ ) -> None:
200
+ """列会话下的 run。"""
201
+ st = current()
202
+ payload = st.client().call(
203
+ "agent.ListRuns",
204
+ path_params={"session_id": session_id},
205
+ query=pagination(page_size or 50, page_token),
206
+ )
207
+ output.emit_items(payload, items_key="runs", columns=RUN_COLUMNS, json_out=st.json_out)
208
+
209
+
210
+ @app.command("run")
211
+ def run(run_id: str = typer.Argument(...)) -> None:
212
+ """看单个 run。"""
213
+ st = current()
214
+ payload = st.client().call("agent.GetRun", path_params={"run_id": run_id})
215
+ output.emit_object(payload, json_out=st.json_out)
216
+
217
+
218
+ # ------------------------------------------------------------------ 目录
219
+
220
+ @app.command("directory")
221
+ def directory(
222
+ page_size: int = PAGE_SIZE_OPT,
223
+ page_token: str = PAGE_TOKEN_OPT,
224
+ ) -> None:
225
+ """列 Agent 目录(跨 project 可见的 agent 地址)。"""
226
+ st = current()
227
+ payload = st.client().call(
228
+ "agent.ListAgentDirectory", query=pagination(page_size or 50, page_token)
229
+ )
230
+ output.emit_items(
231
+ payload,
232
+ items_key="entries",
233
+ columns=[("AGENT ID", "agentId"), ("地址", "address"), ("状态", "status")],
234
+ json_out=st.json_out,
235
+ )
236
+
237
+
238
+ @app.command("resolve")
239
+ def resolve(agent_id: str = typer.Argument(...)) -> None:
240
+ """把 agent id 解析成地址。"""
241
+ st = current()
242
+ payload = st.client().call(
243
+ "agent.ResolveAgentAddress", path_params={"agent_id": agent_id}
244
+ )
245
+ output.emit_object(payload, json_out=st.json_out)
246
+
247
+
248
+ @app.command("messages")
249
+ def messages(
250
+ agent_id: str = typer.Argument(...),
251
+ page_size: int = PAGE_SIZE_OPT,
252
+ page_token: str = PAGE_TOKEN_OPT,
253
+ ) -> None:
254
+ """看某个 agent 的消息记录。"""
255
+ st = current()
256
+ payload = st.client().call(
257
+ "agent.ListAgentMessages",
258
+ path_params={"agent_id": agent_id},
259
+ query=pagination(page_size or 50, page_token),
260
+ )
261
+ output.emit_items(
262
+ payload,
263
+ items_key="messages", # 见 ListAgentMessagesResponse
264
+ columns=[
265
+ ("MESSAGE ID", "messageId"),
266
+ ("方向", "direction"),
267
+ ("对端", "peerAgentId"),
268
+ ("负载", "payloadJson"),
269
+ ],
270
+ json_out=st.json_out,
271
+ )
272
+
273
+
274
+ def _text_of(text: str | None, file: str | None) -> str | None:
275
+ """config_json 是自由 JSON 文本:字面量或文件(- 读 stdin)。"""
276
+ if text and file:
277
+ raise UsageError("--config 与 --config-file 不能同时用")
278
+ if file:
279
+ if file == "-":
280
+ import sys
281
+
282
+ return sys.stdin.read()
283
+ try:
284
+ with open(file, encoding="utf-8") as fh:
285
+ return fh.read()
286
+ except OSError as exc:
287
+ raise UsageError(f"读取失败:{exc}") from None
288
+ return text