@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.59

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.
Files changed (34) hide show
  1. package/README.md +3 -2
  2. package/docs/local-agent-install.md +9 -0
  3. package/npm/bin/qingflow.mjs +1 -1
  4. package/npm/lib/runtime.mjs +156 -21
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/src/qingflow_mcp/builder_facade/service.py +137 -5
  8. package/src/qingflow_mcp/cli/commands/app.py +16 -16
  9. package/src/qingflow_mcp/cli/commands/auth.py +19 -16
  10. package/src/qingflow_mcp/cli/commands/builder.py +124 -162
  11. package/src/qingflow_mcp/cli/commands/common.py +21 -95
  12. package/src/qingflow_mcp/cli/commands/imports.py +42 -34
  13. package/src/qingflow_mcp/cli/commands/record.py +131 -133
  14. package/src/qingflow_mcp/cli/commands/task.py +43 -44
  15. package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
  16. package/src/qingflow_mcp/cli/context.py +35 -32
  17. package/src/qingflow_mcp/cli/formatters.py +124 -121
  18. package/src/qingflow_mcp/cli/main.py +52 -17
  19. package/src/qingflow_mcp/server_app_builder.py +122 -190
  20. package/src/qingflow_mcp/server_app_user.py +63 -662
  21. package/src/qingflow_mcp/tools/solution_tools.py +95 -3
  22. package/src/qingflow_mcp/ops/__init__.py +0 -3
  23. package/src/qingflow_mcp/ops/apps.py +0 -64
  24. package/src/qingflow_mcp/ops/auth.py +0 -121
  25. package/src/qingflow_mcp/ops/base.py +0 -290
  26. package/src/qingflow_mcp/ops/builder.py +0 -357
  27. package/src/qingflow_mcp/ops/context.py +0 -120
  28. package/src/qingflow_mcp/ops/directory.py +0 -171
  29. package/src/qingflow_mcp/ops/feedback.py +0 -49
  30. package/src/qingflow_mcp/ops/files.py +0 -78
  31. package/src/qingflow_mcp/ops/imports.py +0 -140
  32. package/src/qingflow_mcp/ops/records.py +0 -415
  33. package/src/qingflow_mcp/ops/tasks.py +0 -171
  34. package/src/qingflow_mcp/ops/workspace.py +0 -76
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.58
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.59
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.58 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.59 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -28,3 +28,4 @@ Note:
28
28
 
29
29
  - The skill files are included in the npm package.
30
30
  - On install, the package copies them to `$CODEX_HOME/skills` (or `~/.codex/skills` if `CODEX_HOME` is unset).
31
+ - If a stdio MCP client reports `Transport closed`, delete `.npm-python`, reinstall the package, and make sure CLI/user/builder packages are on the same version. The stdio entrypoints refuse runtime bootstrap so install logs never corrupt MCP stdout.
@@ -233,3 +233,12 @@ rm -rf .npm-python
233
233
  ```bash
234
234
  npm install
235
235
  ```
236
+
237
+ 如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
238
+
239
+ 1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
240
+ 2. 删除安装目录下的 `.npm-python`
241
+ 3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
242
+ 4. 再启动 MCP 客户端
243
+
244
+ 现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
@@ -2,4 +2,4 @@
2
2
  import { spawnServer, getPackageRoot } from "../lib/runtime.mjs";
3
3
 
4
4
  const packageRoot = getPackageRoot(import.meta.url);
5
- spawnServer(packageRoot, process.argv.slice(2), "qingflow");
5
+ spawnServer(packageRoot, process.argv.slice(2), "qingflow", { allowRuntimeBootstrap: true });
@@ -7,14 +7,16 @@ const WINDOWS = process.platform === "win32";
7
7
 
8
8
  function runChecked(command, args, options = {}) {
9
9
  const result = spawnSync(command, args, {
10
- stdio: "inherit",
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "pipe"],
11
12
  ...options,
12
13
  });
13
14
  if (result.error) {
14
15
  throw result.error;
15
16
  }
16
17
  if (result.status !== 0) {
17
- throw new Error(`Command failed: ${command} ${args.join(" ")}`);
18
+ const details = [result.stderr, result.stdout].filter((value) => typeof value === "string" && value.trim()).join("\n");
19
+ throw new Error(details ? `Command failed: ${command} ${args.join(" ")}\n${details}` : `Command failed: ${command} ${args.join(" ")}`);
18
20
  }
19
21
  }
20
22
 
@@ -92,6 +94,95 @@ function readPackageVersion(packageRoot) {
92
94
  }
93
95
  }
94
96
 
97
+ function readBootstrapStamp(stampPath) {
98
+ if (!fs.existsSync(stampPath)) {
99
+ return { exists: false, version: null };
100
+ }
101
+ try {
102
+ const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
103
+ const version = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
104
+ return { exists: true, version };
105
+ } catch {
106
+ return { exists: true, version: null };
107
+ }
108
+ }
109
+
110
+ export function inspectPythonEnv(packageRoot, commandName = "qingflow-mcp") {
111
+ const venvDir = getVenvDir(packageRoot);
112
+ const serverCommand = getVenvServerCommand(packageRoot, commandName);
113
+ const stampPath = path.join(venvDir, ".bootstrap.json");
114
+ const packageVersion = readPackageVersion(packageRoot);
115
+ const stamp = readBootstrapStamp(stampPath);
116
+ const problems = [];
117
+
118
+ if (!packageVersion) {
119
+ problems.push("package-version-missing");
120
+ }
121
+ if (!fs.existsSync(serverCommand)) {
122
+ problems.push("server-command-missing");
123
+ }
124
+ if (!stamp.exists) {
125
+ problems.push("bootstrap-stamp-missing");
126
+ } else if (!stamp.version) {
127
+ problems.push("bootstrap-stamp-invalid");
128
+ } else if (packageVersion && stamp.version !== packageVersion) {
129
+ problems.push("bootstrap-version-mismatch");
130
+ }
131
+
132
+ return {
133
+ ready: problems.length === 0,
134
+ packageVersion,
135
+ stampVersion: stamp.version,
136
+ stampExists: stamp.exists,
137
+ stampPath,
138
+ serverCommand,
139
+ venvDir,
140
+ problems,
141
+ };
142
+ }
143
+
144
+ function formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap = false } = {}) {
145
+ const problemLines = [];
146
+ for (const problem of runtime.problems) {
147
+ switch (problem) {
148
+ case "package-version-missing":
149
+ problemLines.push("- package.json is missing a valid version field");
150
+ break;
151
+ case "server-command-missing":
152
+ problemLines.push(`- missing Python entrypoint: ${runtime.serverCommand}`);
153
+ break;
154
+ case "bootstrap-stamp-missing":
155
+ problemLines.push(`- missing bootstrap stamp: ${runtime.stampPath}`);
156
+ break;
157
+ case "bootstrap-stamp-invalid":
158
+ problemLines.push(`- bootstrap stamp is unreadable or invalid: ${runtime.stampPath}`);
159
+ break;
160
+ case "bootstrap-version-mismatch":
161
+ problemLines.push(
162
+ `- bootstrap version mismatch: package=${runtime.packageVersion ?? "unknown"}, runtime=${runtime.stampVersion ?? "unknown"}`
163
+ );
164
+ break;
165
+ default:
166
+ problemLines.push(`- ${problem}`);
167
+ break;
168
+ }
169
+ }
170
+
171
+ const action = allowRuntimeBootstrap
172
+ ? "Delete .npm-python and retry, or rerun npm install to rebuild the embedded Python runtime."
173
+ : "Delete .npm-python and rerun npm install, or reinstall the npm package before starting the MCP server.";
174
+
175
+ const bootstrapNote = allowRuntimeBootstrap
176
+ ? ""
177
+ : "\nRuntime bootstrap is disabled for stdio MCP entrypoints so install logs can never corrupt the MCP stdout transport.";
178
+
179
+ return [
180
+ `[qingflow-mcp] Python runtime for ${commandName} is not ready.`,
181
+ ...problemLines,
182
+ action + bootstrapNote,
183
+ ].join("\n");
184
+ }
185
+
95
186
  function getVenvPip(packageRoot) {
96
187
  return WINDOWS
97
188
  ? path.join(getVenvDir(packageRoot), "Scripts", "pip.exe")
@@ -129,20 +220,9 @@ export function findPython() {
129
220
 
130
221
  export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
131
222
  const python = findPython();
132
- const venvDir = getVenvDir(packageRoot);
223
+ const runtime = inspectPythonEnv(packageRoot, commandName);
133
224
  const venvPython = getVenvPython(packageRoot);
134
- const serverCommand = getVenvServerCommand(packageRoot, commandName);
135
- const stampPath = path.join(venvDir, ".bootstrap.json");
136
- const packageVersion = readPackageVersion(packageRoot);
137
- let stampVersion = null;
138
- if (fs.existsSync(stampPath)) {
139
- try {
140
- const payload = JSON.parse(fs.readFileSync(stampPath, "utf8"));
141
- stampVersion = typeof payload.package_version === "string" && payload.package_version.trim() ? payload.package_version.trim() : null;
142
- } catch {
143
- stampVersion = null;
144
- }
145
- }
225
+ const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
146
226
 
147
227
  if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
148
228
  return serverCommand;
@@ -179,17 +259,72 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
179
259
  return serverCommand;
180
260
  }
181
261
 
182
- export function spawnServer(packageRoot, args, commandName = "qingflow-mcp") {
183
- const serverCommand = fs.existsSync(getVenvServerCommand(packageRoot, commandName))
184
- ? getVenvServerCommand(packageRoot, commandName)
185
- : ensurePythonEnv(packageRoot, { commandName });
262
+ function proxyStreams(child) {
263
+ if (process.stdin.readable && child.stdin) {
264
+ process.stdin.pipe(child.stdin);
265
+ child.stdin.on("error", (error) => {
266
+ if (error.code !== "EPIPE") {
267
+ console.error(`[qingflow-mcp] Failed to forward stdin: ${error.message}`);
268
+ }
269
+ });
270
+ } else if (child.stdin) {
271
+ child.stdin.end();
272
+ }
273
+
274
+ if (child.stdout) {
275
+ child.stdout.pipe(process.stdout);
276
+ }
277
+ if (child.stderr) {
278
+ child.stderr.pipe(process.stderr);
279
+ }
280
+ }
281
+
282
+ function forwardSignal(child, signal) {
283
+ process.on(signal, () => {
284
+ if (!child.killed) {
285
+ child.kill(signal);
286
+ }
287
+ });
288
+ }
289
+
290
+ export function spawnServer(packageRoot, args, commandName = "qingflow-mcp", { allowRuntimeBootstrap = false } = {}) {
291
+ let runtime = inspectPythonEnv(packageRoot, commandName);
292
+ let serverCommand = runtime.serverCommand;
293
+
294
+ if (!runtime.ready) {
295
+ if (!allowRuntimeBootstrap) {
296
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
297
+ process.exit(1);
298
+ return;
299
+ }
300
+
301
+ try {
302
+ serverCommand = ensurePythonEnv(packageRoot, { commandName });
303
+ runtime = inspectPythonEnv(packageRoot, commandName);
304
+ } catch (error) {
305
+ console.error(`[qingflow-mcp] Failed to prepare Python runtime for ${commandName}: ${error.message}`);
306
+ process.exit(1);
307
+ return;
308
+ }
309
+
310
+ if (!runtime.ready) {
311
+ console.error(formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap }));
312
+ process.exit(1);
313
+ return;
314
+ }
315
+ }
186
316
 
187
317
  const child = spawn(serverCommand, args, {
188
- stdio: "inherit",
318
+ stdio: ["pipe", "pipe", "pipe"],
189
319
  env: process.env,
320
+ windowsHide: true,
190
321
  });
191
322
 
192
- child.on("exit", (code, signal) => {
323
+ proxyStreams(child);
324
+ forwardSignal(child, "SIGINT");
325
+ forwardSignal(child, "SIGTERM");
326
+
327
+ child.on("close", (code, signal) => {
193
328
  if (signal) {
194
329
  process.kill(process.pid, signal);
195
330
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.58",
3
+ "version": "0.2.0-beta.59",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b58"
7
+ version = "0.2.0b59"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -161,7 +161,18 @@ class AiBuilderFacade:
161
161
  "package_name is required",
162
162
  suggested_next_call=None,
163
163
  )
164
- listing = self.packages.package_list(profile=profile, trial_status="all", include_raw=False)
164
+ normalized_args = {"package_name": requested}
165
+ try:
166
+ listing = self.packages.package_list(profile=profile, trial_status="all", include_raw=False)
167
+ except (QingflowApiError, RuntimeError) as error:
168
+ api_error = _coerce_api_error(error)
169
+ return _failed_from_api_error(
170
+ "PACKAGE_RESOLVE_FAILED",
171
+ api_error,
172
+ normalized_args=normalized_args,
173
+ details={"package_name": requested},
174
+ suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": "all"}},
175
+ )
165
176
  items = listing.get("items") if isinstance(listing.get("items"), list) else []
166
177
  matches = [
167
178
  {"tag_id": item.get("tagId"), "tag_name": item.get("tagName")}
@@ -212,6 +223,7 @@ class AiBuilderFacade:
212
223
  suggested_next_call=None,
213
224
  )
214
225
  existing = self.package_resolve(profile=profile, package_name=requested)
226
+ lookup_permission_blocked = None
215
227
  if existing.get("status") == "success":
216
228
  return {
217
229
  "status": "success",
@@ -231,6 +243,16 @@ class AiBuilderFacade:
231
243
  }
232
244
  if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
233
245
  return existing
246
+ if existing.get("error_code") == "PACKAGE_RESOLVE_FAILED":
247
+ if existing.get("backend_code") not in {40002, 40027}:
248
+ return existing
249
+ lookup_permission_blocked = {
250
+ "backend_code": existing.get("backend_code"),
251
+ "http_status": existing.get("http_status"),
252
+ "request_id": existing.get("request_id"),
253
+ }
254
+ elif existing.get("error_code") not in {"PACKAGE_NOT_FOUND"}:
255
+ return existing
234
256
  try:
235
257
  created = self.packages.package_create(profile=profile, payload={"tagName": requested})
236
258
  except (QingflowApiError, RuntimeError) as error:
@@ -239,7 +261,10 @@ class AiBuilderFacade:
239
261
  "PACKAGE_CREATE_FAILED",
240
262
  api_error,
241
263
  normalized_args=normalized_args,
242
- details={"package_name": requested},
264
+ details={
265
+ "package_name": requested,
266
+ **({"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {}),
267
+ },
243
268
  suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": requested}},
244
269
  )
245
270
  result = created.get("result") if isinstance(created.get("result"), dict) else {}
@@ -259,7 +284,7 @@ class AiBuilderFacade:
259
284
  "normalized_args": normalized_args,
260
285
  "missing_fields": [],
261
286
  "allowed_values": {},
262
- "details": {},
287
+ "details": {"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {},
263
288
  "request_id": None,
264
289
  "suggested_next_call": None
265
290
  if verified
@@ -1012,7 +1037,20 @@ class AiBuilderFacade:
1012
1037
  details={"app_name": requested, "package_tag_id": package_tag_id, "matches": package_matches},
1013
1038
  suggested_next_call=None,
1014
1039
  )
1015
- search = self.apps.app_search(profile=profile, keyword=requested, page_num=1, page_size=200)
1040
+ search_error: QingflowApiError | None = None
1041
+ try:
1042
+ search = self.apps.app_search(profile=profile, keyword=requested, page_num=1, page_size=200)
1043
+ except (QingflowApiError, RuntimeError) as exc:
1044
+ api_error = _coerce_api_error(exc)
1045
+ if package_tag_id is None or package_tag_id <= 0 or api_error.backend_code not in {40002, 40027}:
1046
+ return _failed_from_api_error(
1047
+ "APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
1048
+ api_error,
1049
+ details={"app_name": requested, "package_tag_id": package_tag_id},
1050
+ suggested_next_call=None,
1051
+ )
1052
+ search = {}
1053
+ search_error = api_error
1016
1054
  apps = search.get("apps") if isinstance(search.get("apps"), list) else []
1017
1055
  matches = []
1018
1056
  for item in apps:
@@ -1046,11 +1084,72 @@ class AiBuilderFacade:
1046
1084
  "tag_ids": tag_ids,
1047
1085
  }
1048
1086
  )
1087
+ if not matches and package_tag_id is not None and package_tag_id > 0 and search_error is not None:
1088
+ visible_matches = self._resolve_app_matches_in_visible_apps(
1089
+ profile=profile,
1090
+ app_name=requested,
1091
+ package_tag_id=package_tag_id,
1092
+ )
1093
+ if len(visible_matches) == 1:
1094
+ match = visible_matches[0]
1095
+ return {
1096
+ "status": "success",
1097
+ "error_code": None,
1098
+ "recoverable": False,
1099
+ "message": "resolved app",
1100
+ "normalized_args": {"app_name": requested, "package_tag_id": package_tag_id},
1101
+ "missing_fields": [],
1102
+ "allowed_values": {},
1103
+ "details": {
1104
+ "match_scope": "visible_apps",
1105
+ "search_permission_blocked": {
1106
+ "backend_code": search_error.backend_code,
1107
+ "http_status": search_error.http_status,
1108
+ "request_id": search_error.request_id,
1109
+ },
1110
+ },
1111
+ "request_id": None,
1112
+ "suggested_next_call": None,
1113
+ "noop": False,
1114
+ "verification": {},
1115
+ **match,
1116
+ }
1117
+ if len(visible_matches) > 1:
1118
+ return _failed(
1119
+ "AMBIGUOUS_APP",
1120
+ f"multiple apps matched '{requested}' inside package {package_tag_id}",
1121
+ details={
1122
+ "app_name": requested,
1123
+ "package_tag_id": package_tag_id,
1124
+ "matches": visible_matches,
1125
+ "search_permission_blocked": {
1126
+ "backend_code": search_error.backend_code,
1127
+ "http_status": search_error.http_status,
1128
+ "request_id": search_error.request_id,
1129
+ },
1130
+ },
1131
+ suggested_next_call=None,
1132
+ )
1049
1133
  if not matches:
1050
1134
  return _failed(
1051
1135
  "APP_NOT_FOUND",
1052
1136
  f"app '{requested}' was not found",
1053
- details={"app_name": requested, "package_tag_id": package_tag_id},
1137
+ details={
1138
+ "app_name": requested,
1139
+ "package_tag_id": package_tag_id,
1140
+ **(
1141
+ {
1142
+ "search_permission_blocked": {
1143
+ "backend_code": search_error.backend_code,
1144
+ "http_status": search_error.http_status,
1145
+ "request_id": search_error.request_id,
1146
+ },
1147
+ "match_scope": "visible_apps_fallback",
1148
+ }
1149
+ if search_error is not None
1150
+ else {}
1151
+ ),
1152
+ },
1054
1153
  suggested_next_call=None,
1055
1154
  )
1056
1155
  if len(matches) > 1:
@@ -1077,6 +1176,39 @@ class AiBuilderFacade:
1077
1176
  **match,
1078
1177
  }
1079
1178
 
1179
+ def _resolve_app_matches_in_visible_apps(
1180
+ self,
1181
+ *,
1182
+ profile: str,
1183
+ app_name: str,
1184
+ package_tag_id: int,
1185
+ ) -> list[JSONObject]:
1186
+ try:
1187
+ listing = self.apps.app_list(profile=profile, ship_auth=False)
1188
+ except (QingflowApiError, RuntimeError):
1189
+ return []
1190
+ items = listing.get("items") if isinstance(listing.get("items"), list) else []
1191
+ matches: list[JSONObject] = []
1192
+ seen_app_keys: set[str] = set()
1193
+ for item in items:
1194
+ if not isinstance(item, dict):
1195
+ continue
1196
+ title = str(item.get("title") or item.get("app_name") or "").strip()
1197
+ if title != app_name:
1198
+ continue
1199
+ candidate_key = str(item.get("app_key") or item.get("appKey") or "").strip()
1200
+ if not candidate_key or candidate_key in seen_app_keys:
1201
+ continue
1202
+ tag_ids = _coerce_int_list(item.get("tag_ids"))
1203
+ tag_id = _coerce_positive_int(item.get("tag_id"))
1204
+ if tag_id is not None and tag_id not in tag_ids:
1205
+ tag_ids.append(tag_id)
1206
+ if package_tag_id not in tag_ids:
1207
+ continue
1208
+ seen_app_keys.add(candidate_key)
1209
+ matches.append({"app_key": candidate_key, "app_name": title, "tag_ids": tag_ids})
1210
+ return matches
1211
+
1080
1212
  def _resolve_app_matches_in_package(
1081
1213
  self,
1082
1214
  *,
@@ -6,29 +6,29 @@ from ..context import CliContext
6
6
 
7
7
 
8
8
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
9
- parser = subparsers.add_parser("apps", help="应用发现")
10
- app_subparsers = parser.add_subparsers(dest="apps_command", required=True)
9
+ parser = subparsers.add_parser("app", help="应用发现")
10
+ app_subparsers = parser.add_subparsers(dest="app_command", required=True)
11
11
 
12
12
  list_parser = app_subparsers.add_parser("list", help="列出可见应用")
13
- list_parser.set_defaults(handler=_handle_list, format_hint="apps_list")
13
+ list_parser.set_defaults(handler=_handle_list, format_hint="app_list")
14
14
 
15
- find = app_subparsers.add_parser("find", help="搜索应用")
16
- find.add_argument("--keyword", default="")
17
- find.add_argument("--page", type=int, default=1)
18
- find.add_argument("--page-size", type=int, default=50)
19
- find.set_defaults(handler=_handle_find, format_hint="apps_list")
15
+ search = app_subparsers.add_parser("search", help="搜索应用")
16
+ search.add_argument("--keyword", default="")
17
+ search.add_argument("--page", type=int, default=1)
18
+ search.add_argument("--page-size", type=int, default=50)
19
+ search.set_defaults(handler=_handle_search, format_hint="app_search")
20
20
 
21
- show = app_subparsers.add_parser("show", help="读取应用信息")
22
- show.add_argument("--app", required=True)
23
- show.set_defaults(handler=_handle_show, format_hint="app_show")
21
+ get = app_subparsers.add_parser("get", help="读取应用可访问视图与导入能力")
22
+ get.add_argument("--app-key", required=True)
23
+ get.set_defaults(handler=_handle_get, format_hint="app_get")
24
24
 
25
25
 
26
26
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
27
- return context.apps.list(profile=args.profile)
27
+ return context.app.app_list(profile=args.profile)
28
28
 
29
29
 
30
- def _handle_find(args: argparse.Namespace, context: CliContext) -> dict:
31
- return context.apps.find(
30
+ def _handle_search(args: argparse.Namespace, context: CliContext) -> dict:
31
+ return context.app.app_search(
32
32
  profile=args.profile,
33
33
  keyword=args.keyword,
34
34
  page_num=args.page,
@@ -36,5 +36,5 @@ def _handle_find(args: argparse.Namespace, context: CliContext) -> dict:
36
36
  )
37
37
 
38
38
 
39
- def _handle_show(args: argparse.Namespace, context: CliContext) -> dict:
40
- return context.apps.show(profile=args.profile, app_key=args.app)
39
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
40
+ return context.app.app_get(profile=args.profile, app_key=args.app_key)
@@ -10,34 +10,33 @@ from .common import read_secret_arg
10
10
 
11
11
 
12
12
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
13
- me = subparsers.add_parser("me", help="查看当前会话")
14
- me.set_defaults(handler=_handle_me, format_hint="me")
13
+ parser = subparsers.add_parser("auth", help="认证与会话")
14
+ auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
15
15
 
16
- login = subparsers.add_parser("login", help="邮箱密码登录")
16
+ login = auth_subparsers.add_parser("login", help="邮箱密码登录")
17
17
  login.add_argument("--base-url")
18
18
  login.add_argument("--qf-version")
19
19
  login.add_argument("--email", required=True)
20
20
  login.add_argument("--password")
21
21
  login.add_argument("--password-stdin", action="store_true")
22
22
  login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
23
- login.set_defaults(handler=_handle_login, format_hint="session")
23
+ login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
24
24
 
25
- use_token = subparsers.add_parser("use-token", help="直接接入 token")
25
+ use_token = auth_subparsers.add_parser("use-token", help="直接注入 token")
26
26
  use_token.add_argument("--base-url")
27
27
  use_token.add_argument("--qf-version")
28
28
  use_token.add_argument("--token")
29
29
  use_token.add_argument("--token-stdin", action="store_true")
30
- use_token.add_argument("--ws", dest="ws_id", type=int)
30
+ use_token.add_argument("--ws-id", type=int)
31
31
  use_token.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
32
- use_token.set_defaults(handler=_handle_use_token, format_hint="session")
33
-
34
- logout = subparsers.add_parser("logout", help="退出当前会话")
35
- logout.add_argument("--forget-persisted", action="store_true")
36
- logout.set_defaults(handler=_handle_logout, format_hint="session")
32
+ use_token.set_defaults(handler=_handle_use_token, format_hint="auth_whoami")
37
33
 
34
+ whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
35
+ whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
38
36
 
39
- def _handle_me(args: argparse.Namespace, context: CliContext) -> dict:
40
- return context.auth.me(profile=args.profile)
37
+ logout = auth_subparsers.add_parser("logout", help="退出登录")
38
+ logout.add_argument("--forget-persisted", action="store_true")
39
+ logout.set_defaults(handler=_handle_logout, format_hint="")
41
40
 
42
41
 
43
42
  def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
@@ -49,7 +48,7 @@ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
49
48
  password = getpass.getpass("Password: ")
50
49
  else:
51
50
  raise QingflowApiError.config_error("password is required; use --password or --password-stdin")
52
- return context.auth.login(
51
+ return context.auth.auth_login(
53
52
  profile=args.profile,
54
53
  base_url=args.base_url,
55
54
  qf_version=args.qf_version,
@@ -61,7 +60,7 @@ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
61
60
 
62
61
  def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
63
62
  token = read_secret_arg(args.token, stdin_enabled=bool(args.token_stdin), label="token")
64
- return context.auth.use_token(
63
+ return context.auth.auth_use_token(
65
64
  profile=args.profile,
66
65
  base_url=args.base_url,
67
66
  qf_version=args.qf_version,
@@ -71,5 +70,9 @@ def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
71
70
  )
72
71
 
73
72
 
73
+ def _handle_whoami(args: argparse.Namespace, context: CliContext) -> dict:
74
+ return context.auth.auth_whoami(profile=args.profile)
75
+
76
+
74
77
  def _handle_logout(args: argparse.Namespace, context: CliContext) -> dict:
75
- return context.auth.logout(profile=args.profile, forget_persisted=bool(args.forget_persisted))
78
+ return context.auth.auth_logout(profile=args.profile, forget_persisted=bool(args.forget_persisted))