@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
@@ -3,11 +3,8 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any, TextIO
5
5
 
6
- from ..ops.base import public_result
7
-
8
6
 
9
7
  def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
10
- result = public_result(result)
11
8
  formatter = _FORMATTERS.get(hint, _format_generic)
12
9
  text = formatter(result)
13
10
  stream.write(text)
@@ -15,65 +12,61 @@ def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> No
15
12
  stream.write("\n")
16
13
 
17
14
 
18
- def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
19
- json.dump(public_result(result), stream, ensure_ascii=False, indent=2)
20
- stream.write("\n")
21
-
22
-
23
15
  def _format_generic(result: dict[str, Any]) -> str:
24
- lines = [f"{result.get('message') or result.get('code') or '完成'}"]
16
+ lines: list[str] = []
17
+ title = _first_present(result, "status", "message")
18
+ if title:
19
+ lines.append(str(title))
25
20
  data = result.get("data")
26
21
  if isinstance(data, dict):
27
- lines.extend(_scalar_lines(data, limit=10))
22
+ scalar_lines = _dict_scalar_lines(data)
23
+ if scalar_lines:
24
+ lines.extend(scalar_lines)
25
+ elif result:
26
+ scalar_lines = _dict_scalar_lines(result)
27
+ if scalar_lines:
28
+ lines.extend(scalar_lines)
29
+ if not lines:
30
+ lines.append(json.dumps(result, ensure_ascii=False, indent=2))
28
31
  _append_warnings(lines, result.get("warnings"))
32
+ _append_verification(lines, result.get("verification"))
29
33
  return "\n".join(lines) + "\n"
30
34
 
31
35
 
32
- def _format_me(result: dict[str, Any]) -> str:
33
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
34
- user = data.get("user") if isinstance(data.get("user"), dict) else {}
35
- workspace = data.get("workspace") if isinstance(data.get("workspace"), dict) else {}
36
+ def _format_whoami(result: dict[str, Any]) -> str:
36
37
  lines = [
37
- f"Profile: {data.get('profile') or result.get('meta', {}).get('profile') or '-'}",
38
- f"User: {user.get('nick_name') or '-'} ({user.get('email') or '-'})",
39
- f"UID: {user.get('uid') or '-'}",
40
- f"Workspace: {workspace.get('name') or '-'} ({workspace.get('ws_id') or '-'})",
41
- f"Base URL: {data.get('base_url') or '-'}",
38
+ f"Profile: {result.get('profile')}",
39
+ f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
40
+ f"UID: {result.get('uid')}",
41
+ f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
42
+ f"Base URL: {result.get('base_url')}",
42
43
  ]
43
44
  return "\n".join(lines) + "\n"
44
45
 
45
46
 
46
47
  def _format_workspace_list(result: dict[str, Any]) -> str:
47
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
48
- items = data.get("items") if isinstance(data.get("items"), list) else []
49
- rows = [
50
- [
51
- str(item.get("ws_id") or ""),
52
- str(item.get("name") or ""),
53
- str(item.get("remark") or ""),
54
- ]
55
- for item in items
56
- if isinstance(item, dict)
57
- ]
48
+ page = result.get("page") if isinstance(result.get("page"), dict) else {}
49
+ items = page.get("list") if isinstance(page.get("list"), list) else []
50
+ rows = []
51
+ for item in items:
52
+ if not isinstance(item, dict):
53
+ continue
54
+ rows.append(
55
+ [
56
+ str(item.get("wsId") or ""),
57
+ str(item.get("workspaceName") or item.get("wsName") or ""),
58
+ str(item.get("remark") or ""),
59
+ ]
60
+ )
58
61
  return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
59
62
 
60
63
 
61
- def _format_workspace_use(result: dict[str, Any]) -> str:
62
- workspace = result.get("data", {}).get("workspace") if isinstance(result.get("data"), dict) else {}
63
- lines = [
64
- f"Workspace: {workspace.get('name') or '-'}",
65
- f"WS ID: {workspace.get('ws_id') or '-'}",
66
- f"QF Version: {result.get('data', {}).get('qf_version') or '-'}",
67
- f"QF Version Source: {result.get('data', {}).get('qf_version_source') or '-'}",
68
- ]
69
- return "\n".join(lines) + "\n"
70
-
71
-
72
- def _format_apps_list(result: dict[str, Any]) -> str:
73
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
74
- items = data.get("items") if isinstance(data.get("items"), list) else []
64
+ def _format_app_items(result: dict[str, Any]) -> str:
65
+ items = result.get("items")
66
+ if not isinstance(items, list):
67
+ items = result.get("apps")
75
68
  rows = []
76
- for item in items:
69
+ for item in items or []:
77
70
  if not isinstance(item, dict):
78
71
  continue
79
72
  rows.append(
@@ -86,7 +79,7 @@ def _format_apps_list(result: dict[str, Any]) -> str:
86
79
  return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
87
80
 
88
81
 
89
- def _format_app_show(result: dict[str, Any]) -> str:
82
+ def _format_app_get(result: dict[str, Any]) -> str:
90
83
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
91
84
  lines = [
92
85
  f"App: {data.get('app_name') or '-'}",
@@ -97,7 +90,8 @@ def _format_app_show(result: dict[str, Any]) -> str:
97
90
  if isinstance(import_capability, dict):
98
91
  lines.append(
99
92
  "Import Capability: "
100
- f"{import_capability.get('auth_source') or 'unknown'} / can_import={import_capability.get('can_import')}"
93
+ f"{import_capability.get('auth_source') or 'unknown'} / "
94
+ f"can_import={import_capability.get('can_import')}"
101
95
  )
102
96
  views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
103
97
  lines.append(f"Accessible Views: {len(views)}")
@@ -108,7 +102,7 @@ def _format_app_show(result: dict[str, Any]) -> str:
108
102
  return "\n".join(lines) + "\n"
109
103
 
110
104
 
111
- def _format_records_list(result: dict[str, Any]) -> str:
105
+ def _format_record_list(result: dict[str, Any]) -> str:
112
106
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
113
107
  items = data.get("items") if isinstance(data.get("items"), list) else []
114
108
  lines = [f"Returned Records: {len(items)}"]
@@ -118,43 +112,40 @@ def _format_records_list(result: dict[str, Any]) -> str:
118
112
  if len(items) > 10:
119
113
  lines.append(f"... {len(items) - 10} more")
120
114
  _append_warnings(lines, result.get("warnings"))
115
+ _append_verification(lines, result.get("verification"))
121
116
  return "\n".join(lines) + "\n"
122
117
 
123
118
 
124
- def _format_record_show(result: dict[str, Any]) -> str:
125
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
126
- record = data.get("record")
127
- if isinstance(record, dict):
128
- lines = [f"Record ID: {data.get('record_id') or '-'}"]
129
- lines.extend(_scalar_lines(record, limit=12))
130
- _append_warnings(lines, result.get("warnings"))
131
- return "\n".join(lines) + "\n"
132
- return _format_generic(result)
133
-
134
-
135
- def _format_record_write(result: dict[str, Any]) -> str:
119
+ def _format_task_list(result: dict[str, Any]) -> str:
136
120
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
137
- lines = [
138
- f"Code: {result.get('code')}",
139
- f"Message: {result.get('message')}",
140
- ]
141
- if data.get("record_id") is not None:
142
- lines.append(f"Record ID: {data.get('record_id')}")
143
- if isinstance(data.get("record_ids"), list):
144
- lines.append(f"Record IDs: {', '.join(str(item) for item in data['record_ids'])}")
121
+ items = data.get("items") if isinstance(data.get("items"), list) else []
122
+ rows = []
123
+ for item in items:
124
+ if not isinstance(item, dict):
125
+ continue
126
+ rows.append(
127
+ [
128
+ str(item.get("app_key") or ""),
129
+ str(item.get("record_id") or ""),
130
+ str(item.get("workflow_node_id") or ""),
131
+ str(item.get("title") or item.get("task_name") or ""),
132
+ ]
133
+ )
134
+ output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
135
+ lines = output.rstrip("\n").split("\n")
145
136
  _append_warnings(lines, result.get("warnings"))
146
137
  return "\n".join(lines) + "\n"
147
138
 
148
139
 
149
140
  def _format_import_verify(result: dict[str, Any]) -> str:
150
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
151
141
  lines = [
152
- f"File: {data.get('file_name') or data.get('file_path') or '-'}",
153
- f"Can Import: {data.get('can_import')}",
154
- f"Apply Rows: {data.get('apply_rows')}",
155
- f"Verification ID: {data.get('verification_id') or '-'}",
142
+ f"App Key: {result.get('app_key') or '-'}",
143
+ f"File: {result.get('file_name') or result.get('file_path') or '-'}",
144
+ f"Can Import: {result.get('can_import')}",
145
+ f"Apply Rows: {result.get('apply_rows')}",
146
+ f"Verification ID: {result.get('verification_id') or '-'}",
156
147
  ]
157
- issues = data.get("issues") if isinstance(data.get("issues"), list) else []
148
+ issues = result.get("issues") if isinstance(result.get("issues"), list) else []
158
149
  if issues:
159
150
  lines.append("Issues:")
160
151
  for issue in issues:
@@ -163,53 +154,50 @@ def _format_import_verify(result: dict[str, Any]) -> str:
163
154
  else:
164
155
  lines.append(f"- {issue}")
165
156
  _append_warnings(lines, result.get("warnings"))
157
+ _append_verification(lines, result.get("verification"))
166
158
  return "\n".join(lines) + "\n"
167
159
 
168
160
 
169
161
  def _format_import_status(result: dict[str, Any]) -> str:
170
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
171
162
  lines = [
172
- f"Status: {data.get('status') or '-'}",
173
- f"Import ID: {data.get('import_id') or '-'}",
174
- f"Process ID: {data.get('process_id_str') or '-'}",
175
- f"Success Rows: {data.get('success_rows') or 0}",
176
- f"Failed Rows: {data.get('failed_rows') or 0}",
177
- f"Progress: {data.get('progress') or '-'}",
163
+ f"Status: {result.get('status') or '-'}",
164
+ f"Import ID: {result.get('import_id') or '-'}",
165
+ f"Process ID: {result.get('process_id_str') or '-'}",
166
+ f"Success Rows: {result.get('success_rows') or 0}",
167
+ f"Failed Rows: {result.get('failed_rows') or 0}",
168
+ f"Progress: {result.get('progress') or '-'}",
178
169
  ]
179
170
  _append_warnings(lines, result.get("warnings"))
171
+ _append_verification(lines, result.get("verification"))
180
172
  return "\n".join(lines) + "\n"
181
173
 
182
174
 
183
- def _format_tasks_list(result: dict[str, Any]) -> str:
184
- data = result.get("data") if isinstance(result.get("data"), dict) else {}
185
- items = data.get("items") if isinstance(data.get("items"), list) else []
186
- rows = []
187
- for item in items:
188
- if not isinstance(item, dict):
189
- continue
190
- rows.append(
191
- [
192
- str(item.get("app_key") or ""),
193
- str(item.get("record_id") or ""),
194
- str(item.get("workflow_node_id") or ""),
195
- str(item.get("title") or ""),
196
- ]
197
- )
198
- output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
199
- lines = output.rstrip("\n").split("\n")
200
- _append_warnings(lines, result.get("warnings"))
201
- return "\n".join(lines) + "\n"
202
-
203
-
204
- def _format_builder_result(result: dict[str, Any]) -> str:
205
- lines = [f"Code: {result.get('code')}", f"Message: {result.get('message')}"]
175
+ def _format_builder_summary(result: dict[str, Any]) -> str:
176
+ lines = []
177
+ if "status" in result:
178
+ lines.append(f"Status: {result.get('status')}")
179
+ if "app_key" in result:
180
+ lines.append(f"App Key: {result.get('app_key')}")
181
+ if "dash_key" in result:
182
+ lines.append(f"Dash Key: {result.get('dash_key')}")
183
+ if "verified" in result:
184
+ lines.append(f"Verified: {result.get('verified')}")
206
185
  data = result.get("data")
207
186
  if isinstance(data, dict):
208
- lines.extend(_scalar_lines(data, limit=10))
187
+ scalar_lines = _dict_scalar_lines(data)
188
+ lines.extend(scalar_lines[:8])
209
189
  _append_warnings(lines, result.get("warnings"))
190
+ _append_verification(lines, result.get("verification"))
191
+ if not lines:
192
+ return _format_generic(result)
210
193
  return "\n".join(lines) + "\n"
211
194
 
212
195
 
196
+ def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
197
+ json.dump(result, stream, ensure_ascii=False, indent=2)
198
+ stream.write("\n")
199
+
200
+
213
201
  def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
214
202
  lines = [title]
215
203
  if not rows:
@@ -219,20 +207,19 @@ def _render_titled_table(title: str, headers: list[str], rows: list[list[str]])
219
207
  for row in rows:
220
208
  for index, cell in enumerate(row):
221
209
  widths[index] = max(widths[index], len(cell))
222
- lines.append(" ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
210
+ header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
211
+ lines.append(header_line)
223
212
  lines.append(" ".join("-" * width for width in widths))
224
213
  for row in rows:
225
214
  lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
226
215
  return "\n".join(lines) + "\n"
227
216
 
228
217
 
229
- def _scalar_lines(payload: dict[str, Any], *, limit: int) -> list[str]:
218
+ def _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
230
219
  lines: list[str] = []
231
220
  for key, value in payload.items():
232
221
  if isinstance(value, (str, int, float, bool)) or value is None:
233
222
  lines.append(f"{key}: {value}")
234
- if len(lines) >= limit:
235
- break
236
223
  return lines
237
224
 
238
225
 
@@ -244,23 +231,39 @@ def _append_warnings(lines: list[str], warnings: Any) -> None:
244
231
  if isinstance(warning, dict):
245
232
  code = warning.get("code")
246
233
  message = warning.get("message")
247
- lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
234
+ if code or message:
235
+ lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
236
+ else:
237
+ lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
248
238
  else:
249
239
  lines.append(f"- {warning}")
250
240
 
251
241
 
242
+ def _append_verification(lines: list[str], verification: Any) -> None:
243
+ if not isinstance(verification, dict) or not verification:
244
+ return
245
+ lines.append("Verification:")
246
+ for key, value in verification.items():
247
+ if isinstance(value, (str, int, float, bool)) or value is None:
248
+ lines.append(f"- {key}: {value}")
249
+
250
+
251
+ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
252
+ for key in keys:
253
+ if key in payload and payload.get(key) is not None:
254
+ return payload.get(key)
255
+ return None
256
+
257
+
252
258
  _FORMATTERS = {
253
- "me": _format_me,
254
- "session": _format_me,
255
- "ws_list": _format_workspace_list,
256
- "ws_use": _format_workspace_use,
257
- "apps_list": _format_apps_list,
258
- "app_show": _format_app_show,
259
- "records_list": _format_records_list,
260
- "record_show": _format_record_show,
261
- "record_write": _format_record_write,
259
+ "auth_whoami": _format_whoami,
260
+ "workspace_list": _format_workspace_list,
261
+ "app_list": _format_app_items,
262
+ "app_search": _format_app_items,
263
+ "app_get": _format_app_get,
264
+ "record_list": _format_record_list,
265
+ "task_list": _format_task_list,
262
266
  "import_verify": _format_import_verify,
263
267
  "import_status": _format_import_status,
264
- "tasks_list": _format_tasks_list,
265
- "builder_result": _format_builder_result,
268
+ "builder_summary": _format_builder_summary,
266
269
  }
@@ -1,23 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import json
4
5
  import sys
5
6
  from typing import Any, Callable, TextIO
6
7
 
7
8
  from ..errors import QingflowApiError
8
- from ..ops.base import normalize_exception
9
- from .commands import register_all_commands
10
9
  from .context import CliContext, build_cli_context
11
10
  from .formatters import emit_json_result, emit_text_result
11
+ from .commands import register_all_commands
12
12
 
13
13
 
14
14
  Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
15
15
 
16
16
 
17
17
  def build_parser() -> argparse.ArgumentParser:
18
- parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI 2.0")
18
+ parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
19
19
  parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
20
- parser.add_argument("--json", action="store_true", help="输出结构化 JSON")
20
+ parser.add_argument("--json", action="store_true", help="输出原始 JSON")
21
21
  subparsers = parser.add_subparsers(dest="command", required=True)
22
22
  register_all_commands(subparsers)
23
23
  return parser
@@ -46,16 +46,15 @@ def run(
46
46
  if handler is None:
47
47
  parser.print_help(out)
48
48
  return 2
49
-
50
49
  context = context_factory()
51
50
  try:
52
51
  result = handler(args, context)
53
52
  except RuntimeError as exc:
54
- result = normalize_exception(exc)
53
+ payload = _parse_error_payload(exc)
54
+ return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
55
55
  except QingflowApiError as exc:
56
- result = normalize_exception(exc)
57
- except Exception as exc: # noqa: BLE001
58
- result = normalize_exception(exc)
56
+ payload = exc.to_dict()
57
+ return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
59
58
  finally:
60
59
  context.close()
61
60
 
@@ -95,17 +94,53 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
95
94
  return global_args + remaining
96
95
 
97
96
 
97
+ def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
98
+ raw = str(exc)
99
+ try:
100
+ payload = json.loads(raw)
101
+ except json.JSONDecodeError:
102
+ return {"category": "runtime", "message": raw}
103
+ return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}
104
+
105
+
106
+ def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
107
+ exit_code = _error_exit_code(payload)
108
+ if json_mode:
109
+ emit_json_result(payload, stream=stdout)
110
+ return exit_code
111
+ lines = [
112
+ f"Category: {payload.get('category') or 'error'}",
113
+ f"Message: {payload.get('message') or 'Unknown error'}",
114
+ ]
115
+ if payload.get("backend_code") is not None:
116
+ lines.append(f"Backend Code: {payload.get('backend_code')}")
117
+ if payload.get("request_id"):
118
+ lines.append(f"Request ID: {payload.get('request_id')}")
119
+ details = payload.get("details")
120
+ if isinstance(details, dict):
121
+ for key, value in details.items():
122
+ if isinstance(value, (str, int, float, bool)) or value is None:
123
+ lines.append(f"{key}: {value}")
124
+ stderr.write("\n".join(lines) + "\n")
125
+ return exit_code
126
+
127
+
128
+ def _error_exit_code(payload: dict[str, Any]) -> int:
129
+ category = str(payload.get("category") or "").lower()
130
+ if category in {"auth", "workspace"}:
131
+ return 3
132
+ return 4
133
+
134
+
98
135
  def _result_exit_code(result: dict[str, Any]) -> int:
99
136
  if not isinstance(result, dict):
100
137
  return 0
101
- if result.get("ok") is not False:
102
- return 0
103
- code = str(result.get("code") or "").upper()
104
- if code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
105
- return 3
106
- if code in {"INVALID_ARGUMENT", "CONFIG"}:
107
- return 2
108
- return 4
138
+ if result.get("ok") is False:
139
+ return 4
140
+ status = str(result.get("status") or "").lower()
141
+ if status in {"failed", "blocked"}:
142
+ return 4
143
+ return 0
109
144
 
110
145
 
111
146
  if __name__ == "__main__":