@josephyan/qingflow-app-user-mcp 0.2.0-beta.995 → 0.2.0-beta.997
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/builder_facade/service.py +114 -28
- package/src/qingflow_mcp/cli/commands/task.py +5 -1
- package/src/qingflow_mcp/cli/commands/workspace.py +11 -0
- package/src/qingflow_mcp/cli/formatters.py +140 -0
- package/src/qingflow_mcp/cli/main.py +6 -1
- package/src/qingflow_mcp/public_surface.py +1 -0
- package/src/qingflow_mcp/response_trim.py +1 -1
- package/src/qingflow_mcp/server_app_user.py +10 -0
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
- package/src/qingflow_mcp/tools/task_context_tools.py +73 -2
- package/src/qingflow_mcp/tools/workspace_tools.py +47 -0
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.997
|
|
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.997 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -14419,6 +14419,8 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
14419
14419
|
view_type = _normalize_view_type_name(view.get("viewgraphType") or view.get("type"))
|
|
14420
14420
|
columns = view.get("columnNames") or view.get("columns") or []
|
|
14421
14421
|
group_by = view.get("groupBy") or view.get("group_by")
|
|
14422
|
+
if not any((name, view_type, columns, group_by)) and str(view_key or "").isdigit():
|
|
14423
|
+
continue
|
|
14422
14424
|
if not any((name, view_key, view_type, columns, group_by)):
|
|
14423
14425
|
continue
|
|
14424
14426
|
items.append(
|
|
@@ -14461,11 +14463,24 @@ def _summarize_views_with_config(views_tool: ViewTools, *, profile: str, views:
|
|
|
14461
14463
|
enriched_items.append(item)
|
|
14462
14464
|
continue
|
|
14463
14465
|
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
14464
|
-
|
|
14466
|
+
question_list: list[dict[str, Any]] = []
|
|
14467
|
+
try:
|
|
14468
|
+
question_response = views_tool.view_list_questions(profile=profile, viewgraph_key=view_key)
|
|
14469
|
+
raw_question_list = question_response.get("result")
|
|
14470
|
+
if isinstance(raw_question_list, list):
|
|
14471
|
+
question_list = [deepcopy(entry) for entry in raw_question_list if isinstance(entry, dict)]
|
|
14472
|
+
except (QingflowApiError, RuntimeError):
|
|
14473
|
+
question_list = []
|
|
14474
|
+
enriched_items.append(_merge_view_summary_with_config(item, config=config, question_list=question_list))
|
|
14465
14475
|
return enriched_items, config_read_errors
|
|
14466
14476
|
|
|
14467
14477
|
|
|
14468
|
-
def _merge_view_summary_with_config(
|
|
14478
|
+
def _merge_view_summary_with_config(
|
|
14479
|
+
base: dict[str, Any],
|
|
14480
|
+
*,
|
|
14481
|
+
config: dict[str, Any],
|
|
14482
|
+
question_list: list[dict[str, Any]] | None = None,
|
|
14483
|
+
) -> dict[str, Any]:
|
|
14469
14484
|
summary = deepcopy(base)
|
|
14470
14485
|
if not isinstance(config, dict) or not config:
|
|
14471
14486
|
return summary
|
|
@@ -14473,6 +14488,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
14473
14488
|
summary["visibility_summary"] = _visibility_summary(_public_visibility_from_member_auth(config.get("auth")))
|
|
14474
14489
|
legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
|
|
14475
14490
|
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
14491
|
+
canonical_question_entries = _extract_view_question_entries(question_list)
|
|
14476
14492
|
question_entries_by_id = {
|
|
14477
14493
|
field_id: entry
|
|
14478
14494
|
for entry in question_entries
|
|
@@ -14492,19 +14508,20 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
14492
14508
|
display_entries = _sort_view_question_entries(
|
|
14493
14509
|
[entry for entry in question_entries if bool(entry.get("visible", True))],
|
|
14494
14510
|
)
|
|
14511
|
+
public_display_entries = _filter_public_view_display_entries(display_entries, configured_column_ids=configured_column_ids)
|
|
14495
14512
|
display_column_ids = [
|
|
14496
14513
|
field_id
|
|
14497
|
-
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in
|
|
14514
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in public_display_entries)
|
|
14498
14515
|
if field_id is not None
|
|
14499
14516
|
]
|
|
14500
14517
|
display_columns = [
|
|
14501
14518
|
str(entry.get("name") or "").strip()
|
|
14502
|
-
for entry in
|
|
14519
|
+
for entry in public_display_entries
|
|
14503
14520
|
if str(entry.get("name") or "").strip()
|
|
14504
14521
|
]
|
|
14505
14522
|
apply_entries = [
|
|
14506
14523
|
entry
|
|
14507
|
-
for entry in
|
|
14524
|
+
for entry in public_display_entries
|
|
14508
14525
|
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
14509
14526
|
and str(entry.get("name") or "").strip()
|
|
14510
14527
|
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
@@ -14551,8 +14568,42 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
14551
14568
|
summary["apply_columns"] = apply_columns
|
|
14552
14569
|
summary["apply_column_ids"] = apply_column_ids
|
|
14553
14570
|
config_enriched = True
|
|
14554
|
-
if
|
|
14555
|
-
|
|
14571
|
+
if canonical_question_entries:
|
|
14572
|
+
canonical_display_entries = _sort_view_question_entries(canonical_question_entries)
|
|
14573
|
+
canonical_display_column_ids = [
|
|
14574
|
+
field_id
|
|
14575
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_display_entries)
|
|
14576
|
+
if field_id is not None
|
|
14577
|
+
]
|
|
14578
|
+
canonical_display_columns = [
|
|
14579
|
+
str(entry.get("name") or "").strip()
|
|
14580
|
+
for entry in canonical_display_entries
|
|
14581
|
+
if str(entry.get("name") or "").strip()
|
|
14582
|
+
]
|
|
14583
|
+
canonical_apply_entries = [
|
|
14584
|
+
entry
|
|
14585
|
+
for entry in canonical_display_entries
|
|
14586
|
+
if _coerce_nonnegative_int(entry.get("field_id")) is not None
|
|
14587
|
+
and str(entry.get("name") or "").strip()
|
|
14588
|
+
and str(entry.get("name") or "").strip() not in _KNOWN_SYSTEM_VIEW_COLUMNS
|
|
14589
|
+
]
|
|
14590
|
+
summary["columns"] = canonical_display_columns
|
|
14591
|
+
summary["display_columns"] = canonical_display_columns
|
|
14592
|
+
summary["display_column_ids"] = canonical_display_column_ids
|
|
14593
|
+
summary["column_details"] = canonical_display_entries
|
|
14594
|
+
summary["apply_columns"] = [
|
|
14595
|
+
str(entry.get("name") or "").strip()
|
|
14596
|
+
for entry in canonical_apply_entries
|
|
14597
|
+
if str(entry.get("name") or "").strip()
|
|
14598
|
+
]
|
|
14599
|
+
summary["apply_column_ids"] = [
|
|
14600
|
+
field_id
|
|
14601
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in canonical_apply_entries)
|
|
14602
|
+
if field_id is not None
|
|
14603
|
+
]
|
|
14604
|
+
config_enriched = True
|
|
14605
|
+
elif question_entries:
|
|
14606
|
+
summary["column_details"] = public_display_entries or _sort_view_question_entries(question_entries)
|
|
14556
14607
|
config_enriched = True
|
|
14557
14608
|
display_config = _extract_view_display_config(
|
|
14558
14609
|
config,
|
|
@@ -14577,7 +14628,7 @@ def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, A
|
|
|
14577
14628
|
summary["button_read_source"] = button_source
|
|
14578
14629
|
config_enriched = True
|
|
14579
14630
|
if config_enriched:
|
|
14580
|
-
summary["read_source"] = "view_config"
|
|
14631
|
+
summary["read_source"] = "view_config+question" if canonical_question_entries else "view_config"
|
|
14581
14632
|
return summary
|
|
14582
14633
|
|
|
14583
14634
|
|
|
@@ -14585,29 +14636,64 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
|
|
|
14585
14636
|
if not isinstance(questions, list):
|
|
14586
14637
|
return []
|
|
14587
14638
|
entries: list[dict[str, Any]] = []
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14639
|
+
fallback_order = 0
|
|
14640
|
+
|
|
14641
|
+
def walk(nodes: Any) -> None:
|
|
14642
|
+
nonlocal fallback_order
|
|
14643
|
+
if not isinstance(nodes, list):
|
|
14644
|
+
return
|
|
14645
|
+
for item in nodes:
|
|
14646
|
+
if not isinstance(item, dict):
|
|
14647
|
+
continue
|
|
14648
|
+
children: list[Any] = []
|
|
14649
|
+
for child_key in ("innerQues", "subQues", "innerQuestions", "subQuestions"):
|
|
14650
|
+
child_value = item.get(child_key)
|
|
14651
|
+
if isinstance(child_value, list) and child_value:
|
|
14652
|
+
children.extend(child_value)
|
|
14653
|
+
if children:
|
|
14654
|
+
walk(children)
|
|
14655
|
+
continue
|
|
14656
|
+
field_id = _coerce_nonnegative_int(item.get("queId"))
|
|
14657
|
+
name = str(item.get("queTitle") or "").strip() or None
|
|
14658
|
+
if field_id is None and name is None:
|
|
14659
|
+
continue
|
|
14660
|
+
visible_raw = item.get("beingListDisplay")
|
|
14661
|
+
if visible_raw is None:
|
|
14662
|
+
visible_raw = item.get("beingVisible")
|
|
14663
|
+
visible = bool(visible_raw) if visible_raw is not None else True
|
|
14664
|
+
display_order = _coerce_positive_int(item.get("displayOrdinal"))
|
|
14665
|
+
fallback_order += 1
|
|
14666
|
+
entry: dict[str, Any] = {
|
|
14667
|
+
"field_id": field_id,
|
|
14668
|
+
"name": name,
|
|
14669
|
+
"visible": visible,
|
|
14670
|
+
"display_order": display_order if display_order is not None else fallback_order,
|
|
14671
|
+
}
|
|
14672
|
+
width = _coerce_positive_int(item.get("width"))
|
|
14673
|
+
if width is not None:
|
|
14674
|
+
entry["width"] = width
|
|
14675
|
+
entries.append(entry)
|
|
14676
|
+
|
|
14677
|
+
walk(questions)
|
|
14608
14678
|
return entries
|
|
14609
14679
|
|
|
14610
14680
|
|
|
14681
|
+
def _filter_public_view_display_entries(
|
|
14682
|
+
entries: list[dict[str, Any]],
|
|
14683
|
+
*,
|
|
14684
|
+
configured_column_ids: list[int],
|
|
14685
|
+
) -> list[dict[str, Any]]:
|
|
14686
|
+
configured_set = set(configured_column_ids)
|
|
14687
|
+
filtered: list[dict[str, Any]] = []
|
|
14688
|
+
for entry in entries:
|
|
14689
|
+
name = str(entry.get("name") or "").strip()
|
|
14690
|
+
field_id = _coerce_nonnegative_int(entry.get("field_id"))
|
|
14691
|
+
if name in _KNOWN_SYSTEM_VIEW_COLUMNS and field_id not in configured_set:
|
|
14692
|
+
continue
|
|
14693
|
+
filtered.append(entry)
|
|
14694
|
+
return filtered or entries
|
|
14695
|
+
|
|
14696
|
+
|
|
14611
14697
|
def _sort_view_question_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
14612
14698
|
return sorted(
|
|
14613
14699
|
entries,
|
|
@@ -40,7 +40,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
40
40
|
action.add_argument("--action", required=True)
|
|
41
41
|
action.add_argument("--payload-file")
|
|
42
42
|
action.add_argument("--fields-file")
|
|
43
|
-
action.set_defaults(
|
|
43
|
+
action.set_defaults(
|
|
44
|
+
handler=_handle_action,
|
|
45
|
+
format_hint="task_action_execute",
|
|
46
|
+
hide_effective_context_line=True,
|
|
47
|
+
)
|
|
44
48
|
|
|
45
49
|
report = task_subparsers.add_parser("report", help="读取待办关联报表详情;推荐直接传 --task-id")
|
|
46
50
|
report.add_argument("--task-id")
|
|
@@ -19,6 +19,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
19
19
|
get_parser.add_argument("--ws-id", type=int, default=0)
|
|
20
20
|
get_parser.set_defaults(handler=_handle_get, format_hint="workspace_get")
|
|
21
21
|
|
|
22
|
+
select_parser = workspace_subparsers.add_parser("select", help="切换当前工作区")
|
|
23
|
+
select_parser.add_argument("--ws-id", type=int, required=True)
|
|
24
|
+
select_parser.set_defaults(handler=_handle_select, format_hint="workspace_get")
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
24
28
|
return context.workspace.workspace_list(
|
|
@@ -34,3 +38,10 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
34
38
|
profile=args.profile,
|
|
35
39
|
ws_id=args.ws_id if int(args.ws_id or 0) > 0 else None,
|
|
36
40
|
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
44
|
+
return context.workspace.workspace_select(
|
|
45
|
+
profile=args.profile,
|
|
46
|
+
ws_id=int(args.ws_id),
|
|
47
|
+
)
|
|
@@ -249,6 +249,30 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
249
249
|
return "\n".join(lines) + "\n"
|
|
250
250
|
|
|
251
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
|
+
|
|
252
276
|
def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
253
277
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
254
278
|
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
@@ -416,6 +440,121 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
|
|
|
416
440
|
return None
|
|
417
441
|
|
|
418
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
|
+
|
|
419
558
|
_FORMATTERS = {
|
|
420
559
|
"auth_whoami": _format_whoami,
|
|
421
560
|
"workspace_list": _format_workspace_list,
|
|
@@ -426,6 +565,7 @@ _FORMATTERS = {
|
|
|
426
565
|
"record_list": _format_record_list,
|
|
427
566
|
"task_list": _format_task_list,
|
|
428
567
|
"task_get": _format_task_get,
|
|
568
|
+
"task_action_execute": _format_task_action,
|
|
429
569
|
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
430
570
|
"import_verify": _format_import_verify,
|
|
431
571
|
"import_status": _format_import_status,
|
|
@@ -152,6 +152,7 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
|
|
|
152
152
|
spec = cli_public_tool_spec_from_namespace(args)
|
|
153
153
|
if spec is None or not spec.cli_show_effective_context:
|
|
154
154
|
return
|
|
155
|
+
hide_context_line = bool(getattr(args, "hide_effective_context_line", False))
|
|
155
156
|
sessions = getattr(context, "sessions", None)
|
|
156
157
|
if sessions is None or not hasattr(sessions, "get_profile"):
|
|
157
158
|
return
|
|
@@ -168,9 +169,13 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
|
|
|
168
169
|
workspace_label = f"{workspace_name} ({workspace_id})"
|
|
169
170
|
else:
|
|
170
171
|
workspace_label = str(workspace_id)
|
|
171
|
-
lines
|
|
172
|
+
lines: list[str] = []
|
|
173
|
+
if not hide_context_line:
|
|
174
|
+
lines.append(f"Context: profile={profile_name} workspace={workspace_label}")
|
|
172
175
|
if spec.cli_context_write and profile_name == "default":
|
|
173
176
|
lines.append("Warning: using default profile for a workspace-sensitive write command")
|
|
177
|
+
if not lines:
|
|
178
|
+
return
|
|
174
179
|
stream.write("\n".join(lines) + "\n")
|
|
175
180
|
|
|
176
181
|
|
|
@@ -36,6 +36,7 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
36
36
|
PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
|
|
37
37
|
PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
|
|
38
38
|
PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
|
|
39
|
+
PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
|
|
39
40
|
PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
|
|
40
41
|
PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
|
|
41
42
|
PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
|
|
@@ -737,7 +737,7 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
|
|
|
737
737
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
|
|
738
738
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
|
|
739
739
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
|
|
740
|
-
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get",), _trim_workspace_get)
|
|
740
|
+
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
|
|
741
741
|
_register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
|
|
742
742
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
|
|
743
743
|
_register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
|
|
@@ -242,6 +242,16 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
242
242
|
ws_id=ws_id,
|
|
243
243
|
)
|
|
244
244
|
|
|
245
|
+
@server.tool()
|
|
246
|
+
def workspace_select(
|
|
247
|
+
profile: str = DEFAULT_PROFILE,
|
|
248
|
+
ws_id: int = 0,
|
|
249
|
+
) -> dict:
|
|
250
|
+
return workspace.workspace_select(
|
|
251
|
+
profile=profile,
|
|
252
|
+
ws_id=ws_id,
|
|
253
|
+
)
|
|
254
|
+
|
|
245
255
|
@server.tool()
|
|
246
256
|
def app_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
247
257
|
return apps.app_list(profile=profile)
|
|
@@ -729,6 +729,7 @@ class AppTools(ToolBase):
|
|
|
729
729
|
tag_ids = item.get("tagIds") if isinstance(item.get("tagIds"), list) else []
|
|
730
730
|
compact = {
|
|
731
731
|
"app_key": app_key,
|
|
732
|
+
"app_name": title,
|
|
732
733
|
"title": title,
|
|
733
734
|
"form_id": item.get("formId"),
|
|
734
735
|
"tag_id": package_tag_id,
|
|
@@ -7,7 +7,6 @@ from ..errors import QingflowApiError, raise_tool_error
|
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
|
|
9
9
|
from .app_tools import _analysis_supported_for_view_type
|
|
10
|
-
from .app_tools import AppTools
|
|
11
10
|
from .base import ToolBase, tool_cn_name
|
|
12
11
|
from .qingbi_report_tools import QingbiReportTools
|
|
13
12
|
|
|
@@ -25,7 +24,6 @@ class ResourceReadTools(ToolBase):
|
|
|
25
24
|
def __init__(self, sessions, backend) -> None:
|
|
26
25
|
"""执行内部辅助逻辑。"""
|
|
27
26
|
super().__init__(sessions, backend)
|
|
28
|
-
self.apps = AppTools(sessions, backend)
|
|
29
27
|
self.charts = QingbiReportTools(sessions, backend)
|
|
30
28
|
|
|
31
29
|
@tool_cn_name("资源读取-门户列表")
|
|
@@ -153,9 +151,11 @@ class ResourceReadTools(ToolBase):
|
|
|
153
151
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
154
152
|
)
|
|
155
153
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
154
|
+
if not resolved_app_key:
|
|
155
|
+
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
156
156
|
if not resolved_app_key:
|
|
157
157
|
resolved_app_key = self._resolve_app_key_from_form_id(
|
|
158
|
-
|
|
158
|
+
context=context,
|
|
159
159
|
form_id=_coerce_positive_int(base_info.get("formId") or config.get("formId")),
|
|
160
160
|
)
|
|
161
161
|
if not resolved_app_key:
|
|
@@ -198,25 +198,56 @@ class ResourceReadTools(ToolBase):
|
|
|
198
198
|
|
|
199
199
|
def runner(session_profile, _context):
|
|
200
200
|
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
warnings: list[JSONObject] = []
|
|
202
|
+
verification = {
|
|
203
|
+
"chart_exists": True,
|
|
204
|
+
"chart_data_loaded": False,
|
|
205
|
+
"chart_config_loaded": False,
|
|
206
|
+
}
|
|
207
|
+
data: Any = None
|
|
208
|
+
data_config: dict[str, Any] = {}
|
|
209
|
+
try:
|
|
210
|
+
data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
|
|
211
|
+
verification["chart_data_loaded"] = True
|
|
212
|
+
if isinstance(data, dict) and isinstance(data.get("config"), dict):
|
|
213
|
+
data_config = deepcopy(data.get("config"))
|
|
214
|
+
verification["chart_config_loaded"] = True
|
|
215
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
216
|
+
api_error = error if isinstance(error, QingflowApiError) else None
|
|
217
|
+
warnings.append(
|
|
218
|
+
{
|
|
219
|
+
"code": "CHART_DATA_UNAVAILABLE",
|
|
220
|
+
"message": "chart_get could not load chart data; returning chart metadata and config when available.",
|
|
221
|
+
"backend_code": api_error.backend_code if api_error is not None else None,
|
|
222
|
+
"http_status": api_error.http_status if api_error is not None else None,
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
if not data_config:
|
|
226
|
+
try:
|
|
227
|
+
config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
228
|
+
except (QingflowApiError, RuntimeError):
|
|
229
|
+
config_result = {}
|
|
230
|
+
if isinstance(config_result, dict) and config_result:
|
|
231
|
+
data_config = deepcopy(config_result)
|
|
232
|
+
verification["chart_config_loaded"] = True
|
|
233
|
+
data_payload: dict[str, Any] = {
|
|
234
|
+
"chart_id": chart_id,
|
|
235
|
+
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
236
|
+
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
237
|
+
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
238
|
+
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
239
|
+
}
|
|
240
|
+
if verification["chart_data_loaded"]:
|
|
241
|
+
data_payload["data"] = deepcopy(data) if isinstance(data, dict) else {"value": data}
|
|
242
|
+
if data_config and not verification["chart_data_loaded"]:
|
|
243
|
+
data_payload["config"] = deepcopy(data_config)
|
|
203
244
|
return {
|
|
204
245
|
"profile": profile,
|
|
205
246
|
"ws_id": session_profile.selected_ws_id,
|
|
206
247
|
"ok": True,
|
|
207
|
-
"warnings":
|
|
208
|
-
"verification":
|
|
209
|
-
|
|
210
|
-
"chart_data_loaded": True,
|
|
211
|
-
},
|
|
212
|
-
"data": {
|
|
213
|
-
"chart_id": chart_id,
|
|
214
|
-
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
215
|
-
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
216
|
-
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
217
|
-
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
218
|
-
"data": deepcopy(data) if isinstance(data, dict) else {"value": data},
|
|
219
|
-
},
|
|
248
|
+
"warnings": warnings,
|
|
249
|
+
"verification": verification,
|
|
250
|
+
"data": data_payload,
|
|
220
251
|
}
|
|
221
252
|
|
|
222
253
|
return self._run(profile, runner)
|
|
@@ -236,25 +267,54 @@ class ResourceReadTools(ToolBase):
|
|
|
236
267
|
if not chart_id:
|
|
237
268
|
raise_tool_error(QingflowApiError.config_error("chart_id is required"))
|
|
238
269
|
|
|
239
|
-
def
|
|
270
|
+
def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
|
|
271
|
+
try:
|
|
272
|
+
form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
|
|
273
|
+
except QingflowApiError:
|
|
274
|
+
return None
|
|
275
|
+
if not isinstance(form_payload, dict):
|
|
276
|
+
return None
|
|
277
|
+
app_key = str(form_payload.get("appKey") or "").strip()
|
|
278
|
+
return app_key or None
|
|
279
|
+
|
|
280
|
+
def _resolve_app_key_from_form_id(self, *, context: Any, form_id: int | None) -> str | None:
|
|
240
281
|
"""执行内部辅助逻辑。"""
|
|
241
282
|
if form_id is None:
|
|
242
283
|
return None
|
|
243
284
|
try:
|
|
244
|
-
payload = self.
|
|
285
|
+
payload = self.backend.request("GET", context, "/tag/apps")
|
|
245
286
|
except QingflowApiError:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
payload = None
|
|
288
|
+
app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
|
|
289
|
+
if app_key:
|
|
290
|
+
return app_key
|
|
291
|
+
page_num = 1
|
|
292
|
+
page_size = 200
|
|
293
|
+
while page_num <= 20:
|
|
294
|
+
try:
|
|
295
|
+
page = self.backend.request(
|
|
296
|
+
"GET",
|
|
297
|
+
context,
|
|
298
|
+
"/app/item",
|
|
299
|
+
params={"pageNum": page_num, "pageSize": page_size},
|
|
300
|
+
)
|
|
301
|
+
except QingflowApiError:
|
|
302
|
+
return None
|
|
303
|
+
items = page.get("list") if isinstance(page, dict) else []
|
|
304
|
+
if not isinstance(items, list) or not items:
|
|
305
|
+
return None
|
|
306
|
+
for item in items:
|
|
307
|
+
if not isinstance(item, dict):
|
|
308
|
+
continue
|
|
309
|
+
if _coerce_positive_int(item.get("formId") or item.get("form_id")) != form_id:
|
|
310
|
+
continue
|
|
311
|
+
app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
|
|
312
|
+
if app_key:
|
|
313
|
+
return app_key
|
|
314
|
+
total = _coerce_positive_int(page.get("total")) if isinstance(page, dict) else None
|
|
315
|
+
if total is not None and page_num * page_size >= total:
|
|
316
|
+
break
|
|
317
|
+
page_num += 1
|
|
258
318
|
return None
|
|
259
319
|
|
|
260
320
|
|
|
@@ -419,3 +479,25 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
419
479
|
except (TypeError, ValueError):
|
|
420
480
|
return None
|
|
421
481
|
return number if number > 0 else None
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _find_visible_app_key_by_form_id(payload: Any, *, form_id: int) -> str | None:
|
|
485
|
+
if isinstance(payload, list):
|
|
486
|
+
for item in payload:
|
|
487
|
+
resolved = _find_visible_app_key_by_form_id(item, form_id=form_id)
|
|
488
|
+
if resolved:
|
|
489
|
+
return resolved
|
|
490
|
+
return None
|
|
491
|
+
if not isinstance(payload, dict):
|
|
492
|
+
return None
|
|
493
|
+
candidate_form_id = _coerce_positive_int(payload.get("formId") or payload.get("form_id"))
|
|
494
|
+
if candidate_form_id == form_id:
|
|
495
|
+
app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
|
|
496
|
+
if app_key:
|
|
497
|
+
return app_key
|
|
498
|
+
for value in payload.values():
|
|
499
|
+
if isinstance(value, (list, dict)):
|
|
500
|
+
resolved = _find_visible_app_key_by_form_id(value, form_id=form_id)
|
|
501
|
+
if resolved:
|
|
502
|
+
return resolved
|
|
503
|
+
return None
|
|
@@ -54,6 +54,7 @@ class TaskContextTools(ToolBase):
|
|
|
54
54
|
self._task_tools = TaskTools(sessions, backend)
|
|
55
55
|
self._approval_tools = ApprovalTools(sessions, backend)
|
|
56
56
|
self._record_tools = RecordTools(sessions, backend)
|
|
57
|
+
self._app_name_cache: dict[str, str | None] = {}
|
|
57
58
|
|
|
58
59
|
def register(self, mcp: FastMCP) -> None:
|
|
59
60
|
"""注册当前工具到 MCP 服务。"""
|
|
@@ -1586,7 +1587,7 @@ class TaskContextTools(ToolBase):
|
|
|
1586
1587
|
f"/app/{app_key}/apply/{record_id}",
|
|
1587
1588
|
params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
|
|
1588
1589
|
)
|
|
1589
|
-
app_name = self._task_app_name(detail, node_info)
|
|
1590
|
+
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1590
1591
|
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1591
1592
|
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
|
|
1592
1593
|
if include_associated_reports and associated_report_visible:
|
|
@@ -1833,12 +1834,82 @@ class TaskContextTools(ToolBase):
|
|
|
1833
1834
|
}
|
|
1834
1835
|
return {key: value for key, value in compact.items() if value not in (None, "", [])} or None
|
|
1835
1836
|
|
|
1836
|
-
def _task_app_name(
|
|
1837
|
+
def _task_app_name(
|
|
1838
|
+
self,
|
|
1839
|
+
*,
|
|
1840
|
+
context: BackendRequestContext,
|
|
1841
|
+
app_key: str,
|
|
1842
|
+
detail: dict[str, Any],
|
|
1843
|
+
node_info: dict[str, Any],
|
|
1844
|
+
) -> Any:
|
|
1837
1845
|
for source in (detail, node_info):
|
|
1838
1846
|
for key in ("formTitle", "appName", "worksheetName", "appTitle"):
|
|
1839
1847
|
value = source.get(key)
|
|
1840
1848
|
if value not in (None, ""):
|
|
1849
|
+
if app_key:
|
|
1850
|
+
self._app_name_cache[app_key] = str(value)
|
|
1851
|
+
return value
|
|
1852
|
+
normalized_app_key = str(app_key or "").strip()
|
|
1853
|
+
if not normalized_app_key:
|
|
1854
|
+
return None
|
|
1855
|
+
if normalized_app_key in self._app_name_cache:
|
|
1856
|
+
return self._app_name_cache[normalized_app_key]
|
|
1857
|
+
resolved = self._resolve_task_app_name_from_base_info(context=context, app_key=normalized_app_key)
|
|
1858
|
+
if resolved is None:
|
|
1859
|
+
resolved = self._resolve_task_app_name_from_visible_apps(context=context, app_key=normalized_app_key)
|
|
1860
|
+
self._app_name_cache[normalized_app_key] = resolved
|
|
1861
|
+
return resolved
|
|
1862
|
+
|
|
1863
|
+
def _resolve_task_app_name_from_base_info(
|
|
1864
|
+
self,
|
|
1865
|
+
*,
|
|
1866
|
+
context: BackendRequestContext,
|
|
1867
|
+
app_key: str,
|
|
1868
|
+
) -> str | None:
|
|
1869
|
+
try:
|
|
1870
|
+
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
1871
|
+
except QingflowApiError:
|
|
1872
|
+
return None
|
|
1873
|
+
if not isinstance(base_info, dict):
|
|
1874
|
+
return None
|
|
1875
|
+
for key in ("formTitle", "title", "appName", "name"):
|
|
1876
|
+
value = str(base_info.get(key) or "").strip()
|
|
1877
|
+
if value:
|
|
1878
|
+
return value
|
|
1879
|
+
return None
|
|
1880
|
+
|
|
1881
|
+
def _resolve_task_app_name_from_visible_apps(
|
|
1882
|
+
self,
|
|
1883
|
+
*,
|
|
1884
|
+
context: BackendRequestContext,
|
|
1885
|
+
app_key: str,
|
|
1886
|
+
) -> str | None:
|
|
1887
|
+
try:
|
|
1888
|
+
visible_apps = self.backend.request("GET", context, "/tag/apps")
|
|
1889
|
+
except QingflowApiError:
|
|
1890
|
+
return None
|
|
1891
|
+
return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
|
|
1892
|
+
|
|
1893
|
+
def _find_task_app_name_in_visible_apps(self, payload: Any, *, app_key: str) -> str | None:
|
|
1894
|
+
if isinstance(payload, list):
|
|
1895
|
+
for item in payload:
|
|
1896
|
+
resolved = self._find_task_app_name_in_visible_apps(item, app_key=app_key)
|
|
1897
|
+
if resolved:
|
|
1898
|
+
return resolved
|
|
1899
|
+
return None
|
|
1900
|
+
if not isinstance(payload, dict):
|
|
1901
|
+
return None
|
|
1902
|
+
candidate_app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
|
|
1903
|
+
if candidate_app_key == app_key:
|
|
1904
|
+
for key in ("formTitle", "title", "appName", "name"):
|
|
1905
|
+
value = str(payload.get(key) or "").strip()
|
|
1906
|
+
if value:
|
|
1841
1907
|
return value
|
|
1908
|
+
for value in payload.values():
|
|
1909
|
+
if isinstance(value, (list, dict)):
|
|
1910
|
+
resolved = self._find_task_app_name_in_visible_apps(value, app_key=app_key)
|
|
1911
|
+
if resolved:
|
|
1912
|
+
return resolved
|
|
1842
1913
|
return None
|
|
1843
1914
|
|
|
1844
1915
|
def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
|
|
@@ -45,6 +45,16 @@ class WorkspaceTools(ToolBase):
|
|
|
45
45
|
ws_id=ws_id if ws_id > 0 else None,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
def workspace_select(
|
|
50
|
+
profile: str = DEFAULT_PROFILE,
|
|
51
|
+
ws_id: int = 0,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
return self.workspace_select(
|
|
54
|
+
profile=profile,
|
|
55
|
+
ws_id=ws_id,
|
|
56
|
+
)
|
|
57
|
+
|
|
48
58
|
@mcp.tool()
|
|
49
59
|
def workspace_set_plugin_status(
|
|
50
60
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -128,6 +138,43 @@ class WorkspaceTools(ToolBase):
|
|
|
128
138
|
|
|
129
139
|
return self._run(profile, runner, require_workspace=False)
|
|
130
140
|
|
|
141
|
+
@tool_cn_name("切换工作区")
|
|
142
|
+
def workspace_select(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
profile: str = DEFAULT_PROFILE,
|
|
146
|
+
ws_id: int,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""切换当前 profile 选中的工作区,并尽量同步真实 systemVersion。"""
|
|
149
|
+
if ws_id <= 0:
|
|
150
|
+
raise_tool_error(QingflowApiError.config_error("ws_id must be positive"))
|
|
151
|
+
|
|
152
|
+
def runner(_, context):
|
|
153
|
+
workspace = self._fetch_workspace_with_fallback(context, ws_id=ws_id)
|
|
154
|
+
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip() or None
|
|
155
|
+
selected = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=workspace_name)
|
|
156
|
+
system_version = self._workspace_system_version(workspace)
|
|
157
|
+
qf_version_source = "workspace_system_version" if system_version else "unverified"
|
|
158
|
+
if system_version:
|
|
159
|
+
selected = self.sessions.update_route(
|
|
160
|
+
profile,
|
|
161
|
+
qf_version=system_version,
|
|
162
|
+
qf_version_source=qf_version_source,
|
|
163
|
+
)
|
|
164
|
+
return {
|
|
165
|
+
"profile": profile,
|
|
166
|
+
"ws_id": ws_id,
|
|
167
|
+
"qf_version": selected.qf_version,
|
|
168
|
+
"qf_version_source": selected.qf_version_source or qf_version_source,
|
|
169
|
+
"workspace": workspace,
|
|
170
|
+
"selected": {
|
|
171
|
+
"ws_id": selected.selected_ws_id,
|
|
172
|
+
"workspace_name": selected.selected_ws_name,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return self._run(profile, runner, require_workspace=False)
|
|
177
|
+
|
|
131
178
|
@tool_cn_name("设置工作区插件状态")
|
|
132
179
|
def workspace_set_plugin_status(
|
|
133
180
|
self,
|