@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4
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.
- package/README.md +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +21 -12
- package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
- package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
- package/skills/qingflow-app-user/references/record-patterns.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +44 -2
- package/skills/qingflow-record-insert/SKILL.md +3 -0
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +31 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +58 -9
- package/src/qingflow_mcp/builder_facade/service.py +1711 -240
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -3
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +701 -27
- package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +424 -50
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +11 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +4 -4
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +16 -1
- package/src/qingflow_mcp/response_trim.py +394 -9
- package/src/qingflow_mcp/server.py +26 -0
- package/src/qingflow_mcp/server_app_builder.py +15 -1
- package/src/qingflow_mcp/server_app_user.py +113 -0
- package/src/qingflow_mcp/session_store.py +126 -21
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +243 -9
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +78 -4
- package/src/qingflow_mcp/tools/record_tools.py +551 -165
- package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
- package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import textwrap
|
|
4
5
|
from typing import Any, TextIO
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
8
|
-
|
|
9
|
-
text
|
|
9
|
+
text = _format_cancelled_result(result)
|
|
10
|
+
if text is None:
|
|
11
|
+
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
12
|
+
text = formatter(result)
|
|
10
13
|
stream.write(text)
|
|
11
14
|
if not text.endswith("\n"):
|
|
12
15
|
stream.write("\n")
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
def _format_cancelled_result(result: dict[str, Any]) -> str | None:
|
|
19
|
+
if str(result.get("status") or "").lower() != "cancelled":
|
|
20
|
+
return None
|
|
21
|
+
return str(result.get("message") or "已取消") + "\n"
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
def _format_generic(result: dict[str, Any]) -> str:
|
|
16
25
|
lines: list[str] = []
|
|
17
26
|
title = _first_present(result, "status", "message")
|
|
@@ -38,8 +47,19 @@ def _format_whoami(result: dict[str, Any]) -> str:
|
|
|
38
47
|
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
39
48
|
f"UID: {result.get('uid')}",
|
|
40
49
|
f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
|
|
41
|
-
f"QF Version: {result.get('qf_version') or '-'}",
|
|
50
|
+
f"Workspace QF Version: {result.get('qf_version') or '-'}",
|
|
42
51
|
]
|
|
52
|
+
cli_auth = result.get("cli_auth") if isinstance(result.get("cli_auth"), dict) else {}
|
|
53
|
+
if cli_auth:
|
|
54
|
+
lines.append(f"Login Flow: {cli_auth.get('flow') or '-'}")
|
|
55
|
+
if cli_auth.get("verification_uri"):
|
|
56
|
+
lines.append(f"Verification URL: {cli_auth.get('verification_uri')}")
|
|
57
|
+
if cli_auth.get("user_code"):
|
|
58
|
+
lines.append(f"User Code: {cli_auth.get('user_code')}")
|
|
59
|
+
request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
|
|
60
|
+
route_qf_version = request_route.get("qf_version")
|
|
61
|
+
if route_qf_version and route_qf_version != result.get("qf_version"):
|
|
62
|
+
lines.append(f"Request Route QF Version: {route_qf_version}")
|
|
43
63
|
lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
|
|
44
64
|
departments = result.get("departments") if isinstance(result.get("departments"), list) else []
|
|
45
65
|
roles = result.get("roles") if isinstance(result.get("roles"), list) else []
|
|
@@ -82,6 +102,34 @@ def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
|
82
102
|
return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
83
103
|
|
|
84
104
|
|
|
105
|
+
def _format_workspace_get(result: dict[str, Any]) -> str:
|
|
106
|
+
workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
|
|
107
|
+
lines = [
|
|
108
|
+
f"Workspace: {workspace.get('workspaceName') or workspace.get('wsName') or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
|
|
109
|
+
f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
|
|
110
|
+
f"Identity: {workspace.get('identity') or '-'}",
|
|
111
|
+
f"Auth: {workspace.get('auth') if workspace.get('auth') is not None else '-'}",
|
|
112
|
+
f"State: {workspace.get('state') if workspace.get('state') is not None else '-'}",
|
|
113
|
+
]
|
|
114
|
+
_append_warnings(lines, result.get("warnings"))
|
|
115
|
+
return "\n".join(lines) + "\n"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _format_workspace_select(result: dict[str, Any]) -> str:
|
|
119
|
+
status = str(result.get("status") or "").lower()
|
|
120
|
+
if status == "cancelled":
|
|
121
|
+
return str(result.get("message") or "已取消") + "\n"
|
|
122
|
+
|
|
123
|
+
workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
|
|
124
|
+
workspace_name = workspace.get("workspaceName") or workspace.get("wsName") or result.get("selected", {}).get("workspace_name") if isinstance(result.get("selected"), dict) else None
|
|
125
|
+
lines = [
|
|
126
|
+
f"已切换到: {workspace_name or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
|
|
127
|
+
f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
|
|
128
|
+
]
|
|
129
|
+
_append_warnings(lines, result.get("warnings"))
|
|
130
|
+
return "\n".join(lines) + "\n"
|
|
131
|
+
|
|
132
|
+
|
|
85
133
|
def _format_app_items(result: dict[str, Any]) -> str:
|
|
86
134
|
items = result.get("items")
|
|
87
135
|
if not isinstance(items, list):
|
|
@@ -118,6 +166,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
118
166
|
if editability:
|
|
119
167
|
lines.append(
|
|
120
168
|
"Editability: "
|
|
169
|
+
f"app_base={editability.get('can_edit_app_base')} / "
|
|
121
170
|
f"form={editability.get('can_edit_form')} / "
|
|
122
171
|
f"flow={editability.get('can_edit_flow')} / "
|
|
123
172
|
f"views={editability.get('can_edit_views')} / "
|
|
@@ -148,20 +197,24 @@ def _format_record_list(result: dict[str, Any]) -> str:
|
|
|
148
197
|
def _format_task_list(result: dict[str, Any]) -> str:
|
|
149
198
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
150
199
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
151
|
-
|
|
200
|
+
lines = ["Tasks"]
|
|
152
201
|
for item in items:
|
|
153
202
|
if not isinstance(item, dict):
|
|
154
203
|
continue
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
204
|
+
header_parts = [
|
|
205
|
+
str(item.get("task_id") or "-"),
|
|
206
|
+
str(item.get("app_name") or item.get("app_key") or "-"),
|
|
207
|
+
str(item.get("workflow_node_name") or "-"),
|
|
208
|
+
]
|
|
209
|
+
apply_time = item.get("apply_time")
|
|
210
|
+
if apply_time not in (None, ""):
|
|
211
|
+
header_parts.append(str(apply_time))
|
|
212
|
+
lines.append("- " + " / ".join(header_parts))
|
|
213
|
+
summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
|
|
214
|
+
for summary in summary_fields:
|
|
215
|
+
if not isinstance(summary, dict):
|
|
216
|
+
continue
|
|
217
|
+
lines.append(f" {summary.get('title') or '-'}: {summary.get('answer') or '-'}")
|
|
165
218
|
_append_warnings(lines, result.get("warnings"))
|
|
166
219
|
return "\n".join(lines) + "\n"
|
|
167
220
|
|
|
@@ -169,34 +222,150 @@ def _format_task_list(result: dict[str, Any]) -> str:
|
|
|
169
222
|
def _format_task_get(result: dict[str, Any]) -> str:
|
|
170
223
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
171
224
|
task = data.get("task") if isinstance(data.get("task"), dict) else {}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
f"
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
record_summary = data.get("record_summary") if isinstance(data.get("record_summary"), dict) else {}
|
|
226
|
+
editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
|
|
227
|
+
available_actions = data.get("available_actions") if isinstance(data.get("available_actions"), list) else []
|
|
228
|
+
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
229
|
+
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
230
|
+
initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
|
|
231
|
+
lines = []
|
|
232
|
+
if task.get("task_id") not in (None, ""):
|
|
233
|
+
lines.append(f"Task ID: {task.get('task_id')}")
|
|
234
|
+
lines.extend(
|
|
235
|
+
[
|
|
236
|
+
f"Locator: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
|
|
237
|
+
f"Node: {task.get('workflow_node_name') or '-'}",
|
|
238
|
+
f"App: {task.get('app_name') or '-'}",
|
|
239
|
+
f"Initiator: {initiator_label}",
|
|
240
|
+
f"Apply Status: {record_summary.get('apply_status')}",
|
|
241
|
+
f"Apply Number: {record_summary.get('custom_apply_num') or record_summary.get('apply_num') or '-'}",
|
|
242
|
+
f"Apply Time: {record_summary.get('apply_time') or '-'}",
|
|
243
|
+
f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
|
|
244
|
+
f"Editable Fields: {len(editable_fields)}",
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
all_fields = record_summary.get("all_fields") if isinstance(record_summary.get("all_fields"), dict) else {}
|
|
248
|
+
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
249
|
+
if all_fields:
|
|
250
|
+
lines.append("Fields:")
|
|
251
|
+
for key, value in all_fields.items():
|
|
252
|
+
lines.extend(_format_field_line(key, value))
|
|
253
|
+
elif core_fields:
|
|
254
|
+
lines.append("Core Fields:")
|
|
255
|
+
for key, value in list(core_fields.items())[:12]:
|
|
256
|
+
lines.extend(_format_field_line(key, value))
|
|
257
|
+
if editable_fields:
|
|
258
|
+
lines.append("Editable Fields:")
|
|
259
|
+
for item in editable_fields[:10]:
|
|
185
260
|
if isinstance(item, dict):
|
|
186
261
|
lines.append(f"- {item.get('title') or '-'} ({item.get('kind') or 'field'})")
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
associated_reports = extras.get("associated_reports") if isinstance(extras.get("associated_reports"), dict) else {}
|
|
263
|
+
rollback_candidates = extras.get("rollback_candidates") if isinstance(extras.get("rollback_candidates"), dict) else {}
|
|
264
|
+
transfer_candidates = extras.get("transfer_candidates") if isinstance(extras.get("transfer_candidates"), dict) else {}
|
|
265
|
+
lines.append(
|
|
266
|
+
"Extras: "
|
|
267
|
+
f"reports={associated_reports.get('count', 0)}, "
|
|
268
|
+
f"rollback={rollback_candidates.get('count', 0)}, "
|
|
269
|
+
f"transfer={transfer_candidates.get('count', 0)}"
|
|
270
|
+
)
|
|
271
|
+
transfer_items = transfer_candidates.get("items") if isinstance(transfer_candidates.get("items"), list) else []
|
|
272
|
+
if transfer_items:
|
|
273
|
+
lines.append("Transfer Candidates:")
|
|
274
|
+
for item in transfer_items:
|
|
196
275
|
if isinstance(item, dict):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
lines.append(f"- {item}")
|
|
276
|
+
display = item.get("name") or item.get("uid") or item
|
|
277
|
+
suffix = f" <{item.get('email')}>" if item.get("email") else ""
|
|
278
|
+
lines.append(f"- {display}{suffix} (uid={item.get('uid') or '-'})")
|
|
279
|
+
_append_warnings(lines, result.get("warnings"))
|
|
280
|
+
return "\n".join(lines) + "\n"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _format_task_action(result: dict[str, Any]) -> str:
|
|
284
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
285
|
+
action = str(data.get("action") or "").strip().lower()
|
|
286
|
+
status = str(result.get("status") or "").strip().lower()
|
|
287
|
+
|
|
288
|
+
if status == "failed" or result.get("ok") is False:
|
|
289
|
+
lines = [_task_action_failure_label(action)]
|
|
290
|
+
reason = _task_action_failure_reason(result)
|
|
291
|
+
if reason:
|
|
292
|
+
lines.append(f"原因:{reason}")
|
|
293
|
+
debug_lines = _task_action_debug_lines(result)
|
|
294
|
+
if debug_lines:
|
|
295
|
+
lines.append("调试信息:")
|
|
296
|
+
lines.extend(f"- {line}" for line in debug_lines)
|
|
297
|
+
return "\n".join(lines) + "\n"
|
|
298
|
+
|
|
299
|
+
if status == "partial_success":
|
|
300
|
+
lines = [_task_action_success_label(action)]
|
|
301
|
+
lines.append(f"说明:{_task_action_partial_success_message(result)}")
|
|
302
|
+
return "\n".join(lines) + "\n"
|
|
303
|
+
|
|
304
|
+
return _task_action_success_label(action) + "\n"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _format_task_workbench(result: dict[str, Any]) -> str:
|
|
308
|
+
message = str(result.get("message") or "").strip()
|
|
309
|
+
if message:
|
|
310
|
+
return message + "\n"
|
|
311
|
+
return ""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _format_field_line(key: Any, value: Any) -> list[str]:
|
|
315
|
+
if isinstance(value, list):
|
|
316
|
+
text = " / ".join(str(item) for item in value if item not in (None, ""))
|
|
317
|
+
else:
|
|
318
|
+
text = str(value if value not in (None, "") else "-")
|
|
319
|
+
wrapped = textwrap.wrap(
|
|
320
|
+
text,
|
|
321
|
+
width=120,
|
|
322
|
+
initial_indent=f"- {key}: ",
|
|
323
|
+
subsequent_indent=" ",
|
|
324
|
+
replace_whitespace=False,
|
|
325
|
+
drop_whitespace=False,
|
|
326
|
+
break_long_words=True,
|
|
327
|
+
)
|
|
328
|
+
return wrapped or [f"- {key}: -"]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
332
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
333
|
+
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
334
|
+
result_type = str(data.get("result_type") or "-")
|
|
335
|
+
context = data.get("context") if isinstance(data.get("context"), dict) else {}
|
|
336
|
+
lines = []
|
|
337
|
+
if selection.get("task_id") not in (None, ""):
|
|
338
|
+
lines.append(f"Task ID: {selection.get('task_id')}")
|
|
339
|
+
lines.extend(
|
|
340
|
+
[
|
|
341
|
+
f"Report: {selection.get('chart_name') or '-'} ({selection.get('report_id') or '-'})",
|
|
342
|
+
f"Type: {result_type}",
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
if result_type == "view_list":
|
|
346
|
+
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
347
|
+
items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
|
|
348
|
+
lines.append(f"Returned Records: {len(items)}")
|
|
349
|
+
for item in items[:10]:
|
|
350
|
+
if isinstance(item, dict):
|
|
351
|
+
lines.append(json.dumps(item, ensure_ascii=False))
|
|
352
|
+
if len(items) > 10:
|
|
353
|
+
lines.append(f"... {len(items) - 10} more")
|
|
354
|
+
elif result_type == "chart_data":
|
|
355
|
+
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
356
|
+
summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
|
|
357
|
+
rows = result_payload.get("rows") if isinstance(result_payload.get("rows"), list) else []
|
|
358
|
+
if summary:
|
|
359
|
+
lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
|
|
360
|
+
lines.append(f"Rows: {len(rows)}")
|
|
361
|
+
for row in rows[:10]:
|
|
362
|
+
if isinstance(row, dict):
|
|
363
|
+
lines.append(json.dumps(row, ensure_ascii=False))
|
|
364
|
+
if len(rows) > 10:
|
|
365
|
+
lines.append(f"... {len(rows) - 10} more")
|
|
366
|
+
resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
|
|
367
|
+
if resolved_filters:
|
|
368
|
+
lines.append(f"Resolved Filters: {len(resolved_filters)}")
|
|
200
369
|
_append_warnings(lines, result.get("warnings"))
|
|
201
370
|
return "\n".join(lines) + "\n"
|
|
202
371
|
|
|
@@ -206,17 +375,22 @@ def _format_import_verify(result: dict[str, Any]) -> str:
|
|
|
206
375
|
f"App Key: {result.get('app_key') or '-'}",
|
|
207
376
|
f"File: {result.get('file_name') or result.get('file_path') or '-'}",
|
|
208
377
|
f"Can Import: {result.get('can_import')}",
|
|
209
|
-
f"Apply Rows: {result.get('apply_rows')}",
|
|
210
378
|
f"Verification ID: {result.get('verification_id') or '-'}",
|
|
211
379
|
]
|
|
212
|
-
|
|
213
|
-
if
|
|
214
|
-
lines.append(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
380
|
+
issue_summary = result.get("issue_summary") if isinstance(result.get("issue_summary"), dict) else {}
|
|
381
|
+
if issue_summary:
|
|
382
|
+
lines.append(
|
|
383
|
+
"Issues: "
|
|
384
|
+
f"total={issue_summary.get('total', 0)}, "
|
|
385
|
+
f"errors={issue_summary.get('errors', 0)}, "
|
|
386
|
+
f"warnings={issue_summary.get('warnings', 0)}"
|
|
387
|
+
)
|
|
388
|
+
sample = issue_summary.get("sample") if isinstance(issue_summary.get("sample"), list) else []
|
|
389
|
+
if sample:
|
|
390
|
+
lines.append("Issue Samples:")
|
|
391
|
+
for item in sample:
|
|
392
|
+
if isinstance(item, dict):
|
|
393
|
+
lines.append(f"- {item.get('code') or 'ISSUE'}: {item.get('message') or ''}".rstrip())
|
|
220
394
|
_append_warnings(lines, result.get("warnings"))
|
|
221
395
|
_append_verification(lines, result.get("verification"))
|
|
222
396
|
return "\n".join(lines) + "\n"
|
|
@@ -227,15 +401,91 @@ def _format_import_status(result: dict[str, Any]) -> str:
|
|
|
227
401
|
f"Status: {result.get('status') or '-'}",
|
|
228
402
|
f"Import ID: {result.get('import_id') or '-'}",
|
|
229
403
|
f"Process ID: {result.get('process_id_str') or '-'}",
|
|
230
|
-
f"
|
|
231
|
-
f"
|
|
404
|
+
f"Total Rows: {result.get('total') or 0}",
|
|
405
|
+
f"Finished Rows: {result.get('finished') or 0}",
|
|
406
|
+
f"Succeeded Rows: {result.get('succeeded') or 0}",
|
|
407
|
+
f"Failed Rows: {result.get('failed') or 0}",
|
|
232
408
|
f"Progress: {result.get('progress') or '-'}",
|
|
233
409
|
]
|
|
410
|
+
if result.get("process_status") not in (None, ""):
|
|
411
|
+
lines.append(f"Process Status: {result.get('process_status')}")
|
|
412
|
+
error_file_urls = result.get("error_file_urls") if isinstance(result.get("error_file_urls"), list) else []
|
|
413
|
+
if error_file_urls:
|
|
414
|
+
lines.append("Error Files:")
|
|
415
|
+
for url in error_file_urls:
|
|
416
|
+
lines.append(f"- {url}")
|
|
234
417
|
_append_warnings(lines, result.get("warnings"))
|
|
235
418
|
_append_verification(lines, result.get("verification"))
|
|
236
419
|
return "\n".join(lines) + "\n"
|
|
237
420
|
|
|
238
421
|
|
|
422
|
+
def _format_export_common(result: dict[str, Any], *, title: str | None = None) -> str:
|
|
423
|
+
lines: list[str] = []
|
|
424
|
+
if title:
|
|
425
|
+
lines.append(title)
|
|
426
|
+
lines.extend(
|
|
427
|
+
[
|
|
428
|
+
f"Status: {result.get('status') or '-'}",
|
|
429
|
+
f"Export Handle: {result.get('export_handle') or '-'}",
|
|
430
|
+
f"App Key: {result.get('app_key') or '-'}",
|
|
431
|
+
f"View ID: {result.get('view_id') or '-'}",
|
|
432
|
+
f"Process Status: {result.get('process_status') or '-'}",
|
|
433
|
+
f"Rows: {result.get('num') if result.get('num') is not None else '-'}",
|
|
434
|
+
]
|
|
435
|
+
)
|
|
436
|
+
row_scope = result.get("row_scope")
|
|
437
|
+
if row_scope not in (None, ""):
|
|
438
|
+
lines.append(f"Row Scope: {row_scope}")
|
|
439
|
+
selected_record_count = result.get("selected_record_count")
|
|
440
|
+
if selected_record_count not in (None, ""):
|
|
441
|
+
lines.append(f"Selected Rows: {selected_record_count}")
|
|
442
|
+
field_scope = result.get("field_scope")
|
|
443
|
+
if field_scope not in (None, ""):
|
|
444
|
+
lines.append(f"Field Scope: {field_scope}")
|
|
445
|
+
selected_field_count = result.get("selected_field_count")
|
|
446
|
+
if selected_field_count not in (None, ""):
|
|
447
|
+
lines.append(f"Selected Fields: {selected_field_count}")
|
|
448
|
+
include_workflow_log = result.get("include_workflow_log")
|
|
449
|
+
if include_workflow_log not in (None, ""):
|
|
450
|
+
lines.append(f"Include Workflow Log: {include_workflow_log}")
|
|
451
|
+
file_names = result.get("file_names") if isinstance(result.get("file_names"), list) else []
|
|
452
|
+
file_urls = result.get("file_urls") if isinstance(result.get("file_urls"), list) else []
|
|
453
|
+
if file_names or file_urls:
|
|
454
|
+
lines.append("Remote Files:")
|
|
455
|
+
max_items = max(len(file_names), len(file_urls))
|
|
456
|
+
for index in range(max_items):
|
|
457
|
+
name = file_names[index] if index < len(file_names) else "-"
|
|
458
|
+
url = file_urls[index] if index < len(file_urls) else "-"
|
|
459
|
+
lines.append(f"- {name}: {url}")
|
|
460
|
+
downloaded_files = result.get("downloaded_files") if isinstance(result.get("downloaded_files"), list) else []
|
|
461
|
+
if downloaded_files:
|
|
462
|
+
lines.append("Downloaded Files:")
|
|
463
|
+
for item in downloaded_files:
|
|
464
|
+
if isinstance(item, dict):
|
|
465
|
+
lines.append(f"- {item.get('file_name') or '-'}: {item.get('path') or '-'}")
|
|
466
|
+
else:
|
|
467
|
+
lines.append(f"- {item}")
|
|
468
|
+
_append_warnings(lines, result.get("warnings"))
|
|
469
|
+
_append_verification(lines, result.get("verification"))
|
|
470
|
+
return "\n".join(lines) + "\n"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _format_export_start(result: dict[str, Any]) -> str:
|
|
474
|
+
return _format_export_common(result, title="Export Accepted")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _format_export_status(result: dict[str, Any]) -> str:
|
|
478
|
+
return _format_export_common(result, title="Export Status")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _format_export_get(result: dict[str, Any]) -> str:
|
|
482
|
+
return _format_export_common(result, title="Export Result")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _format_export_direct(result: dict[str, Any]) -> str:
|
|
486
|
+
return _format_export_common(result, title="Export Direct")
|
|
487
|
+
|
|
488
|
+
|
|
239
489
|
def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
240
490
|
lines = []
|
|
241
491
|
if "status" in result:
|
|
@@ -319,16 +569,140 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
|
|
|
319
569
|
return None
|
|
320
570
|
|
|
321
571
|
|
|
572
|
+
def _task_action_success_label(action: str) -> str:
|
|
573
|
+
return {
|
|
574
|
+
"approve": "已通过",
|
|
575
|
+
"reject": "已驳回",
|
|
576
|
+
"rollback": "已退回",
|
|
577
|
+
"transfer": "已转交",
|
|
578
|
+
"save_only": "已保存",
|
|
579
|
+
"urge": "已催办",
|
|
580
|
+
}.get(action, "已执行")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _task_action_failure_label(action: str) -> str:
|
|
584
|
+
return {
|
|
585
|
+
"approve": "审批失败",
|
|
586
|
+
"reject": "驳回失败",
|
|
587
|
+
"rollback": "退回失败",
|
|
588
|
+
"transfer": "转交失败",
|
|
589
|
+
"save_only": "保存失败",
|
|
590
|
+
"urge": "催办失败",
|
|
591
|
+
}.get(action, "执行失败")
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _task_action_partial_success_message(result: dict[str, Any]) -> str:
|
|
595
|
+
error_code = str(result.get("error_code") or "").strip().upper()
|
|
596
|
+
if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
|
|
597
|
+
return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
|
|
598
|
+
if error_code == "TASK_ALREADY_PROCESSED":
|
|
599
|
+
return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
|
|
600
|
+
warnings = result.get("warnings")
|
|
601
|
+
if isinstance(warnings, list):
|
|
602
|
+
for warning in warnings:
|
|
603
|
+
if not isinstance(warning, dict):
|
|
604
|
+
continue
|
|
605
|
+
code = str(warning.get("code") or "").strip().upper()
|
|
606
|
+
if code == "TASK_ALREADY_PROCESSED_UNCONFIRMED_ACTOR":
|
|
607
|
+
return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
|
|
608
|
+
if code == "WORKFLOW_CONTINUATION_UNVERIFIED":
|
|
609
|
+
return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
|
|
610
|
+
return "动作已提交,但结果验证不完整。可使用 --json 查看详细信息。"
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _task_action_failure_reason(result: dict[str, Any]) -> str | None:
|
|
614
|
+
error_code = str(result.get("error_code") or "").strip().upper()
|
|
615
|
+
mapped_error = {
|
|
616
|
+
"TASK_CONTEXT_VISIBILITY_UNVERIFIED": "当前待办已不可操作,且系统未能确认是否已被处理。",
|
|
617
|
+
"TASK_SAVE_ONLY_VERIFICATION_FAILED": "保存请求已发送,但未能确认字段是否全部保存成功。",
|
|
618
|
+
"WORKFLOW_CONTINUATION_UNVERIFIED": "动作已提交,但暂未验证到流程继续推进。",
|
|
619
|
+
"TASK_ALREADY_PROCESSED": "当前待办已不可操作,系统判断流程可能已被其他人处理。",
|
|
620
|
+
}.get(error_code)
|
|
621
|
+
if mapped_error:
|
|
622
|
+
return mapped_error
|
|
623
|
+
|
|
624
|
+
warnings = result.get("warnings")
|
|
625
|
+
if isinstance(warnings, list):
|
|
626
|
+
for warning in warnings:
|
|
627
|
+
if isinstance(warning, dict) and warning.get("message"):
|
|
628
|
+
return str(warning.get("message"))
|
|
629
|
+
|
|
630
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
631
|
+
transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
|
|
632
|
+
backend_code = transport_error.get("backend_code")
|
|
633
|
+
http_status = transport_error.get("http_status")
|
|
634
|
+
if backend_code not in (None, ""):
|
|
635
|
+
return f"后端返回错误码 {backend_code}。"
|
|
636
|
+
if http_status not in (None, ""):
|
|
637
|
+
return f"请求返回 HTTP {http_status}。"
|
|
638
|
+
|
|
639
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
640
|
+
record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
|
|
641
|
+
backend_code = record_state_error.get("backend_code")
|
|
642
|
+
http_status = record_state_error.get("http_status")
|
|
643
|
+
if backend_code not in (None, ""):
|
|
644
|
+
return f"后端返回错误码 {backend_code}。"
|
|
645
|
+
if http_status not in (None, ""):
|
|
646
|
+
return f"请求返回 HTTP {http_status}。"
|
|
647
|
+
if error_code:
|
|
648
|
+
return f"错误码:{error_code}"
|
|
649
|
+
return None
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
|
|
653
|
+
lines: list[str] = []
|
|
654
|
+
error_code = result.get("error_code")
|
|
655
|
+
if error_code not in (None, ""):
|
|
656
|
+
lines.append(f"error_code: {error_code}")
|
|
657
|
+
|
|
658
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
659
|
+
transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
|
|
660
|
+
for key in ("backend_code", "http_status", "category"):
|
|
661
|
+
value = transport_error.get(key)
|
|
662
|
+
if value not in (None, ""):
|
|
663
|
+
lines.append(f"{key}: {value}")
|
|
664
|
+
|
|
665
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
666
|
+
for key in (
|
|
667
|
+
"runtime_continuation_verified",
|
|
668
|
+
"task_context_visibility_verified",
|
|
669
|
+
"fields_saved_verified",
|
|
670
|
+
"task_still_actionable",
|
|
671
|
+
"workflow_not_advanced",
|
|
672
|
+
"record_state_readable",
|
|
673
|
+
):
|
|
674
|
+
if key in verification and verification.get(key) is not None:
|
|
675
|
+
lines.append(f"{key}: {verification.get(key)}")
|
|
676
|
+
|
|
677
|
+
record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
|
|
678
|
+
for key in ("backend_code", "http_status", "category"):
|
|
679
|
+
value = record_state_error.get(key)
|
|
680
|
+
if value not in (None, ""):
|
|
681
|
+
entry = f"record_state_{key}: {value}"
|
|
682
|
+
if entry not in lines:
|
|
683
|
+
lines.append(entry)
|
|
684
|
+
return lines
|
|
685
|
+
|
|
686
|
+
|
|
322
687
|
_FORMATTERS = {
|
|
323
688
|
"auth_whoami": _format_whoami,
|
|
324
689
|
"workspace_list": _format_workspace_list,
|
|
690
|
+
"workspace_get": _format_workspace_get,
|
|
691
|
+
"workspace_select": _format_workspace_select,
|
|
325
692
|
"app_list": _format_app_items,
|
|
326
693
|
"app_search": _format_app_items,
|
|
327
694
|
"app_get": _format_app_get,
|
|
328
695
|
"record_list": _format_record_list,
|
|
329
696
|
"task_list": _format_task_list,
|
|
697
|
+
"task_workbench": _format_task_workbench,
|
|
330
698
|
"task_get": _format_task_get,
|
|
699
|
+
"task_action_execute": _format_task_action,
|
|
700
|
+
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
331
701
|
"import_verify": _format_import_verify,
|
|
332
702
|
"import_status": _format_import_status,
|
|
703
|
+
"export_start": _format_export_start,
|
|
704
|
+
"export_status": _format_export_status,
|
|
705
|
+
"export_get": _format_export_get,
|
|
706
|
+
"export_direct": _format_export_direct,
|
|
333
707
|
"builder_summary": _format_builder_summary,
|
|
334
708
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from .terminal_ui import SelectionOption, select_option
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class InteractiveSelectionResult(Generic[T]):
|
|
14
|
+
status: str
|
|
15
|
+
value: T | None = None
|
|
16
|
+
message: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cancelled_result(message: str = "已取消") -> dict[str, str]:
|
|
20
|
+
return {"status": "cancelled", "message": message}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_interactive_selection(
|
|
24
|
+
args: object,
|
|
25
|
+
*,
|
|
26
|
+
title: str,
|
|
27
|
+
unavailable_message: str,
|
|
28
|
+
empty_message: str,
|
|
29
|
+
load_options: Callable[[], list[SelectionOption[T]]],
|
|
30
|
+
) -> InteractiveSelectionResult[T]:
|
|
31
|
+
input_stream = getattr(args, "_stdin", None)
|
|
32
|
+
output_stream = getattr(args, "_stderr_stream", None)
|
|
33
|
+
if input_stream is None or output_stream is None:
|
|
34
|
+
return InteractiveSelectionResult(status="unavailable", message=unavailable_message)
|
|
35
|
+
if not bool(getattr(input_stream, "isatty", lambda: False)()):
|
|
36
|
+
return InteractiveSelectionResult(status="unavailable", message=unavailable_message)
|
|
37
|
+
|
|
38
|
+
options = list(load_options())
|
|
39
|
+
if not options:
|
|
40
|
+
return InteractiveSelectionResult(status="empty", message=empty_message)
|
|
41
|
+
|
|
42
|
+
value = select_option(
|
|
43
|
+
title=title,
|
|
44
|
+
options=options,
|
|
45
|
+
input_stream=input_stream,
|
|
46
|
+
output_stream=output_stream,
|
|
47
|
+
)
|
|
48
|
+
if value is None:
|
|
49
|
+
return InteractiveSelectionResult(status="cancelled", message="已取消")
|
|
50
|
+
return InteractiveSelectionResult(status="selected", value=value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_interactive_text_input(
|
|
54
|
+
args: object,
|
|
55
|
+
*,
|
|
56
|
+
prompt: str,
|
|
57
|
+
unavailable_message: str,
|
|
58
|
+
) -> InteractiveSelectionResult[str]:
|
|
59
|
+
input_stream = getattr(args, "_stdin", None)
|
|
60
|
+
output_stream = getattr(args, "_stderr_stream", None)
|
|
61
|
+
if input_stream is None or output_stream is None:
|
|
62
|
+
return InteractiveSelectionResult(status="unavailable", message=unavailable_message)
|
|
63
|
+
if not bool(getattr(input_stream, "isatty", lambda: False)()):
|
|
64
|
+
return InteractiveSelectionResult(status="unavailable", message=unavailable_message)
|
|
65
|
+
|
|
66
|
+
output_stream.write(prompt)
|
|
67
|
+
output_stream.flush()
|
|
68
|
+
line = input_stream.readline()
|
|
69
|
+
text = str(line or "").strip()
|
|
70
|
+
if not text:
|
|
71
|
+
return InteractiveSelectionResult(status="cancelled", message="已取消")
|
|
72
|
+
return InteractiveSelectionResult(status="selected", value=text)
|