@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,573 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, TextIO
5
+
6
+
7
+ def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
8
+ formatter = _FORMATTERS.get(hint, _format_generic)
9
+ text = formatter(result)
10
+ stream.write(text)
11
+ if not text.endswith("\n"):
12
+ stream.write("\n")
13
+
14
+
15
+ def _format_generic(result: dict[str, Any]) -> str:
16
+ lines: list[str] = []
17
+ title = _first_present(result, "status", "message")
18
+ if title:
19
+ lines.append(str(title))
20
+ data = result.get("data")
21
+ if isinstance(data, dict):
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))
31
+ _append_warnings(lines, result.get("warnings"))
32
+ _append_verification(lines, result.get("verification"))
33
+ return "\n".join(lines) + "\n"
34
+
35
+
36
+ def _format_whoami(result: dict[str, Any]) -> str:
37
+ lines = [
38
+ f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
39
+ f"UID: {result.get('uid')}",
40
+ f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
41
+ f"Workspace QF Version: {result.get('qf_version') or '-'}",
42
+ ]
43
+ cli_auth = result.get("cli_auth") if isinstance(result.get("cli_auth"), dict) else {}
44
+ if cli_auth:
45
+ lines.append(f"Login Flow: {cli_auth.get('flow') or '-'}")
46
+ if cli_auth.get("verification_uri"):
47
+ lines.append(f"Verification URL: {cli_auth.get('verification_uri')}")
48
+ if cli_auth.get("user_code"):
49
+ lines.append(f"User Code: {cli_auth.get('user_code')}")
50
+ request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
51
+ route_qf_version = request_route.get("qf_version")
52
+ if route_qf_version and route_qf_version != result.get("qf_version"):
53
+ lines.append(f"Request Route QF Version: {route_qf_version}")
54
+ lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
55
+ departments = result.get("departments") if isinstance(result.get("departments"), list) else []
56
+ roles = result.get("roles") if isinstance(result.get("roles"), list) else []
57
+ if departments:
58
+ lines.append(
59
+ "Departments: "
60
+ + ", ".join(
61
+ str(item.get("dept_name") or item.get("dept_id"))
62
+ for item in departments
63
+ if isinstance(item, dict)
64
+ )
65
+ )
66
+ if roles:
67
+ lines.append(
68
+ "Roles: "
69
+ + ", ".join(
70
+ str(item.get("role_name") or item.get("role_id"))
71
+ for item in roles
72
+ if isinstance(item, dict)
73
+ )
74
+ )
75
+ _append_warnings(lines, result.get("warnings"))
76
+ return "\n".join(lines) + "\n"
77
+
78
+
79
+ def _format_workspace_list(result: dict[str, Any]) -> str:
80
+ page = result.get("page") if isinstance(result.get("page"), dict) else {}
81
+ items = page.get("list") if isinstance(page.get("list"), list) else []
82
+ rows = []
83
+ for item in items:
84
+ if not isinstance(item, dict):
85
+ continue
86
+ rows.append(
87
+ [
88
+ str(item.get("wsId") or ""),
89
+ str(item.get("workspaceName") or item.get("wsName") or ""),
90
+ str(item.get("remark") or ""),
91
+ ]
92
+ )
93
+ return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
94
+
95
+
96
+ def _format_workspace_get(result: dict[str, Any]) -> str:
97
+ workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
98
+ lines = [
99
+ f"Workspace: {workspace.get('workspaceName') or workspace.get('wsName') or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
100
+ f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
101
+ f"Identity: {workspace.get('identity') or '-'}",
102
+ f"Auth: {workspace.get('auth') if workspace.get('auth') is not None else '-'}",
103
+ f"State: {workspace.get('state') if workspace.get('state') is not None else '-'}",
104
+ ]
105
+ _append_warnings(lines, result.get("warnings"))
106
+ return "\n".join(lines) + "\n"
107
+
108
+
109
+ def _format_app_items(result: dict[str, Any]) -> str:
110
+ items = result.get("items")
111
+ if not isinstance(items, list):
112
+ items = result.get("apps")
113
+ rows = []
114
+ for item in items or []:
115
+ if not isinstance(item, dict):
116
+ continue
117
+ rows.append(
118
+ [
119
+ str(item.get("app_key") or item.get("appKey") or ""),
120
+ str(item.get("app_name") or item.get("name") or item.get("title") or ""),
121
+ str(item.get("package_name") or item.get("tag_name") or ""),
122
+ ]
123
+ )
124
+ return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
125
+
126
+
127
+ def _format_app_get(result: dict[str, Any]) -> str:
128
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
129
+ lines = [
130
+ f"App: {data.get('app_name') or '-'}",
131
+ f"App Key: {data.get('app_key') or '-'}",
132
+ f"Can Create: {data.get('can_create')}",
133
+ ]
134
+ import_capability = data.get("import_capability")
135
+ if isinstance(import_capability, dict):
136
+ lines.append(
137
+ "Import Capability: "
138
+ f"{import_capability.get('auth_source') or 'unknown'} / "
139
+ f"can_import={import_capability.get('can_import')}"
140
+ )
141
+ editability = data.get("editability") if isinstance(data.get("editability"), dict) else {}
142
+ if editability:
143
+ lines.append(
144
+ "Editability: "
145
+ f"app_base={editability.get('can_edit_app_base')} / "
146
+ f"form={editability.get('can_edit_form')} / "
147
+ f"flow={editability.get('can_edit_flow')} / "
148
+ f"views={editability.get('can_edit_views')} / "
149
+ f"charts={editability.get('can_edit_charts')}"
150
+ )
151
+ views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
152
+ lines.append(f"Accessible Views: {len(views)}")
153
+ for item in views[:10]:
154
+ if isinstance(item, dict):
155
+ lines.append(f"- {item.get('view_id')}: {item.get('name')}")
156
+ _append_warnings(lines, result.get("warnings"))
157
+ return "\n".join(lines) + "\n"
158
+
159
+ def _format_record_list(result: dict[str, Any]) -> str:
160
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
161
+ items = data.get("items") if isinstance(data.get("items"), list) else []
162
+ lines = [f"Returned Records: {len(items)}"]
163
+ for item in items[:10]:
164
+ if isinstance(item, dict):
165
+ lines.append(json.dumps(item, ensure_ascii=False))
166
+ if len(items) > 10:
167
+ lines.append(f"... {len(items) - 10} more")
168
+ _append_warnings(lines, result.get("warnings"))
169
+ _append_verification(lines, result.get("verification"))
170
+ return "\n".join(lines) + "\n"
171
+
172
+
173
+ def _format_task_list(result: dict[str, Any]) -> str:
174
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
175
+ items = data.get("items") if isinstance(data.get("items"), list) else []
176
+ lines = ["Tasks"]
177
+ for item in items:
178
+ if not isinstance(item, dict):
179
+ continue
180
+ header_parts = [
181
+ str(item.get("task_id") or "-"),
182
+ str(item.get("app_name") or item.get("app_key") or "-"),
183
+ str(item.get("workflow_node_name") or "-"),
184
+ ]
185
+ apply_time = item.get("apply_time")
186
+ if apply_time not in (None, ""):
187
+ header_parts.append(str(apply_time))
188
+ lines.append("- " + " / ".join(header_parts))
189
+ summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
190
+ for summary in summary_fields:
191
+ if not isinstance(summary, dict):
192
+ continue
193
+ lines.append(f" {summary.get('title') or '-'}: {summary.get('answer') or '-'}")
194
+ _append_warnings(lines, result.get("warnings"))
195
+ return "\n".join(lines) + "\n"
196
+
197
+
198
+ def _format_task_get(result: dict[str, Any]) -> str:
199
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
200
+ task = data.get("task") if isinstance(data.get("task"), dict) else {}
201
+ record_summary = data.get("record_summary") if isinstance(data.get("record_summary"), dict) else {}
202
+ editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
203
+ available_actions = data.get("available_actions") if isinstance(data.get("available_actions"), list) else []
204
+ extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
205
+ initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
206
+ initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
207
+ lines = []
208
+ if task.get("task_id") not in (None, ""):
209
+ lines.append(f"Task ID: {task.get('task_id')}")
210
+ lines.extend(
211
+ [
212
+ f"Locator: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
213
+ f"Node: {task.get('workflow_node_name') or '-'}",
214
+ f"App: {task.get('app_name') or '-'}",
215
+ f"Initiator: {initiator_label}",
216
+ f"Apply Status: {record_summary.get('apply_status')}",
217
+ f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
218
+ f"Editable Fields: {len(editable_fields)}",
219
+ ]
220
+ )
221
+ core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
222
+ if core_fields:
223
+ lines.append("Core Fields:")
224
+ for key, value in list(core_fields.items())[:12]:
225
+ lines.append(f"- {key}: {value}")
226
+ if editable_fields:
227
+ lines.append("Editable Fields:")
228
+ for item in editable_fields[:10]:
229
+ if isinstance(item, dict):
230
+ lines.append(f"- {item.get('title') or '-'} ({item.get('kind') or 'field'})")
231
+ associated_reports = extras.get("associated_reports") if isinstance(extras.get("associated_reports"), dict) else {}
232
+ rollback_candidates = extras.get("rollback_candidates") if isinstance(extras.get("rollback_candidates"), dict) else {}
233
+ transfer_candidates = extras.get("transfer_candidates") if isinstance(extras.get("transfer_candidates"), dict) else {}
234
+ lines.append(
235
+ "Extras: "
236
+ f"reports={associated_reports.get('count', 0)}, "
237
+ f"rollback={rollback_candidates.get('count', 0)}, "
238
+ f"transfer={transfer_candidates.get('count', 0)}"
239
+ )
240
+ transfer_items = transfer_candidates.get("items") if isinstance(transfer_candidates.get("items"), list) else []
241
+ if transfer_items:
242
+ lines.append("Transfer Candidates:")
243
+ for item in transfer_items:
244
+ if isinstance(item, dict):
245
+ display = item.get("name") or item.get("uid") or item
246
+ suffix = f" <{item.get('email')}>" if item.get("email") else ""
247
+ lines.append(f"- {display}{suffix} (uid={item.get('uid') or '-'})")
248
+ _append_warnings(lines, result.get("warnings"))
249
+ return "\n".join(lines) + "\n"
250
+
251
+
252
+ def _format_task_action(result: dict[str, Any]) -> str:
253
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
254
+ action = str(data.get("action") or "").strip().lower()
255
+ status = str(result.get("status") or "").strip().lower()
256
+
257
+ if status == "failed" or result.get("ok") is False:
258
+ lines = [_task_action_failure_label(action)]
259
+ reason = _task_action_failure_reason(result)
260
+ if reason:
261
+ lines.append(f"原因:{reason}")
262
+ debug_lines = _task_action_debug_lines(result)
263
+ if debug_lines:
264
+ lines.append("调试信息:")
265
+ lines.extend(f"- {line}" for line in debug_lines)
266
+ return "\n".join(lines) + "\n"
267
+
268
+ if status == "partial_success":
269
+ lines = [_task_action_success_label(action)]
270
+ lines.append(f"说明:{_task_action_partial_success_message(result)}")
271
+ return "\n".join(lines) + "\n"
272
+
273
+ return _task_action_success_label(action) + "\n"
274
+
275
+
276
+ def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
277
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
278
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
279
+ result_type = str(data.get("result_type") or "-")
280
+ context = data.get("context") if isinstance(data.get("context"), dict) else {}
281
+ lines = []
282
+ if selection.get("task_id") not in (None, ""):
283
+ lines.append(f"Task ID: {selection.get('task_id')}")
284
+ lines.extend(
285
+ [
286
+ f"Report: {selection.get('chart_name') or '-'} ({selection.get('report_id') or '-'})",
287
+ f"Type: {result_type}",
288
+ ]
289
+ )
290
+ if result_type == "view_list":
291
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
292
+ items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
293
+ lines.append(f"Returned Records: {len(items)}")
294
+ for item in items[:10]:
295
+ if isinstance(item, dict):
296
+ lines.append(json.dumps(item, ensure_ascii=False))
297
+ if len(items) > 10:
298
+ lines.append(f"... {len(items) - 10} more")
299
+ elif result_type == "chart_data":
300
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
301
+ summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
302
+ rows = result_payload.get("rows") if isinstance(result_payload.get("rows"), list) else []
303
+ if summary:
304
+ lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
305
+ lines.append(f"Rows: {len(rows)}")
306
+ for row in rows[:10]:
307
+ if isinstance(row, dict):
308
+ lines.append(json.dumps(row, ensure_ascii=False))
309
+ if len(rows) > 10:
310
+ lines.append(f"... {len(rows) - 10} more")
311
+ resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
312
+ if resolved_filters:
313
+ lines.append(f"Resolved Filters: {len(resolved_filters)}")
314
+ _append_warnings(lines, result.get("warnings"))
315
+ return "\n".join(lines) + "\n"
316
+
317
+
318
+ def _format_import_verify(result: dict[str, Any]) -> str:
319
+ lines = [
320
+ f"App Key: {result.get('app_key') or '-'}",
321
+ f"File: {result.get('file_name') or result.get('file_path') or '-'}",
322
+ f"Can Import: {result.get('can_import')}",
323
+ f"Verification ID: {result.get('verification_id') or '-'}",
324
+ ]
325
+ issue_summary = result.get("issue_summary") if isinstance(result.get("issue_summary"), dict) else {}
326
+ if issue_summary:
327
+ lines.append(
328
+ "Issues: "
329
+ f"total={issue_summary.get('total', 0)}, "
330
+ f"errors={issue_summary.get('errors', 0)}, "
331
+ f"warnings={issue_summary.get('warnings', 0)}"
332
+ )
333
+ sample = issue_summary.get("sample") if isinstance(issue_summary.get("sample"), list) else []
334
+ if sample:
335
+ lines.append("Issue Samples:")
336
+ for item in sample:
337
+ if isinstance(item, dict):
338
+ lines.append(f"- {item.get('code') or 'ISSUE'}: {item.get('message') or ''}".rstrip())
339
+ _append_warnings(lines, result.get("warnings"))
340
+ _append_verification(lines, result.get("verification"))
341
+ return "\n".join(lines) + "\n"
342
+
343
+
344
+ def _format_import_status(result: dict[str, Any]) -> str:
345
+ lines = [
346
+ f"Status: {result.get('status') or '-'}",
347
+ f"Import ID: {result.get('import_id') or '-'}",
348
+ f"Process ID: {result.get('process_id_str') or '-'}",
349
+ f"Total Rows: {result.get('total') or 0}",
350
+ f"Finished Rows: {result.get('finished') or 0}",
351
+ f"Succeeded Rows: {result.get('succeeded') or 0}",
352
+ f"Failed Rows: {result.get('failed') or 0}",
353
+ f"Progress: {result.get('progress') or '-'}",
354
+ ]
355
+ _append_warnings(lines, result.get("warnings"))
356
+ _append_verification(lines, result.get("verification"))
357
+ return "\n".join(lines) + "\n"
358
+
359
+
360
+ def _format_builder_summary(result: dict[str, Any]) -> str:
361
+ lines = []
362
+ if "status" in result:
363
+ lines.append(f"Status: {result.get('status')}")
364
+ if "app_key" in result:
365
+ lines.append(f"App Key: {result.get('app_key')}")
366
+ if "dash_key" in result:
367
+ lines.append(f"Dash Key: {result.get('dash_key')}")
368
+ if "verified" in result:
369
+ lines.append(f"Verified: {result.get('verified')}")
370
+ data = result.get("data")
371
+ if isinstance(data, dict):
372
+ scalar_lines = _dict_scalar_lines(data)
373
+ lines.extend(scalar_lines[:8])
374
+ _append_warnings(lines, result.get("warnings"))
375
+ _append_verification(lines, result.get("verification"))
376
+ if not lines:
377
+ return _format_generic(result)
378
+ return "\n".join(lines) + "\n"
379
+
380
+
381
+ def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
382
+ json.dump(result, stream, ensure_ascii=False, indent=2)
383
+ stream.write("\n")
384
+
385
+
386
+ def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
387
+ lines = [title]
388
+ if not rows:
389
+ lines.append("(empty)")
390
+ return "\n".join(lines) + "\n"
391
+ widths = [len(header) for header in headers]
392
+ for row in rows:
393
+ for index, cell in enumerate(row):
394
+ widths[index] = max(widths[index], len(cell))
395
+ header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
396
+ lines.append(header_line)
397
+ lines.append(" ".join("-" * width for width in widths))
398
+ for row in rows:
399
+ lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
400
+ return "\n".join(lines) + "\n"
401
+
402
+
403
+ def _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
404
+ lines: list[str] = []
405
+ for key, value in payload.items():
406
+ if isinstance(value, (str, int, float, bool)) or value is None:
407
+ lines.append(f"{key}: {value}")
408
+ return lines
409
+
410
+
411
+ def _append_warnings(lines: list[str], warnings: Any) -> None:
412
+ if not isinstance(warnings, list) or not warnings:
413
+ return
414
+ lines.append("Warnings:")
415
+ for warning in warnings:
416
+ if isinstance(warning, dict):
417
+ code = warning.get("code")
418
+ message = warning.get("message")
419
+ if code or message:
420
+ lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
421
+ else:
422
+ lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
423
+ else:
424
+ lines.append(f"- {warning}")
425
+
426
+
427
+ def _append_verification(lines: list[str], verification: Any) -> None:
428
+ if not isinstance(verification, dict) or not verification:
429
+ return
430
+ lines.append("Verification:")
431
+ for key, value in verification.items():
432
+ if isinstance(value, (str, int, float, bool)) or value is None:
433
+ lines.append(f"- {key}: {value}")
434
+
435
+
436
+ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
437
+ for key in keys:
438
+ if key in payload and payload.get(key) is not None:
439
+ return payload.get(key)
440
+ return None
441
+
442
+
443
+ def _task_action_success_label(action: str) -> str:
444
+ return {
445
+ "approve": "已通过",
446
+ "reject": "已驳回",
447
+ "rollback": "已退回",
448
+ "transfer": "已转交",
449
+ "save_only": "已保存",
450
+ "urge": "已催办",
451
+ }.get(action, "已执行")
452
+
453
+
454
+ def _task_action_failure_label(action: str) -> str:
455
+ return {
456
+ "approve": "审批失败",
457
+ "reject": "驳回失败",
458
+ "rollback": "退回失败",
459
+ "transfer": "转交失败",
460
+ "save_only": "保存失败",
461
+ "urge": "催办失败",
462
+ }.get(action, "执行失败")
463
+
464
+
465
+ def _task_action_partial_success_message(result: dict[str, Any]) -> str:
466
+ error_code = str(result.get("error_code") or "").strip().upper()
467
+ if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
468
+ return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
469
+ if error_code == "TASK_ALREADY_PROCESSED":
470
+ return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
471
+ warnings = result.get("warnings")
472
+ if isinstance(warnings, list):
473
+ for warning in warnings:
474
+ if not isinstance(warning, dict):
475
+ continue
476
+ code = str(warning.get("code") or "").strip().upper()
477
+ if code == "TASK_ALREADY_PROCESSED_UNCONFIRMED_ACTOR":
478
+ return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
479
+ if code == "WORKFLOW_CONTINUATION_UNVERIFIED":
480
+ return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
481
+ return "动作已提交,但结果验证不完整。可使用 --json 查看详细信息。"
482
+
483
+
484
+ def _task_action_failure_reason(result: dict[str, Any]) -> str | None:
485
+ error_code = str(result.get("error_code") or "").strip().upper()
486
+ mapped_error = {
487
+ "TASK_CONTEXT_VISIBILITY_UNVERIFIED": "当前待办已不可操作,且系统未能确认是否已被处理。",
488
+ "TASK_SAVE_ONLY_VERIFICATION_FAILED": "保存请求已发送,但未能确认字段是否全部保存成功。",
489
+ "WORKFLOW_CONTINUATION_UNVERIFIED": "动作已提交,但暂未验证到流程继续推进。",
490
+ "TASK_ALREADY_PROCESSED": "当前待办已不可操作,系统判断流程可能已被其他人处理。",
491
+ }.get(error_code)
492
+ if mapped_error:
493
+ return mapped_error
494
+
495
+ warnings = result.get("warnings")
496
+ if isinstance(warnings, list):
497
+ for warning in warnings:
498
+ if isinstance(warning, dict) and warning.get("message"):
499
+ return str(warning.get("message"))
500
+
501
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
502
+ transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
503
+ backend_code = transport_error.get("backend_code")
504
+ http_status = transport_error.get("http_status")
505
+ if backend_code not in (None, ""):
506
+ return f"后端返回错误码 {backend_code}。"
507
+ if http_status not in (None, ""):
508
+ return f"请求返回 HTTP {http_status}。"
509
+
510
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
511
+ record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
512
+ backend_code = record_state_error.get("backend_code")
513
+ http_status = record_state_error.get("http_status")
514
+ if backend_code not in (None, ""):
515
+ return f"后端返回错误码 {backend_code}。"
516
+ if http_status not in (None, ""):
517
+ return f"请求返回 HTTP {http_status}。"
518
+ if error_code:
519
+ return f"错误码:{error_code}"
520
+ return None
521
+
522
+
523
+ def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
524
+ lines: list[str] = []
525
+ error_code = result.get("error_code")
526
+ if error_code not in (None, ""):
527
+ lines.append(f"error_code: {error_code}")
528
+
529
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
530
+ transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
531
+ for key in ("backend_code", "http_status", "category"):
532
+ value = transport_error.get(key)
533
+ if value not in (None, ""):
534
+ lines.append(f"{key}: {value}")
535
+
536
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
537
+ for key in (
538
+ "runtime_continuation_verified",
539
+ "task_context_visibility_verified",
540
+ "fields_saved_verified",
541
+ "task_still_actionable",
542
+ "workflow_not_advanced",
543
+ "record_state_readable",
544
+ ):
545
+ if key in verification and verification.get(key) is not None:
546
+ lines.append(f"{key}: {verification.get(key)}")
547
+
548
+ record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
549
+ for key in ("backend_code", "http_status", "category"):
550
+ value = record_state_error.get(key)
551
+ if value not in (None, ""):
552
+ entry = f"record_state_{key}: {value}"
553
+ if entry not in lines:
554
+ lines.append(entry)
555
+ return lines
556
+
557
+
558
+ _FORMATTERS = {
559
+ "auth_whoami": _format_whoami,
560
+ "workspace_list": _format_workspace_list,
561
+ "workspace_get": _format_workspace_get,
562
+ "app_list": _format_app_items,
563
+ "app_search": _format_app_items,
564
+ "app_get": _format_app_get,
565
+ "record_list": _format_record_list,
566
+ "task_list": _format_task_list,
567
+ "task_get": _format_task_get,
568
+ "task_action_execute": _format_task_action,
569
+ "task_associated_report_detail_get": _format_task_associated_report_detail,
570
+ "import_verify": _format_import_verify,
571
+ "import_status": _format_import_status,
572
+ "builder_summary": _format_builder_summary,
573
+ }
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..errors import QingflowApiError
9
+
10
+
11
+ def load_json_value(path: str, *, option_name: str) -> Any:
12
+ if not path:
13
+ raise QingflowApiError.config_error(f"{option_name} is required")
14
+ if path == "-":
15
+ raw = sys.stdin.read()
16
+ else:
17
+ try:
18
+ raw = Path(path).expanduser().read_text(encoding="utf-8")
19
+ except OSError as exc:
20
+ raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
21
+ try:
22
+ return json.loads(raw)
23
+ except json.JSONDecodeError as exc:
24
+ raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
25
+
26
+
27
+ def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
28
+ payload = load_json_value(path, option_name=option_name)
29
+ if not isinstance(payload, dict):
30
+ raise QingflowApiError.config_error(f"{option_name} must be a JSON object")
31
+ return payload
32
+
33
+
34
+ def load_json_list(path: str, *, option_name: str) -> list[Any]:
35
+ payload = load_json_value(path, option_name=option_name)
36
+ if not isinstance(payload, list):
37
+ raise QingflowApiError.config_error(f"{option_name} must be a JSON array")
38
+ return payload
39
+
40
+
41
+ def load_optional_json_object(path: str | None, *, option_name: str) -> dict[str, Any] | None:
42
+ if not path:
43
+ return None
44
+ return load_json_object(path, option_name=option_name)
45
+
46
+
47
+ def load_optional_json_list(path: str | None, *, option_name: str) -> list[Any]:
48
+ if not path:
49
+ return []
50
+ return load_json_list(path, option_name=option_name)