@josephyan/qingflow-app-user-mcp 0.2.0-beta.1004 → 0.2.0-beta.1005
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
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.1005
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1005 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import sys
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from ..context import CliContext
|
|
@@ -18,10 +19,20 @@ TASK_ACTION_LABELS = {
|
|
|
18
19
|
"urge": "催办",
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
TASK_ACTION_SUCCESS_LABELS = {
|
|
23
|
+
"approve": "已通过",
|
|
24
|
+
"reject": "已驳回",
|
|
25
|
+
"rollback": "已退回",
|
|
26
|
+
"transfer": "已转交",
|
|
27
|
+
"save_only": "已保存",
|
|
28
|
+
"urge": "已催办",
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
|
|
22
32
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
23
33
|
parser = subparsers.add_parser("task", help="待办与流程上下文")
|
|
24
|
-
task_subparsers = parser.add_subparsers(dest="task_command", required=
|
|
34
|
+
task_subparsers = parser.add_subparsers(dest="task_command", required=False)
|
|
35
|
+
parser.set_defaults(handler=_handle_task_workbench, format_hint="task_workbench", _task_parser=parser)
|
|
25
36
|
|
|
26
37
|
list_parser = task_subparsers.add_parser("list", help="列出待办")
|
|
27
38
|
list_parser.add_argument("--task-box", default="todo")
|
|
@@ -90,6 +101,36 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
90
101
|
)
|
|
91
102
|
|
|
92
103
|
|
|
104
|
+
def _handle_task_workbench(args: argparse.Namespace, context: CliContext) -> dict:
|
|
105
|
+
if bool(args.json) or not _has_interactive_terminal(args):
|
|
106
|
+
_raise_task_subcommand_required(args)
|
|
107
|
+
|
|
108
|
+
banner: str | None = None
|
|
109
|
+
while True:
|
|
110
|
+
title = _build_task_workbench_list_title(banner)
|
|
111
|
+
selection = _choose_todo_task_interactively(args, context, tool_name="task", title=title)
|
|
112
|
+
banner = None
|
|
113
|
+
if selection.status == "empty":
|
|
114
|
+
return {"status": "success", "message": "当前没有待办"}
|
|
115
|
+
if selection.status == "cancelled":
|
|
116
|
+
return cancelled_result("已退出")
|
|
117
|
+
if selection.status != "selected":
|
|
118
|
+
_raise_task_subcommand_required(args)
|
|
119
|
+
|
|
120
|
+
args.task_id = str(selection.value or "")
|
|
121
|
+
args.app_key = ""
|
|
122
|
+
args.record_id = ""
|
|
123
|
+
args.workflow_node_id = 0
|
|
124
|
+
|
|
125
|
+
outcome, payload = _run_task_workbench_task_loop(args, context)
|
|
126
|
+
if outcome == "refresh":
|
|
127
|
+
banner = str(payload or "").strip() or None
|
|
128
|
+
continue
|
|
129
|
+
if outcome == "back":
|
|
130
|
+
continue
|
|
131
|
+
return payload if isinstance(payload, dict) else cancelled_result("已退出")
|
|
132
|
+
|
|
133
|
+
|
|
93
134
|
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
94
135
|
selection_result = _resolve_task_locator_or_select(args, context, tool_name="task get")
|
|
95
136
|
if isinstance(selection_result, dict):
|
|
@@ -165,6 +206,50 @@ def _handle_report(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
165
206
|
)
|
|
166
207
|
|
|
167
208
|
|
|
209
|
+
def _run_task_workbench_task_loop(args: argparse.Namespace, context: CliContext) -> tuple[str, dict | str]:
|
|
210
|
+
while True:
|
|
211
|
+
task_context = _load_task_workbench_context(args, context)
|
|
212
|
+
detail_selection = _show_task_workbench_detail(args, task_context)
|
|
213
|
+
if detail_selection.status == "cancelled":
|
|
214
|
+
return "back", ""
|
|
215
|
+
if str(detail_selection.value or "") == "exit":
|
|
216
|
+
return "result", cancelled_result("已退出")
|
|
217
|
+
if str(detail_selection.value or "") != "action":
|
|
218
|
+
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":
|
|
229
|
+
continue
|
|
230
|
+
if action_selection.status != "selected":
|
|
231
|
+
return "result", cancelled_result("已退出")
|
|
232
|
+
|
|
233
|
+
args.action = str(action_selection.value or "").strip()
|
|
234
|
+
payload_selection = _resolve_action_payload_or_select(args, task_context, payload={})
|
|
235
|
+
if _is_cancelled_result(payload_selection):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
result = context.task.task_action_execute(
|
|
239
|
+
profile=args.profile,
|
|
240
|
+
task_id=args.task_id,
|
|
241
|
+
app_key=args.app_key or "",
|
|
242
|
+
record_id=args.record_id or "",
|
|
243
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
244
|
+
action=args.action,
|
|
245
|
+
payload=payload_selection,
|
|
246
|
+
fields={},
|
|
247
|
+
)
|
|
248
|
+
if _is_success_result(result):
|
|
249
|
+
return "refresh", _task_action_success_label(str(args.action or "").strip().lower())
|
|
250
|
+
return "result", result
|
|
251
|
+
|
|
252
|
+
|
|
168
253
|
def _resolve_task_locator_or_select(args: argparse.Namespace, context: CliContext, *, tool_name: str) -> dict | None:
|
|
169
254
|
if (args.task_id or "").strip():
|
|
170
255
|
return None
|
|
@@ -200,7 +285,13 @@ def _resolve_task_locator_or_select(args: argparse.Namespace, context: CliContex
|
|
|
200
285
|
return None
|
|
201
286
|
|
|
202
287
|
|
|
203
|
-
def _choose_todo_task_interactively(
|
|
288
|
+
def _choose_todo_task_interactively(
|
|
289
|
+
args: argparse.Namespace,
|
|
290
|
+
context: CliContext,
|
|
291
|
+
*,
|
|
292
|
+
tool_name: str,
|
|
293
|
+
title: str = "选择待办",
|
|
294
|
+
):
|
|
204
295
|
def load_options() -> list[SelectionOption[str]]:
|
|
205
296
|
result = context.task.task_list(
|
|
206
297
|
profile=args.profile,
|
|
@@ -244,7 +335,7 @@ def _choose_todo_task_interactively(args: argparse.Namespace, context: CliContex
|
|
|
244
335
|
|
|
245
336
|
return resolve_interactive_selection(
|
|
246
337
|
args,
|
|
247
|
-
title=
|
|
338
|
+
title=title,
|
|
248
339
|
unavailable_message=(
|
|
249
340
|
f"{tool_name} requires --task-id, or --app-key together with --record-id and --workflow-node-id"
|
|
250
341
|
),
|
|
@@ -287,6 +378,18 @@ def _load_task_action_context(args: argparse.Namespace, context: CliContext) ->
|
|
|
287
378
|
)
|
|
288
379
|
|
|
289
380
|
|
|
381
|
+
def _load_task_workbench_context(args: argparse.Namespace, context: CliContext) -> dict[str, Any]:
|
|
382
|
+
return context.task.task_get(
|
|
383
|
+
profile=args.profile,
|
|
384
|
+
task_id=args.task_id,
|
|
385
|
+
app_key=args.app_key or "",
|
|
386
|
+
record_id=args.record_id or "",
|
|
387
|
+
workflow_node_id=int(args.workflow_node_id or 0),
|
|
388
|
+
include_candidates=True,
|
|
389
|
+
include_associated_reports=True,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
290
393
|
def _resolve_task_action_or_select(
|
|
291
394
|
args: argparse.Namespace,
|
|
292
395
|
task_context: dict[str, Any],
|
|
@@ -316,6 +419,7 @@ def _choose_task_action_interactively(
|
|
|
316
419
|
task_context: dict[str, Any],
|
|
317
420
|
*,
|
|
318
421
|
fields: dict[str, Any],
|
|
422
|
+
title: str = "选择操作",
|
|
319
423
|
):
|
|
320
424
|
data = task_context.get("data") if isinstance(task_context.get("data"), dict) else {}
|
|
321
425
|
available_actions = [str(item).strip() for item in (data.get("available_actions") or []) if str(item).strip()]
|
|
@@ -349,13 +453,36 @@ def _choose_task_action_interactively(
|
|
|
349
453
|
|
|
350
454
|
return resolve_interactive_selection(
|
|
351
455
|
args,
|
|
352
|
-
title=
|
|
456
|
+
title=title,
|
|
353
457
|
unavailable_message="task action requires --action, or an interactive terminal to choose the current task action",
|
|
354
458
|
empty_message="task action could not open an action selector because no interactive actions are available.",
|
|
355
459
|
load_options=load_options,
|
|
356
460
|
)
|
|
357
461
|
|
|
358
462
|
|
|
463
|
+
def _show_task_workbench_detail(args: argparse.Namespace, task_context: dict[str, Any]):
|
|
464
|
+
has_actions = _task_has_interactive_actions(task_context)
|
|
465
|
+
|
|
466
|
+
def load_options() -> list[SelectionOption[str]]:
|
|
467
|
+
if has_actions:
|
|
468
|
+
return [
|
|
469
|
+
SelectionOption(value="action", label="执行操作", hint="查看当前节点可执行动作"),
|
|
470
|
+
SelectionOption(value="back", label="返回列表", hint="回到待办列表"),
|
|
471
|
+
]
|
|
472
|
+
return [
|
|
473
|
+
SelectionOption(value="back", label="返回列表", hint="当前节点没有可执行动作"),
|
|
474
|
+
SelectionOption(value="exit", label="退出工作台", hint="结束当前待办工作台"),
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
return resolve_interactive_selection(
|
|
478
|
+
args,
|
|
479
|
+
title=_build_task_detail_title(task_context),
|
|
480
|
+
unavailable_message="task workbench requires an interactive terminal",
|
|
481
|
+
empty_message="task workbench could not open the task detail view.",
|
|
482
|
+
load_options=load_options,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
359
486
|
def _resolve_action_payload_or_select(
|
|
360
487
|
args: argparse.Namespace,
|
|
361
488
|
task_context: dict[str, Any],
|
|
@@ -513,6 +640,100 @@ def _is_cancelled_result(value: Any) -> bool:
|
|
|
513
640
|
return isinstance(value, dict) and str(value.get("status") or "").strip().lower() == "cancelled"
|
|
514
641
|
|
|
515
642
|
|
|
643
|
+
def _is_success_result(result: dict[str, Any]) -> bool:
|
|
644
|
+
if not isinstance(result, dict):
|
|
645
|
+
return False
|
|
646
|
+
if result.get("ok") is False:
|
|
647
|
+
return False
|
|
648
|
+
return str(result.get("status") or "").strip().lower() == "success"
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _task_action_success_label(action: str) -> str:
|
|
652
|
+
return TASK_ACTION_SUCCESS_LABELS.get(action, "已完成")
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _build_task_workbench_list_title(banner: str | None) -> str:
|
|
656
|
+
lines = ["待办工作台", "选择一条待办后查看详情并执行操作"]
|
|
657
|
+
if banner:
|
|
658
|
+
lines.extend(["", f"上一条结果:{banner}"])
|
|
659
|
+
return "\n".join(lines)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _build_task_detail_title(task_context: dict[str, Any]) -> str:
|
|
663
|
+
data = task_context.get("data") if isinstance(task_context.get("data"), dict) else {}
|
|
664
|
+
task = data.get("task") if isinstance(data.get("task"), dict) else {}
|
|
665
|
+
record_summary = data.get("record_summary") if isinstance(data.get("record_summary"), dict) else {}
|
|
666
|
+
available_actions = data.get("available_actions") if isinstance(data.get("available_actions"), list) else []
|
|
667
|
+
editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
|
|
668
|
+
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
669
|
+
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
670
|
+
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
671
|
+
action_labels = ", ".join(TASK_ACTION_LABELS.get(str(item), str(item)) for item in available_actions if str(item).strip()) or "-"
|
|
672
|
+
lines = [
|
|
673
|
+
"待办详情",
|
|
674
|
+
f"Task ID: {task.get('task_id') or '-'}",
|
|
675
|
+
f"应用: {task.get('app_name') or '-'}",
|
|
676
|
+
f"节点: {task.get('workflow_node_name') or '-'}",
|
|
677
|
+
f"发起人: {_task_initiator_label(initiator)}",
|
|
678
|
+
f"状态: {record_summary.get('apply_status') or '-'}",
|
|
679
|
+
f"可执行动作: {action_labels}",
|
|
680
|
+
f"可编辑字段: {len(editable_fields)}",
|
|
681
|
+
(
|
|
682
|
+
"附加信息: "
|
|
683
|
+
f"报表 {_count_candidate_items(extras.get('associated_reports'))} / "
|
|
684
|
+
f"退回 {_count_candidate_items(extras.get('rollback_candidates'))} / "
|
|
685
|
+
f"转交 {_count_candidate_items(extras.get('transfer_candidates'))}"
|
|
686
|
+
),
|
|
687
|
+
]
|
|
688
|
+
if core_fields:
|
|
689
|
+
lines.append("")
|
|
690
|
+
lines.append("摘要字段:")
|
|
691
|
+
for key, value in list(core_fields.items())[:6]:
|
|
692
|
+
lines.append(f"- {key}: {_short_display(value)}")
|
|
693
|
+
return "\n".join(lines)
|
|
694
|
+
|
|
695
|
+
|
|
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
|
+
def _task_initiator_label(initiator: dict[str, Any]) -> str:
|
|
701
|
+
for key in ("display_name", "displayName", "name", "email", "uid"):
|
|
702
|
+
value = initiator.get(key)
|
|
703
|
+
if value not in (None, ""):
|
|
704
|
+
return str(value)
|
|
705
|
+
return "-"
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _task_has_interactive_actions(task_context: dict[str, Any]) -> bool:
|
|
709
|
+
data = task_context.get("data") if isinstance(task_context.get("data"), dict) else {}
|
|
710
|
+
available_actions = [str(item).strip() for item in (data.get("available_actions") or []) if str(item).strip()]
|
|
711
|
+
return any(action != "save_only" for action in available_actions)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _short_display(value: Any, *, limit: int = 60) -> str:
|
|
715
|
+
text = str(value if value not in (None, "") else "-")
|
|
716
|
+
if len(text) <= limit:
|
|
717
|
+
return text
|
|
718
|
+
return text[: limit - 1] + "…"
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _has_interactive_terminal(args: argparse.Namespace) -> bool:
|
|
722
|
+
input_stream = getattr(args, "_stdin", None)
|
|
723
|
+
return bool(getattr(input_stream, "isatty", lambda: False)())
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _raise_task_subcommand_required(args: argparse.Namespace) -> None:
|
|
727
|
+
stream = getattr(args, "_stderr_stream", None) or sys.stderr
|
|
728
|
+
task_parser = getattr(args, "_task_parser", None)
|
|
729
|
+
if isinstance(task_parser, argparse.ArgumentParser):
|
|
730
|
+
task_parser.print_usage(stream)
|
|
731
|
+
stream.write(f"{task_parser.prog}: error: the following arguments are required: task_command\n")
|
|
732
|
+
else:
|
|
733
|
+
stream.write("qingflow task: error: the following arguments are required: task_command\n")
|
|
734
|
+
raise SystemExit(2)
|
|
735
|
+
|
|
736
|
+
|
|
516
737
|
def _choose_associated_report_interactively(args: argparse.Namespace, context: CliContext):
|
|
517
738
|
def load_options() -> list[SelectionOption[int]]:
|
|
518
739
|
result = context.task.task_get(
|
|
@@ -296,6 +296,13 @@ def _format_task_action(result: dict[str, Any]) -> str:
|
|
|
296
296
|
return _task_action_success_label(action) + "\n"
|
|
297
297
|
|
|
298
298
|
|
|
299
|
+
def _format_task_workbench(result: dict[str, Any]) -> str:
|
|
300
|
+
message = str(result.get("message") or "").strip()
|
|
301
|
+
if message:
|
|
302
|
+
return message + "\n"
|
|
303
|
+
return ""
|
|
304
|
+
|
|
305
|
+
|
|
299
306
|
def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
300
307
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
301
308
|
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
@@ -588,6 +595,7 @@ _FORMATTERS = {
|
|
|
588
595
|
"app_get": _format_app_get,
|
|
589
596
|
"record_list": _format_record_list,
|
|
590
597
|
"task_list": _format_task_list,
|
|
598
|
+
"task_workbench": _format_task_workbench,
|
|
591
599
|
"task_get": _format_task_get,
|
|
592
600
|
"task_action_execute": _format_task_action,
|
|
593
601
|
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
@@ -56,6 +56,8 @@ def run(
|
|
|
56
56
|
if not bool(args.json):
|
|
57
57
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
58
58
|
result = handler(args, context)
|
|
59
|
+
except SystemExit as exc:
|
|
60
|
+
return int(exc.code or 0)
|
|
59
61
|
except RuntimeError as exc:
|
|
60
62
|
payload = trim_error_response(_parse_error_payload(exc))
|
|
61
63
|
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|