@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1006 → 0.2.0-beta.1008
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/cli/commands/task.py +53 -66
- package/src/qingflow_mcp/cli/formatters.py +27 -2
- package/src/qingflow_mcp/cli/terminal_ui.py +28 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +26 -8
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1008
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1008 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -212,25 +212,15 @@ def _run_task_workbench_task_loop(args: argparse.Namespace, context: CliContext)
|
|
|
212
212
|
detail_selection = _show_task_workbench_detail(args, task_context)
|
|
213
213
|
if detail_selection.status == "cancelled":
|
|
214
214
|
return "back", ""
|
|
215
|
-
|
|
215
|
+
selected_value = str(detail_selection.value or "").strip()
|
|
216
|
+
if selected_value == "exit":
|
|
216
217
|
return "result", cancelled_result("已退出")
|
|
217
|
-
if
|
|
218
|
+
if selected_value == "back":
|
|
218
219
|
return "back", ""
|
|
219
|
-
|
|
220
|
-
action_selection = _choose_task_action_interactively(
|
|
221
|
-
args,
|
|
222
|
-
task_context,
|
|
223
|
-
fields={},
|
|
224
|
-
title=_build_task_action_title(task_context),
|
|
225
|
-
)
|
|
226
|
-
if action_selection.status == "cancelled":
|
|
227
|
-
continue
|
|
228
|
-
if action_selection.status == "empty":
|
|
220
|
+
if not selected_value:
|
|
229
221
|
continue
|
|
230
|
-
if action_selection.status != "selected":
|
|
231
|
-
return "result", cancelled_result("已退出")
|
|
232
222
|
|
|
233
|
-
args.action =
|
|
223
|
+
args.action = selected_value
|
|
234
224
|
payload_selection = _resolve_action_payload_or_select(args, task_context, payload={})
|
|
235
225
|
if _is_cancelled_result(payload_selection):
|
|
236
226
|
continue
|
|
@@ -421,35 +411,8 @@ def _choose_task_action_interactively(
|
|
|
421
411
|
fields: dict[str, Any],
|
|
422
412
|
title: str = "选择操作",
|
|
423
413
|
):
|
|
424
|
-
data = task_context.get("data") if isinstance(task_context.get("data"), dict) else {}
|
|
425
|
-
available_actions = [str(item).strip() for item in (data.get("available_actions") or []) if str(item).strip()]
|
|
426
|
-
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
427
|
-
action_metadata = data.get("action_metadata") if isinstance(data.get("action_metadata"), dict) else {}
|
|
428
|
-
feedback_required = {
|
|
429
|
-
str(item).strip()
|
|
430
|
-
for item in (action_metadata.get("feedback_required_for") or [])
|
|
431
|
-
if str(item).strip()
|
|
432
|
-
}
|
|
433
|
-
rollback_count = _count_candidate_items(extras.get("rollback_candidates"))
|
|
434
|
-
transfer_count = _count_candidate_items(extras.get("transfer_candidates"))
|
|
435
|
-
|
|
436
414
|
def load_options() -> list[SelectionOption[str]]:
|
|
437
|
-
|
|
438
|
-
for action in available_actions:
|
|
439
|
-
if action == "save_only" and not fields:
|
|
440
|
-
continue
|
|
441
|
-
label = TASK_ACTION_LABELS.get(action, action)
|
|
442
|
-
hint_parts = [action]
|
|
443
|
-
if action in feedback_required:
|
|
444
|
-
hint_parts.append("需要理由")
|
|
445
|
-
if action == "rollback":
|
|
446
|
-
hint_parts.append(f"可退回节点 {rollback_count}")
|
|
447
|
-
if action == "transfer":
|
|
448
|
-
hint_parts.append(f"可转交成员 {transfer_count}")
|
|
449
|
-
if action == "save_only":
|
|
450
|
-
hint_parts.append("仅保存字段,不推进流程")
|
|
451
|
-
options.append(SelectionOption(value=action, label=label, hint=" · ".join(hint_parts)))
|
|
452
|
-
return options
|
|
415
|
+
return _task_interactive_action_options(task_context, fields=fields)
|
|
453
416
|
|
|
454
417
|
return resolve_interactive_selection(
|
|
455
418
|
args,
|
|
@@ -461,14 +424,11 @@ def _choose_task_action_interactively(
|
|
|
461
424
|
|
|
462
425
|
|
|
463
426
|
def _show_task_workbench_detail(args: argparse.Namespace, task_context: dict[str, Any]):
|
|
464
|
-
has_actions = _task_has_interactive_actions(task_context)
|
|
465
|
-
|
|
466
427
|
def load_options() -> list[SelectionOption[str]]:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
]
|
|
428
|
+
options = _task_interactive_action_options(task_context, fields={})
|
|
429
|
+
if options:
|
|
430
|
+
options.append(SelectionOption(value="back", label="返回列表", hint="回到待办列表"))
|
|
431
|
+
return options
|
|
472
432
|
return [
|
|
473
433
|
SelectionOption(value="back", label="返回列表", hint="当前节点没有可执行动作"),
|
|
474
434
|
SelectionOption(value="exit", label="退出工作台", hint="结束当前待办工作台"),
|
|
@@ -667,7 +627,9 @@ def _build_task_detail_title(task_context: dict[str, Any]) -> str:
|
|
|
667
627
|
editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
|
|
668
628
|
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
669
629
|
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
630
|
+
all_fields = record_summary.get("all_fields") if isinstance(record_summary.get("all_fields"), dict) else {}
|
|
670
631
|
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
632
|
+
field_map = all_fields or core_fields
|
|
671
633
|
action_labels = ", ".join(TASK_ACTION_LABELS.get(str(item), str(item)) for item in available_actions if str(item).strip()) or "-"
|
|
672
634
|
lines = [
|
|
673
635
|
"待办详情",
|
|
@@ -676,6 +638,8 @@ def _build_task_detail_title(task_context: dict[str, Any]) -> str:
|
|
|
676
638
|
f"节点: {task.get('workflow_node_name') or '-'}",
|
|
677
639
|
f"发起人: {_task_initiator_label(initiator)}",
|
|
678
640
|
f"状态: {record_summary.get('apply_status') or '-'}",
|
|
641
|
+
f"申请编号: {record_summary.get('custom_apply_num') or record_summary.get('apply_num') or '-'}",
|
|
642
|
+
f"提交时间: {record_summary.get('apply_time') or '-'}",
|
|
679
643
|
f"可执行动作: {action_labels}",
|
|
680
644
|
f"可编辑字段: {len(editable_fields)}",
|
|
681
645
|
(
|
|
@@ -685,18 +649,14 @@ def _build_task_detail_title(task_context: dict[str, Any]) -> str:
|
|
|
685
649
|
f"转交 {_count_candidate_items(extras.get('transfer_candidates'))}"
|
|
686
650
|
),
|
|
687
651
|
]
|
|
688
|
-
if
|
|
652
|
+
if field_map:
|
|
689
653
|
lines.append("")
|
|
690
|
-
lines.append("
|
|
691
|
-
for key, value in
|
|
692
|
-
lines.append(f"- {key}: {
|
|
654
|
+
lines.append(f"字段值({len(field_map)}):")
|
|
655
|
+
for key, value in field_map.items():
|
|
656
|
+
lines.append(f"- {key}: {_full_display(value)}")
|
|
693
657
|
return "\n".join(lines)
|
|
694
658
|
|
|
695
659
|
|
|
696
|
-
def _build_task_action_title(task_context: dict[str, Any]) -> str:
|
|
697
|
-
return _build_task_detail_title(task_context) + "\n\n选择操作"
|
|
698
|
-
|
|
699
|
-
|
|
700
660
|
def _task_initiator_label(initiator: dict[str, Any]) -> str:
|
|
701
661
|
for key in ("display_name", "displayName", "name", "email", "uid"):
|
|
702
662
|
value = initiator.get(key)
|
|
@@ -705,17 +665,44 @@ def _task_initiator_label(initiator: dict[str, Any]) -> str:
|
|
|
705
665
|
return "-"
|
|
706
666
|
|
|
707
667
|
|
|
708
|
-
def
|
|
668
|
+
def _task_interactive_action_options(task_context: dict[str, Any], *, fields: dict[str, Any]) -> list[SelectionOption[str]]:
|
|
709
669
|
data = task_context.get("data") if isinstance(task_context.get("data"), dict) else {}
|
|
710
670
|
available_actions = [str(item).strip() for item in (data.get("available_actions") or []) if str(item).strip()]
|
|
711
|
-
|
|
712
|
-
|
|
671
|
+
action_metadata = data.get("action_metadata") if isinstance(data.get("action_metadata"), dict) else {}
|
|
672
|
+
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
673
|
+
feedback_required = {
|
|
674
|
+
str(item).strip()
|
|
675
|
+
for item in (action_metadata.get("feedback_required_for") or [])
|
|
676
|
+
if str(item).strip()
|
|
677
|
+
}
|
|
678
|
+
rollback_count = _count_candidate_items(extras.get("rollback_candidates"))
|
|
679
|
+
transfer_count = _count_candidate_items(extras.get("transfer_candidates"))
|
|
713
680
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
681
|
+
options: list[SelectionOption[str]] = []
|
|
682
|
+
for action in available_actions:
|
|
683
|
+
if action == "save_only" and not fields:
|
|
684
|
+
continue
|
|
685
|
+
label = TASK_ACTION_LABELS.get(action, action)
|
|
686
|
+
hint_parts = [action]
|
|
687
|
+
if action in feedback_required:
|
|
688
|
+
hint_parts.append("需要理由")
|
|
689
|
+
if action == "rollback":
|
|
690
|
+
hint_parts.append(f"可退回节点 {rollback_count}")
|
|
691
|
+
if action == "transfer":
|
|
692
|
+
hint_parts.append(f"可转交成员 {transfer_count}")
|
|
693
|
+
if action == "save_only":
|
|
694
|
+
hint_parts.append("仅保存字段,不推进流程")
|
|
695
|
+
options.append(SelectionOption(value=action, label=label, hint=" · ".join(hint_parts)))
|
|
696
|
+
return options
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _full_display(value: Any) -> str:
|
|
700
|
+
if value in (None, ""):
|
|
701
|
+
return "-"
|
|
702
|
+
if isinstance(value, list):
|
|
703
|
+
normalized = [str(item) for item in value if item not in (None, "")]
|
|
704
|
+
return " / ".join(normalized) if normalized else "-"
|
|
705
|
+
return str(value)
|
|
719
706
|
|
|
720
707
|
|
|
721
708
|
def _has_interactive_terminal(args: argparse.Namespace) -> bool:
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
|
|
@@ -237,15 +238,22 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
237
238
|
f"App: {task.get('app_name') or '-'}",
|
|
238
239
|
f"Initiator: {initiator_label}",
|
|
239
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 '-'}",
|
|
240
243
|
f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
|
|
241
244
|
f"Editable Fields: {len(editable_fields)}",
|
|
242
245
|
]
|
|
243
246
|
)
|
|
247
|
+
all_fields = record_summary.get("all_fields") if isinstance(record_summary.get("all_fields"), dict) else {}
|
|
244
248
|
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
245
|
-
if
|
|
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:
|
|
246
254
|
lines.append("Core Fields:")
|
|
247
255
|
for key, value in list(core_fields.items())[:12]:
|
|
248
|
-
lines.
|
|
256
|
+
lines.extend(_format_field_line(key, value))
|
|
249
257
|
if editable_fields:
|
|
250
258
|
lines.append("Editable Fields:")
|
|
251
259
|
for item in editable_fields[:10]:
|
|
@@ -303,6 +311,23 @@ def _format_task_workbench(result: dict[str, Any]) -> str:
|
|
|
303
311
|
return ""
|
|
304
312
|
|
|
305
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
|
+
|
|
306
331
|
def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
307
332
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
308
333
|
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import select
|
|
5
5
|
import shutil
|
|
6
|
+
import textwrap
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from typing import Generic, Sequence, TextIO, TypeVar
|
|
8
9
|
|
|
@@ -165,7 +166,33 @@ def _truncate_line(text: str, *, width: int) -> str:
|
|
|
165
166
|
|
|
166
167
|
def _render_multiline_text(text: str, *, width: int) -> list[str]:
|
|
167
168
|
parts = text.splitlines() or [text]
|
|
168
|
-
|
|
169
|
+
rendered: list[str] = []
|
|
170
|
+
wrap_width = max(1, width)
|
|
171
|
+
for part in parts:
|
|
172
|
+
if not part:
|
|
173
|
+
rendered.append("")
|
|
174
|
+
continue
|
|
175
|
+
initial_indent = ""
|
|
176
|
+
subsequent_indent = ""
|
|
177
|
+
stripped = part.lstrip()
|
|
178
|
+
if stripped.startswith("- "):
|
|
179
|
+
leading_spaces = len(part) - len(stripped)
|
|
180
|
+
initial_indent = part[:leading_spaces] + "- "
|
|
181
|
+
subsequent_indent = part[:leading_spaces] + " "
|
|
182
|
+
content = stripped[2:]
|
|
183
|
+
else:
|
|
184
|
+
content = part
|
|
185
|
+
wrapped = textwrap.wrap(
|
|
186
|
+
content,
|
|
187
|
+
width=wrap_width,
|
|
188
|
+
initial_indent=initial_indent,
|
|
189
|
+
subsequent_indent=subsequent_indent,
|
|
190
|
+
replace_whitespace=False,
|
|
191
|
+
drop_whitespace=False,
|
|
192
|
+
break_long_words=True,
|
|
193
|
+
)
|
|
194
|
+
rendered.extend(wrapped or [""])
|
|
195
|
+
return rendered
|
|
169
196
|
|
|
170
197
|
|
|
171
198
|
def _read_key(input_stream: TextIO) -> str:
|
|
@@ -1762,6 +1762,7 @@ class TaskContextTools(ToolBase):
|
|
|
1762
1762
|
"apply_time": record.get("apply_time"),
|
|
1763
1763
|
"last_update_time": record.get("last_update_time"),
|
|
1764
1764
|
"core_fields": self._task_record_core_fields(record.get("answers") or []),
|
|
1765
|
+
"all_fields": self._task_record_all_fields(record.get("answers") or []),
|
|
1765
1766
|
},
|
|
1766
1767
|
"available_actions": available_actions,
|
|
1767
1768
|
"editable_fields": [
|
|
@@ -1921,9 +1922,21 @@ class TaskContextTools(ToolBase):
|
|
|
1921
1922
|
return None
|
|
1922
1923
|
|
|
1923
1924
|
def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
|
|
1925
|
+
return self._task_record_field_map(answers, limit=limit, truncate_text=160)
|
|
1926
|
+
|
|
1927
|
+
def _task_record_all_fields(self, answers: Any) -> dict[str, Any]:
|
|
1928
|
+
return self._task_record_field_map(answers, limit=None, truncate_text=None)
|
|
1929
|
+
|
|
1930
|
+
def _task_record_field_map(
|
|
1931
|
+
self,
|
|
1932
|
+
answers: Any,
|
|
1933
|
+
*,
|
|
1934
|
+
limit: int | None,
|
|
1935
|
+
truncate_text: int | None,
|
|
1936
|
+
) -> dict[str, Any]:
|
|
1924
1937
|
if not isinstance(answers, list):
|
|
1925
1938
|
return {}
|
|
1926
|
-
|
|
1939
|
+
field_map: dict[str, Any] = {}
|
|
1927
1940
|
for answer in answers:
|
|
1928
1941
|
if not isinstance(answer, dict):
|
|
1929
1942
|
continue
|
|
@@ -1943,19 +1956,24 @@ class TaskContextTools(ToolBase):
|
|
|
1943
1956
|
value = values[0] if len(values) == 1 else values
|
|
1944
1957
|
if value in (None, "", []):
|
|
1945
1958
|
continue
|
|
1946
|
-
|
|
1947
|
-
if len(
|
|
1959
|
+
field_map[str(title)] = self._compact_task_value(value, truncate_text=truncate_text)
|
|
1960
|
+
if limit is not None and len(field_map) >= limit:
|
|
1948
1961
|
break
|
|
1949
|
-
return
|
|
1962
|
+
return field_map
|
|
1950
1963
|
|
|
1951
|
-
def _compact_task_value(self, value: Any) -> Any:
|
|
1964
|
+
def _compact_task_value(self, value: Any, *, truncate_text: int | None = 160) -> Any:
|
|
1952
1965
|
if isinstance(value, list):
|
|
1953
|
-
|
|
1966
|
+
items = [self._compact_task_value(item, truncate_text=truncate_text) for item in value]
|
|
1967
|
+
if truncate_text is not None:
|
|
1968
|
+
return items[:8]
|
|
1969
|
+
return items
|
|
1954
1970
|
text = re.sub(r"<[^>]+>", " ", str(value))
|
|
1955
1971
|
text = re.sub(r"\s+", " ", text).strip()
|
|
1956
|
-
if len(text) <=
|
|
1972
|
+
if truncate_text is None or len(text) <= truncate_text:
|
|
1957
1973
|
return text
|
|
1958
|
-
|
|
1974
|
+
if truncate_text <= 3:
|
|
1975
|
+
return text[:truncate_text]
|
|
1976
|
+
return text[: truncate_text - 3].rstrip() + "..."
|
|
1959
1977
|
|
|
1960
1978
|
def _compact_task_editable_field(self, field: dict[str, Any], update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1961
1979
|
payload_template = update_schema.get("payload_template") if isinstance(update_schema.get("payload_template"), dict) else {}
|