@josephyan/qingflow-cli 0.2.0-beta.75 → 0.2.0-beta.77
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/builder_facade/service.py +82 -34
- package/src/qingflow_mcp/cli/commands/builder.py +4 -4
- package/src/qingflow_mcp/cli/commands/record.py +29 -2
- package/src/qingflow_mcp/cli/commands/task.py +0 -17
- package/src/qingflow_mcp/public_surface.py +0 -1
- package/src/qingflow_mcp/response_trim.py +0 -1
- package/src/qingflow_mcp/server_app_builder.py +4 -4
- package/src/qingflow_mcp/server_app_user.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +14 -5
- package/src/qingflow_mcp/tools/approval_tools.py +0 -16
- package/src/qingflow_mcp/tools/record_tools.py +425 -7
- package/src/qingflow_mcp/tools/task_context_tools.py +0 -16
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-cli@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-cli@0.2.0-beta.77
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.77 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyan/qingflow-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.77",
|
|
4
4
|
"description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/pyproject.toml
CHANGED
|
@@ -3530,6 +3530,12 @@ class AiBuilderFacade:
|
|
|
3530
3530
|
app_name=str(resolved["app_name"]),
|
|
3531
3531
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
3532
3532
|
)
|
|
3533
|
+
requested_app_name = str(app_name or "").strip() or None
|
|
3534
|
+
requested_rename = requested_app_name if app_key else None
|
|
3535
|
+
requested_icon = str(icon or "").strip() or None
|
|
3536
|
+
requested_color = str(color or "").strip() or None
|
|
3537
|
+
requested_app_base_update = bool(requested_rename or requested_icon or requested_color)
|
|
3538
|
+
effective_app_name = target.app_name
|
|
3533
3539
|
if not bool(resolved.get("created")):
|
|
3534
3540
|
permission_outcome = self._guard_app_permission(
|
|
3535
3541
|
profile=profile,
|
|
@@ -3540,21 +3546,35 @@ class AiBuilderFacade:
|
|
|
3540
3546
|
if permission_outcome.block is not None:
|
|
3541
3547
|
return permission_outcome.block
|
|
3542
3548
|
permission_outcomes.append(permission_outcome)
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3549
|
+
if requested_app_base_update:
|
|
3550
|
+
visual_result = self._ensure_app_base_visuals(
|
|
3551
|
+
profile=profile,
|
|
3552
|
+
app_key=target.app_key,
|
|
3553
|
+
fallback_title=target.app_name,
|
|
3554
|
+
app_name=requested_rename,
|
|
3555
|
+
icon=requested_icon,
|
|
3556
|
+
color=requested_color,
|
|
3557
|
+
normalized_args=normalized_args,
|
|
3558
|
+
)
|
|
3559
|
+
if visual_result.get("status") == "failed":
|
|
3560
|
+
return finalize(visual_result)
|
|
3561
|
+
effective_app_name = str(visual_result.get("app_name_after") or target.app_name).strip() or target.app_name
|
|
3562
|
+
else:
|
|
3563
|
+
visual_result = {
|
|
3564
|
+
"status": "success",
|
|
3565
|
+
"updated": False,
|
|
3566
|
+
"app_icon": None,
|
|
3567
|
+
"app_name_before": target.app_name,
|
|
3568
|
+
"app_name_after": target.app_name,
|
|
3569
|
+
"request_id": None,
|
|
3570
|
+
}
|
|
3553
3571
|
else:
|
|
3554
3572
|
visual_result = {
|
|
3555
3573
|
"status": "success",
|
|
3556
3574
|
"updated": False,
|
|
3557
3575
|
"app_icon": str(resolved.get("app_icon") or "").strip() or None,
|
|
3576
|
+
"app_name_before": target.app_name,
|
|
3577
|
+
"app_name_after": target.app_name,
|
|
3558
3578
|
"request_id": None,
|
|
3559
3579
|
}
|
|
3560
3580
|
if bool(resolved.get("created")) and not requested_field_changes:
|
|
@@ -3576,10 +3596,14 @@ class AiBuilderFacade:
|
|
|
3576
3596
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
3577
3597
|
"relation_field_limit_verified": True,
|
|
3578
3598
|
"app_visuals_verified": True,
|
|
3599
|
+
"app_base_verified": True,
|
|
3579
3600
|
"publish_skipped": True,
|
|
3580
3601
|
},
|
|
3581
3602
|
"app_key": target.app_key,
|
|
3582
3603
|
"app_icon": str(resolved.get("app_icon") or visual_result.get("app_icon") or "").strip() or None,
|
|
3604
|
+
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
3605
|
+
"app_name_after": str(visual_result.get("app_name_after") or target.app_name),
|
|
3606
|
+
"app_base_updated": bool(visual_result.get("updated")),
|
|
3583
3607
|
"created": True,
|
|
3584
3608
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
3585
3609
|
"verified": True,
|
|
@@ -3602,7 +3626,7 @@ class AiBuilderFacade:
|
|
|
3602
3626
|
details=_with_state_read_blocked_details({"app_key": target.app_key}, resource="schema", error=api_error),
|
|
3603
3627
|
suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3604
3628
|
))
|
|
3605
|
-
schema_result = _empty_schema_result(
|
|
3629
|
+
schema_result = _empty_schema_result(effective_app_name)
|
|
3606
3630
|
_schema_source = "synthetic_new_app"
|
|
3607
3631
|
schema_readback_delayed = True
|
|
3608
3632
|
parsed = _parse_schema(schema_result)
|
|
@@ -3759,13 +3783,19 @@ class AiBuilderFacade:
|
|
|
3759
3783
|
)
|
|
3760
3784
|
|
|
3761
3785
|
if not added and not updated and not removed and not normalized_code_block_fields and not bool(resolved.get("created")):
|
|
3762
|
-
|
|
3786
|
+
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
3787
|
+
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
3763
3788
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
3789
|
+
actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
|
|
3790
|
+
actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
|
|
3791
|
+
expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
|
|
3792
|
+
app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
|
|
3793
|
+
verified = app_base_verified and relation_target_metadata_verified
|
|
3764
3794
|
response = {
|
|
3765
|
-
"status": "success",
|
|
3766
|
-
"error_code": None,
|
|
3767
|
-
"recoverable":
|
|
3768
|
-
"message": "updated app
|
|
3795
|
+
"status": "success" if verified else "partial_success",
|
|
3796
|
+
"error_code": None if verified else "APP_BASE_READBACK_PENDING",
|
|
3797
|
+
"recoverable": not verified,
|
|
3798
|
+
"message": "updated app base metadata; schema already matches requested state" if bool(visual_result.get("updated")) else "schema already matches requested state",
|
|
3769
3799
|
"normalized_args": normalized_args,
|
|
3770
3800
|
"missing_fields": [],
|
|
3771
3801
|
"allowed_values": {"field_types": [item.value for item in PublicFieldType]},
|
|
@@ -3777,14 +3807,18 @@ class AiBuilderFacade:
|
|
|
3777
3807
|
"verification": {
|
|
3778
3808
|
"fields_verified": True,
|
|
3779
3809
|
"relation_field_limit_verified": relation_limit_verified,
|
|
3780
|
-
"app_visuals_verified":
|
|
3810
|
+
"app_visuals_verified": app_base_verified,
|
|
3811
|
+
"app_base_verified": app_base_verified,
|
|
3781
3812
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
3782
3813
|
},
|
|
3783
3814
|
"app_key": target.app_key,
|
|
3784
3815
|
"app_icon": str(visual_result.get("app_icon") or "").strip() or None,
|
|
3816
|
+
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
3817
|
+
"app_name_after": effective_app_name,
|
|
3818
|
+
"app_base_updated": bool(visual_result.get("updated")),
|
|
3785
3819
|
"created": False,
|
|
3786
3820
|
"field_diff": {"added": [], "updated": [], "removed": []},
|
|
3787
|
-
"verified":
|
|
3821
|
+
"verified": verified,
|
|
3788
3822
|
"tag_ids_after": tag_ids_after,
|
|
3789
3823
|
"package_attached": package_attached,
|
|
3790
3824
|
}
|
|
@@ -3796,7 +3830,7 @@ class AiBuilderFacade:
|
|
|
3796
3830
|
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
3797
3831
|
|
|
3798
3832
|
payload = _build_form_payload_from_fields(
|
|
3799
|
-
title=
|
|
3833
|
+
title=effective_app_name,
|
|
3800
3834
|
current_schema=schema_result,
|
|
3801
3835
|
fields=current_fields,
|
|
3802
3836
|
layout=layout,
|
|
@@ -3904,7 +3938,7 @@ class AiBuilderFacade:
|
|
|
3904
3938
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
3905
3939
|
)
|
|
3906
3940
|
rebound_payload = _build_form_payload_from_fields(
|
|
3907
|
-
title=
|
|
3941
|
+
title=effective_app_name,
|
|
3908
3942
|
current_schema=rebound_schema,
|
|
3909
3943
|
fields=rebound_fields,
|
|
3910
3944
|
layout=rebound_layout,
|
|
@@ -3948,11 +3982,15 @@ class AiBuilderFacade:
|
|
|
3948
3982
|
"fields_verified": False,
|
|
3949
3983
|
"package_attached": None,
|
|
3950
3984
|
"app_visuals_verified": True,
|
|
3985
|
+
"app_base_verified": True,
|
|
3951
3986
|
"relation_field_limit_verified": relation_limit_verified,
|
|
3952
3987
|
"relation_target_metadata_verified": relation_target_metadata_verified,
|
|
3953
3988
|
},
|
|
3954
3989
|
"app_key": target.app_key,
|
|
3955
3990
|
"app_icon": str(visual_result.get("app_icon") or resolved.get("app_icon") or "").strip() or None,
|
|
3991
|
+
"app_name_before": str(visual_result.get("app_name_before") or target.app_name),
|
|
3992
|
+
"app_name_after": effective_app_name,
|
|
3993
|
+
"app_base_updated": bool(visual_result.get("updated")),
|
|
3956
3994
|
"created": bool(resolved.get("created")),
|
|
3957
3995
|
"field_diff": {
|
|
3958
3996
|
"added": added,
|
|
@@ -3999,16 +4037,23 @@ class AiBuilderFacade:
|
|
|
3999
4037
|
base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
|
|
4000
4038
|
tag_ids_after = _coerce_int_list(base_info.get("tagIds"))
|
|
4001
4039
|
package_attached = None if package_tag_id is None else package_tag_id in tag_ids_after
|
|
4040
|
+
actual_app_name = str(base_info.get("formTitle") or effective_app_name).strip() or effective_app_name
|
|
4041
|
+
actual_app_icon = str(base_info.get("appIcon") or visual_result.get("app_icon") or "").strip() or None
|
|
4042
|
+
expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
|
|
4043
|
+
app_base_verified = actual_app_name == effective_app_name and actual_app_icon == expected_app_icon
|
|
4002
4044
|
except (QingflowApiError, RuntimeError) as error:
|
|
4003
4045
|
base_error = _coerce_api_error(error)
|
|
4004
4046
|
if verification_error is None:
|
|
4005
4047
|
verification_error = base_error
|
|
4006
4048
|
tag_ids_after = []
|
|
4007
4049
|
package_attached = None if package_tag_id is None else False
|
|
4050
|
+
app_base_verified = False
|
|
4008
4051
|
response["verification"]["fields_verified"] = verification_ok
|
|
4009
4052
|
response["verification"]["package_attached"] = package_attached
|
|
4053
|
+
response["verification"]["app_visuals_verified"] = app_base_verified
|
|
4054
|
+
response["verification"]["app_base_verified"] = app_base_verified
|
|
4010
4055
|
response["verification"]["relation_field_limit_verified"] = relation_limit_verified
|
|
4011
|
-
response["verified"] = verification_ok
|
|
4056
|
+
response["verified"] = verification_ok and app_base_verified
|
|
4012
4057
|
response["tag_ids_after"] = tag_ids_after
|
|
4013
4058
|
response["package_attached"] = package_attached
|
|
4014
4059
|
if package_attached is False:
|
|
@@ -4018,14 +4063,18 @@ class AiBuilderFacade:
|
|
|
4018
4063
|
"profile": profile,
|
|
4019
4064
|
"tag_id": package_tag_id,
|
|
4020
4065
|
"app_key": target.app_key,
|
|
4021
|
-
"app_title":
|
|
4066
|
+
"app_title": effective_app_name,
|
|
4022
4067
|
},
|
|
4023
4068
|
}
|
|
4024
4069
|
publish_failed = bool(response.get("publish_requested")) and not bool(response.get("published"))
|
|
4025
|
-
if verification_ok and package_attached is not False and not publish_failed:
|
|
4070
|
+
if verification_ok and app_base_verified and package_attached is not False and not publish_failed:
|
|
4026
4071
|
response["status"] = "success"
|
|
4027
4072
|
else:
|
|
4028
4073
|
response["status"] = "partial_success"
|
|
4074
|
+
if not app_base_verified and verification_error is None:
|
|
4075
|
+
response["recoverable"] = True
|
|
4076
|
+
response["error_code"] = response.get("error_code") or "APP_BASE_READBACK_PENDING"
|
|
4077
|
+
response["message"] = f"{response.get('message') or 'apply succeeded'}; app base readback pending"
|
|
4029
4078
|
if verification_error is not None:
|
|
4030
4079
|
response["recoverable"] = True
|
|
4031
4080
|
response["error_code"] = response.get("error_code") or (
|
|
@@ -6535,17 +6584,11 @@ class AiBuilderFacade:
|
|
|
6535
6584
|
profile: str,
|
|
6536
6585
|
app_key: str,
|
|
6537
6586
|
fallback_title: str,
|
|
6587
|
+
app_name: str | None,
|
|
6538
6588
|
icon: str | None,
|
|
6539
6589
|
color: str | None,
|
|
6540
6590
|
normalized_args: dict[str, Any],
|
|
6541
6591
|
) -> JSONObject:
|
|
6542
|
-
if not icon and not color:
|
|
6543
|
-
return {
|
|
6544
|
-
"status": "success",
|
|
6545
|
-
"updated": False,
|
|
6546
|
-
"app_icon": None,
|
|
6547
|
-
"request_id": None,
|
|
6548
|
-
}
|
|
6549
6592
|
try:
|
|
6550
6593
|
base_result = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
6551
6594
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -6559,22 +6602,25 @@ class AiBuilderFacade:
|
|
|
6559
6602
|
)
|
|
6560
6603
|
raw_base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
6561
6604
|
effective_title = str(raw_base.get("formTitle") or fallback_title or "未命名应用").strip() or "未命名应用"
|
|
6605
|
+
desired_title = str(app_name or effective_title).strip() or effective_title
|
|
6562
6606
|
existing_icon = str(raw_base.get("appIcon") or "").strip() or None
|
|
6563
6607
|
desired_icon = encode_workspace_icon_with_defaults(
|
|
6564
6608
|
icon=icon,
|
|
6565
6609
|
color=color,
|
|
6566
|
-
title=
|
|
6610
|
+
title=desired_title,
|
|
6567
6611
|
fallback_icon_name="template",
|
|
6568
6612
|
existing_icon=existing_icon,
|
|
6569
6613
|
)
|
|
6570
|
-
if desired_icon == existing_icon:
|
|
6614
|
+
if desired_icon == existing_icon and desired_title == effective_title:
|
|
6571
6615
|
return {
|
|
6572
6616
|
"status": "success",
|
|
6573
6617
|
"updated": False,
|
|
6574
|
-
"app_icon": desired_icon,
|
|
6618
|
+
"app_icon": desired_icon or existing_icon,
|
|
6619
|
+
"app_name_before": effective_title,
|
|
6620
|
+
"app_name_after": desired_title,
|
|
6575
6621
|
"request_id": None,
|
|
6576
6622
|
}
|
|
6577
|
-
payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=
|
|
6623
|
+
payload = self._build_app_base_update_payload(raw_base=raw_base, form_title=desired_title, app_icon=desired_icon)
|
|
6578
6624
|
if payload is None:
|
|
6579
6625
|
return _failed(
|
|
6580
6626
|
"APP_VISUAL_UPDATE_UNSUPPORTED",
|
|
@@ -6598,6 +6644,8 @@ class AiBuilderFacade:
|
|
|
6598
6644
|
"status": "success",
|
|
6599
6645
|
"updated": True,
|
|
6600
6646
|
"app_icon": desired_icon,
|
|
6647
|
+
"app_name_before": effective_title,
|
|
6648
|
+
"app_name_after": desired_title,
|
|
6601
6649
|
"request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
|
|
6602
6650
|
}
|
|
6603
6651
|
|
|
@@ -458,13 +458,13 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
458
458
|
has_app_title = bool((args.app_title or "").strip())
|
|
459
459
|
has_package_tag_id = args.package_tag_id is not None
|
|
460
460
|
if has_app_key:
|
|
461
|
-
if args.create_if_missing or
|
|
461
|
+
if args.create_if_missing or has_package_tag_id:
|
|
462
462
|
raise_config_error(
|
|
463
|
-
"schema apply edit mode
|
|
464
|
-
fix_hint="For existing apps,
|
|
463
|
+
"schema apply edit mode accepts --app-key and optional --app-name only.",
|
|
464
|
+
fix_hint="For existing apps, use `--app-key` and optionally `--app-name`. For create mode, use `--package-tag-id --app-name --create-if-missing`.",
|
|
465
465
|
)
|
|
466
466
|
else:
|
|
467
|
-
if not args.create_if_missing or not has_package_tag_id or not has_app_name:
|
|
467
|
+
if not args.create_if_missing or not has_package_tag_id or not (has_app_name or has_app_title):
|
|
468
468
|
raise_config_error(
|
|
469
469
|
"schema apply create mode requires --package-tag-id, --app-name, and --create-if-missing.",
|
|
470
470
|
fix_hint="Use `--app-key` for existing apps, or pass `--package-tag-id --app-name --create-if-missing` to create a new app.",
|
|
@@ -73,8 +73,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
73
73
|
|
|
74
74
|
update = record_subparsers.add_parser("update", help="更新记录")
|
|
75
75
|
update.add_argument("--app-key", required=True)
|
|
76
|
-
update.add_argument("--record-id",
|
|
77
|
-
update.add_argument("--fields-file"
|
|
76
|
+
update.add_argument("--record-id", type=int)
|
|
77
|
+
update.add_argument("--fields-file")
|
|
78
|
+
update.add_argument("--items-file")
|
|
79
|
+
update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
|
|
78
80
|
update.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
79
81
|
update.set_defaults(handler=_handle_update, format_hint="")
|
|
80
82
|
|
|
@@ -242,6 +244,31 @@ def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
242
244
|
|
|
243
245
|
|
|
244
246
|
def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
247
|
+
if args.items_file:
|
|
248
|
+
if args.record_id is not None or args.fields_file:
|
|
249
|
+
raise_config_error(
|
|
250
|
+
"record update batch mode does not accept --record-id or --fields-file.",
|
|
251
|
+
fix_hint="Use `record update --app-key APP_KEY --items-file ITEMS.json [--dry-run]` for batch updates.",
|
|
252
|
+
)
|
|
253
|
+
return context.record.record_update_public(
|
|
254
|
+
profile=args.profile,
|
|
255
|
+
app_key=args.app_key,
|
|
256
|
+
record_id=None,
|
|
257
|
+
fields=None,
|
|
258
|
+
items=require_list_arg(args.items_file, option_name="--items-file"),
|
|
259
|
+
dry_run=bool(args.dry_run),
|
|
260
|
+
verify_write=bool(args.verify_write),
|
|
261
|
+
)
|
|
262
|
+
if args.dry_run:
|
|
263
|
+
raise_config_error(
|
|
264
|
+
"record update --dry-run currently requires --items-file.",
|
|
265
|
+
fix_hint="Use `record update --app-key APP_KEY --items-file ITEMS.json --dry-run` for batch preflight.",
|
|
266
|
+
)
|
|
267
|
+
if args.record_id is None or not args.fields_file:
|
|
268
|
+
raise_config_error(
|
|
269
|
+
"record update single mode requires --record-id and --fields-file.",
|
|
270
|
+
fix_hint="Use `record update --app-key APP_KEY --record-id RECORD_ID --fields-file FIELDS.json`.",
|
|
271
|
+
)
|
|
245
272
|
return context.record.record_update_public(
|
|
246
273
|
profile=args.profile,
|
|
247
274
|
app_key=args.app_key,
|
|
@@ -37,13 +37,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
37
37
|
action.add_argument("--fields-file")
|
|
38
38
|
action.set_defaults(handler=_handle_action, format_hint="")
|
|
39
39
|
|
|
40
|
-
save_only = task_subparsers.add_parser("save-only", help="仅保存当前待办字段,不提交流程")
|
|
41
|
-
save_only.add_argument("--app-key", required=True)
|
|
42
|
-
save_only.add_argument("--record-id", required=True, type=int)
|
|
43
|
-
save_only.add_argument("--workflow-node-id", required=True, type=int)
|
|
44
|
-
save_only.add_argument("--fields-file", required=True)
|
|
45
|
-
save_only.set_defaults(handler=_handle_save_only, format_hint="")
|
|
46
|
-
|
|
47
40
|
log = task_subparsers.add_parser("log", help="读取流程日志")
|
|
48
41
|
log.add_argument("--app-key", required=True)
|
|
49
42
|
log.add_argument("--record-id", required=True, type=int)
|
|
@@ -87,16 +80,6 @@ def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
87
80
|
)
|
|
88
81
|
|
|
89
82
|
|
|
90
|
-
def _handle_save_only(args: argparse.Namespace, context: CliContext) -> dict:
|
|
91
|
-
return context.task.task_save_only(
|
|
92
|
-
profile=args.profile,
|
|
93
|
-
app_key=args.app_key,
|
|
94
|
-
record_id=args.record_id,
|
|
95
|
-
workflow_node_id=args.workflow_node_id,
|
|
96
|
-
fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
83
|
def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
|
|
101
84
|
return context.task.task_workflow_log_get(
|
|
102
85
|
profile=args.profile,
|
|
@@ -91,7 +91,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
91
91
|
PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run")),
|
|
92
92
|
PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list")),
|
|
93
93
|
PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get")),
|
|
94
|
-
PublicToolSpec(USER_DOMAIN, "task_save_only", ("task_save_only",), ("task", "save-only")),
|
|
95
94
|
PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action")),
|
|
96
95
|
PublicToolSpec(USER_DOMAIN, "task_associated_report_detail_get", ("task_associated_report_detail_get",), cli_public=False),
|
|
97
96
|
PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log")),
|
|
@@ -399,12 +399,12 @@ def build_builder_server() -> FastMCP:
|
|
|
399
399
|
has_app_title = bool((app_title or "").strip())
|
|
400
400
|
has_package_tag_id = package_tag_id is not None
|
|
401
401
|
if has_app_key:
|
|
402
|
-
if create_if_missing or
|
|
402
|
+
if create_if_missing or has_package_tag_id:
|
|
403
403
|
return _config_failure(
|
|
404
|
-
"app_schema_apply edit mode
|
|
405
|
-
fix_hint="For existing apps,
|
|
404
|
+
"app_schema_apply edit mode accepts app_key and optional app_name rename only.",
|
|
405
|
+
fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
|
|
406
406
|
)
|
|
407
|
-
elif not (create_if_missing and has_package_tag_id and has_app_name):
|
|
407
|
+
elif not (create_if_missing and has_package_tag_id and (has_app_name or has_app_title)):
|
|
408
408
|
return _config_failure(
|
|
409
409
|
"app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
|
|
410
410
|
fix_hint="Use `app_key` for existing apps, or pass `package_tag_id + app_name + create_if_missing=true` to create a new app.",
|
|
@@ -144,13 +144,13 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
144
144
|
|
|
145
145
|
## Task Workflow Path
|
|
146
146
|
|
|
147
|
-
`task_list -> task_get ->
|
|
147
|
+
`task_list -> task_get -> task_action_execute`
|
|
148
148
|
|
|
149
149
|
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
150
150
|
- Use `task_workflow_log_get` for full workflow log history.
|
|
151
151
|
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
152
152
|
- Treat `task_action_execute` as the tool-level action enum surface; the current task's real actions are only the ones listed in `task_get.capabilities.available_actions`.
|
|
153
|
-
- Use `
|
|
153
|
+
- Use `task_action_execute(action="save_only", fields=...)` when the user wants to save editable field changes on the current node without advancing the workflow.
|
|
154
154
|
- `save_only` is exposed only when the backend current-node `editableQueIds` signal returns a non-empty result; MCP no longer infers `save_only` from local schema reconstruction.
|
|
155
155
|
|
|
156
156
|
## Time Handling
|
|
@@ -299,13 +299,13 @@ class AiBuilderTools(ToolBase):
|
|
|
299
299
|
has_app_title = bool((app_title or "").strip())
|
|
300
300
|
has_package_tag_id = package_tag_id is not None
|
|
301
301
|
if has_app_key:
|
|
302
|
-
if create_if_missing or
|
|
302
|
+
if create_if_missing or has_package_tag_id:
|
|
303
303
|
return _config_failure(
|
|
304
304
|
tool_name="app_schema_apply",
|
|
305
|
-
message="app_schema_apply edit mode
|
|
306
|
-
fix_hint="For existing apps,
|
|
305
|
+
message="app_schema_apply edit mode accepts app_key and optional app_name rename only.",
|
|
306
|
+
fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
|
|
307
307
|
)
|
|
308
|
-
elif not (create_if_missing and has_package_tag_id and has_app_name):
|
|
308
|
+
elif not (create_if_missing and has_package_tag_id and (has_app_name or has_app_title)):
|
|
309
309
|
return _config_failure(
|
|
310
310
|
tool_name="app_schema_apply",
|
|
311
311
|
message="app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
|
|
@@ -2365,7 +2365,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2365
2365
|
},
|
|
2366
2366
|
"execution_notes": [
|
|
2367
2367
|
"use exactly one resource mode",
|
|
2368
|
-
"edit mode: app_key",
|
|
2368
|
+
"edit mode: app_key, optional app_name to rename the existing app",
|
|
2369
2369
|
"create mode: package_tag_id + app_name + create_if_missing=true",
|
|
2370
2370
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
2371
2371
|
"backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
|
|
@@ -2392,6 +2392,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2392
2392
|
"update_fields": [],
|
|
2393
2393
|
"remove_fields": [],
|
|
2394
2394
|
},
|
|
2395
|
+
"rename_example": {
|
|
2396
|
+
"profile": "default",
|
|
2397
|
+
"app_key": "APP_PROJECT",
|
|
2398
|
+
"app_name": "项目主数据",
|
|
2399
|
+
"publish": True,
|
|
2400
|
+
"add_fields": [],
|
|
2401
|
+
"update_fields": [],
|
|
2402
|
+
"remove_fields": [],
|
|
2403
|
+
},
|
|
2395
2404
|
"relation_example": {
|
|
2396
2405
|
"profile": "default",
|
|
2397
2406
|
"app_key": "APP_ITERATION",
|
|
@@ -117,22 +117,6 @@ class ApprovalTools(ToolBase):
|
|
|
117
117
|
) -> dict[str, Any]:
|
|
118
118
|
return self.task_transfer(profile=profile, app_key=app_key, record_id=record_id, payload=payload or {}, fields=fields or {})
|
|
119
119
|
|
|
120
|
-
@mcp.tool(description=self._high_risk_tool_description(operation="save", target="workflow task fields without advancing the workflow"))
|
|
121
|
-
def task_save_only(
|
|
122
|
-
profile: str = DEFAULT_PROFILE,
|
|
123
|
-
app_key: str = "",
|
|
124
|
-
record_id: int = 0,
|
|
125
|
-
workflow_node_id: int = 0,
|
|
126
|
-
fields: dict[str, Any] | None = None,
|
|
127
|
-
) -> dict[str, Any]:
|
|
128
|
-
return self.task_save_only(
|
|
129
|
-
profile=profile,
|
|
130
|
-
app_key=app_key,
|
|
131
|
-
record_id=record_id,
|
|
132
|
-
workflow_node_id=workflow_node_id,
|
|
133
|
-
fields=fields or {},
|
|
134
|
-
)
|
|
135
|
-
|
|
136
120
|
@mcp.tool()
|
|
137
121
|
def task_transfer_candidates(
|
|
138
122
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -460,6 +460,8 @@ class RecordTools(ToolBase):
|
|
|
460
460
|
app_key: str = "",
|
|
461
461
|
record_id: int | None = None,
|
|
462
462
|
fields: JSONObject | None = None,
|
|
463
|
+
items: list[JSONObject] | None = None,
|
|
464
|
+
dry_run: bool = False,
|
|
463
465
|
verify_write: bool = True,
|
|
464
466
|
output_profile: str = "normal",
|
|
465
467
|
) -> JSONObject:
|
|
@@ -467,7 +469,9 @@ class RecordTools(ToolBase):
|
|
|
467
469
|
profile=DEFAULT_PROFILE,
|
|
468
470
|
app_key=app_key,
|
|
469
471
|
record_id=record_id,
|
|
470
|
-
fields=fields
|
|
472
|
+
fields=fields,
|
|
473
|
+
items=items,
|
|
474
|
+
dry_run=dry_run,
|
|
471
475
|
verify_write=verify_write,
|
|
472
476
|
output_profile=output_profile,
|
|
473
477
|
)
|
|
@@ -1640,6 +1644,8 @@ class RecordTools(ToolBase):
|
|
|
1640
1644
|
),
|
|
1641
1645
|
}
|
|
1642
1646
|
)
|
|
1647
|
+
rows = list_data.get("rows", [])
|
|
1648
|
+
normalized_public_rows = _normalize_public_record_rows(rows if isinstance(rows, list) else [])
|
|
1643
1649
|
response: JSONObject = {
|
|
1644
1650
|
"profile": profile,
|
|
1645
1651
|
"ws_id": raw.get("ws_id"),
|
|
@@ -1650,7 +1656,7 @@ class RecordTools(ToolBase):
|
|
|
1650
1656
|
"output_profile": normalized_output_profile,
|
|
1651
1657
|
"data": {
|
|
1652
1658
|
"app_key": app_key,
|
|
1653
|
-
"items":
|
|
1659
|
+
"items": normalized_public_rows,
|
|
1654
1660
|
"pagination": {
|
|
1655
1661
|
"page": page,
|
|
1656
1662
|
"limit": limit,
|
|
@@ -1818,7 +1824,7 @@ class RecordTools(ToolBase):
|
|
|
1818
1824
|
"output_profile": normalized_output_profile,
|
|
1819
1825
|
"data": {
|
|
1820
1826
|
"app_key": app_key,
|
|
1821
|
-
"record_id": record_id,
|
|
1827
|
+
"record_id": _public_record_id_text(record_id),
|
|
1822
1828
|
"record": row,
|
|
1823
1829
|
"selection": {
|
|
1824
1830
|
"columns": [_column_selector_payload(field_id) for field_id in normalized_columns] if normalized_columns else [],
|
|
@@ -1918,21 +1924,60 @@ class RecordTools(ToolBase):
|
|
|
1918
1924
|
app_key: str,
|
|
1919
1925
|
record_id: int | None,
|
|
1920
1926
|
fields: JSONObject | None = None,
|
|
1927
|
+
items: list[JSONObject] | None = None,
|
|
1928
|
+
dry_run: bool = False,
|
|
1921
1929
|
verify_write: bool = True,
|
|
1922
1930
|
output_profile: str = "normal",
|
|
1923
1931
|
) -> JSONObject:
|
|
1924
1932
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
1925
1933
|
if not app_key:
|
|
1926
1934
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1935
|
+
if items is not None:
|
|
1936
|
+
if dry_run not in {True, False}:
|
|
1937
|
+
raise_tool_error(QingflowApiError.config_error("dry_run must be boolean"))
|
|
1938
|
+
normalized_items = self._normalize_public_record_update_batch_items(
|
|
1939
|
+
record_id=record_id,
|
|
1940
|
+
fields=fields,
|
|
1941
|
+
items=items,
|
|
1942
|
+
)
|
|
1943
|
+
return self._record_update_public_batch(
|
|
1944
|
+
profile=profile,
|
|
1945
|
+
app_key=app_key,
|
|
1946
|
+
items=normalized_items,
|
|
1947
|
+
dry_run=dry_run,
|
|
1948
|
+
verify_write=verify_write,
|
|
1949
|
+
output_profile=normalized_output_profile,
|
|
1950
|
+
)
|
|
1951
|
+
if dry_run:
|
|
1952
|
+
raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
|
|
1927
1953
|
if record_id is None or record_id <= 0:
|
|
1928
1954
|
raise_tool_error(QingflowApiError.config_error("record_id is required"))
|
|
1929
1955
|
if fields is not None and not isinstance(fields, dict):
|
|
1930
1956
|
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
1931
|
-
|
|
1957
|
+
return self._record_update_public_single(
|
|
1932
1958
|
profile=profile,
|
|
1933
1959
|
app_key=app_key,
|
|
1934
1960
|
record_id=record_id,
|
|
1935
1961
|
fields=cast(JSONObject, fields or {}),
|
|
1962
|
+
verify_write=verify_write,
|
|
1963
|
+
output_profile=normalized_output_profile,
|
|
1964
|
+
)
|
|
1965
|
+
|
|
1966
|
+
def _record_update_public_single(
|
|
1967
|
+
self,
|
|
1968
|
+
*,
|
|
1969
|
+
profile: str,
|
|
1970
|
+
app_key: str,
|
|
1971
|
+
record_id: int,
|
|
1972
|
+
fields: JSONObject,
|
|
1973
|
+
verify_write: bool,
|
|
1974
|
+
output_profile: str,
|
|
1975
|
+
) -> JSONObject:
|
|
1976
|
+
raw_preflight = self._preflight_record_update_with_auto_view(
|
|
1977
|
+
profile=profile,
|
|
1978
|
+
app_key=app_key,
|
|
1979
|
+
record_id=record_id,
|
|
1980
|
+
fields=fields,
|
|
1936
1981
|
force_refresh_form=False,
|
|
1937
1982
|
)
|
|
1938
1983
|
preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
|
|
@@ -1950,7 +1995,7 @@ class RecordTools(ToolBase):
|
|
|
1950
1995
|
raw_preflight,
|
|
1951
1996
|
operation="update",
|
|
1952
1997
|
normalized_payload=normalized_payload,
|
|
1953
|
-
output_profile=
|
|
1998
|
+
output_profile=output_profile,
|
|
1954
1999
|
human_review=True,
|
|
1955
2000
|
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
1956
2001
|
)
|
|
@@ -1978,11 +2023,257 @@ class RecordTools(ToolBase):
|
|
|
1978
2023
|
raw_apply,
|
|
1979
2024
|
operation="update",
|
|
1980
2025
|
normalized_payload=normalized_payload,
|
|
1981
|
-
output_profile=
|
|
2026
|
+
output_profile=output_profile,
|
|
1982
2027
|
human_review=True,
|
|
1983
2028
|
preflight=raw_preflight,
|
|
1984
2029
|
)
|
|
1985
2030
|
|
|
2031
|
+
def _record_update_public_batch(
|
|
2032
|
+
self,
|
|
2033
|
+
*,
|
|
2034
|
+
profile: str,
|
|
2035
|
+
app_key: str,
|
|
2036
|
+
items: list[JSONObject],
|
|
2037
|
+
dry_run: bool,
|
|
2038
|
+
verify_write: bool,
|
|
2039
|
+
output_profile: str,
|
|
2040
|
+
) -> JSONObject:
|
|
2041
|
+
preflight_responses = [
|
|
2042
|
+
self._record_update_public_preflight_response(
|
|
2043
|
+
profile=profile,
|
|
2044
|
+
app_key=app_key,
|
|
2045
|
+
record_id=cast(int, item["record_id"]),
|
|
2046
|
+
fields=cast(JSONObject, item["fields"]),
|
|
2047
|
+
output_profile=output_profile,
|
|
2048
|
+
)
|
|
2049
|
+
for item in items
|
|
2050
|
+
]
|
|
2051
|
+
has_preflight_blockers = any(
|
|
2052
|
+
str(response.get("status") or "").lower() in {"blocked", "needs_confirmation"}
|
|
2053
|
+
for response in preflight_responses
|
|
2054
|
+
)
|
|
2055
|
+
if dry_run or has_preflight_blockers:
|
|
2056
|
+
return self._record_update_batch_response(
|
|
2057
|
+
profile=profile,
|
|
2058
|
+
app_key=app_key,
|
|
2059
|
+
responses=preflight_responses,
|
|
2060
|
+
output_profile=output_profile,
|
|
2061
|
+
dry_run=dry_run,
|
|
2062
|
+
)
|
|
2063
|
+
|
|
2064
|
+
apply_responses: list[JSONObject] = []
|
|
2065
|
+
for item in items:
|
|
2066
|
+
record_id = cast(int, item["record_id"])
|
|
2067
|
+
fields = cast(JSONObject, item["fields"])
|
|
2068
|
+
try:
|
|
2069
|
+
apply_responses.append(
|
|
2070
|
+
self._record_update_public_single(
|
|
2071
|
+
profile=profile,
|
|
2072
|
+
app_key=app_key,
|
|
2073
|
+
record_id=record_id,
|
|
2074
|
+
fields=fields,
|
|
2075
|
+
verify_write=verify_write,
|
|
2076
|
+
output_profile=output_profile,
|
|
2077
|
+
)
|
|
2078
|
+
)
|
|
2079
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
2080
|
+
apply_responses.append(
|
|
2081
|
+
self._record_write_exception_response(
|
|
2082
|
+
exc,
|
|
2083
|
+
operation="update",
|
|
2084
|
+
profile=profile,
|
|
2085
|
+
app_key=app_key,
|
|
2086
|
+
record_id=record_id,
|
|
2087
|
+
output_profile=output_profile,
|
|
2088
|
+
human_review=True,
|
|
2089
|
+
)
|
|
2090
|
+
)
|
|
2091
|
+
|
|
2092
|
+
return self._record_update_batch_response(
|
|
2093
|
+
profile=profile,
|
|
2094
|
+
app_key=app_key,
|
|
2095
|
+
responses=apply_responses,
|
|
2096
|
+
output_profile=output_profile,
|
|
2097
|
+
dry_run=False,
|
|
2098
|
+
)
|
|
2099
|
+
|
|
2100
|
+
def _record_update_public_preflight_response(
|
|
2101
|
+
self,
|
|
2102
|
+
*,
|
|
2103
|
+
profile: str,
|
|
2104
|
+
app_key: str,
|
|
2105
|
+
record_id: int,
|
|
2106
|
+
fields: JSONObject,
|
|
2107
|
+
output_profile: str,
|
|
2108
|
+
) -> JSONObject:
|
|
2109
|
+
raw_preflight = self._preflight_record_update_with_auto_view(
|
|
2110
|
+
profile=profile,
|
|
2111
|
+
app_key=app_key,
|
|
2112
|
+
record_id=record_id,
|
|
2113
|
+
fields=fields,
|
|
2114
|
+
force_refresh_form=False,
|
|
2115
|
+
)
|
|
2116
|
+
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
2117
|
+
normalized_payload = self._record_write_normalized_payload(
|
|
2118
|
+
operation="update",
|
|
2119
|
+
record_id=record_id,
|
|
2120
|
+
record_ids=[],
|
|
2121
|
+
normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
|
|
2122
|
+
submit_type=1,
|
|
2123
|
+
selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
|
|
2124
|
+
)
|
|
2125
|
+
confirmation_requests = cast(list[JSONObject], preflight_data.get("confirmation_requests", []))
|
|
2126
|
+
if preflight_data.get("blockers") or confirmation_requests:
|
|
2127
|
+
return self._record_write_blocked_response(
|
|
2128
|
+
raw_preflight,
|
|
2129
|
+
operation="update",
|
|
2130
|
+
normalized_payload=normalized_payload,
|
|
2131
|
+
output_profile=output_profile,
|
|
2132
|
+
human_review=True,
|
|
2133
|
+
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
2134
|
+
)
|
|
2135
|
+
return self._record_write_ready_response(
|
|
2136
|
+
raw_preflight,
|
|
2137
|
+
operation="update",
|
|
2138
|
+
normalized_payload=normalized_payload,
|
|
2139
|
+
output_profile=output_profile,
|
|
2140
|
+
human_review=True,
|
|
2141
|
+
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
2142
|
+
)
|
|
2143
|
+
|
|
2144
|
+
def _normalize_public_record_update_batch_items(
|
|
2145
|
+
self,
|
|
2146
|
+
*,
|
|
2147
|
+
record_id: int | None,
|
|
2148
|
+
fields: JSONObject | None,
|
|
2149
|
+
items: list[JSONObject] | None,
|
|
2150
|
+
) -> list[JSONObject]:
|
|
2151
|
+
if record_id is not None or fields is not None:
|
|
2152
|
+
raise_tool_error(
|
|
2153
|
+
QingflowApiError.config_error("record_update batch mode does not accept record_id or fields")
|
|
2154
|
+
)
|
|
2155
|
+
if not isinstance(items, list) or not items:
|
|
2156
|
+
raise_tool_error(QingflowApiError.config_error("items must be a non-empty list"))
|
|
2157
|
+
normalized_items: list[JSONObject] = []
|
|
2158
|
+
seen_record_ids: set[int] = set()
|
|
2159
|
+
for index, item in enumerate(items):
|
|
2160
|
+
if not isinstance(item, dict):
|
|
2161
|
+
raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
|
|
2162
|
+
normalized_record_id = _coerce_count(item.get("record_id"))
|
|
2163
|
+
if normalized_record_id is None or normalized_record_id <= 0:
|
|
2164
|
+
raise_tool_error(QingflowApiError.config_error(f"items[{index}].record_id must be a positive integer"))
|
|
2165
|
+
if normalized_record_id in seen_record_ids:
|
|
2166
|
+
raise_tool_error(
|
|
2167
|
+
QingflowApiError.config_error(f"duplicate record_id in items: {normalized_record_id}")
|
|
2168
|
+
)
|
|
2169
|
+
item_fields = item.get("fields")
|
|
2170
|
+
if not isinstance(item_fields, dict):
|
|
2171
|
+
raise_tool_error(QingflowApiError.config_error(f"items[{index}].fields must be an object map keyed by field title"))
|
|
2172
|
+
seen_record_ids.add(normalized_record_id)
|
|
2173
|
+
normalized_items.append({"record_id": normalized_record_id, "fields": cast(JSONObject, item_fields)})
|
|
2174
|
+
return normalized_items
|
|
2175
|
+
|
|
2176
|
+
def _record_update_batch_response(
|
|
2177
|
+
self,
|
|
2178
|
+
*,
|
|
2179
|
+
profile: str,
|
|
2180
|
+
app_key: str,
|
|
2181
|
+
responses: list[JSONObject],
|
|
2182
|
+
output_profile: str,
|
|
2183
|
+
dry_run: bool,
|
|
2184
|
+
) -> JSONObject:
|
|
2185
|
+
summary = self._record_update_batch_summary(responses)
|
|
2186
|
+
batch_items = [self._record_update_batch_item_from_response(response, output_profile=output_profile) for response in responses]
|
|
2187
|
+
status, ok, message = self._record_update_batch_envelope_status(summary=summary, dry_run=dry_run)
|
|
2188
|
+
first_response = responses[0] if responses else {}
|
|
2189
|
+
return {
|
|
2190
|
+
"profile": first_response.get("profile", profile),
|
|
2191
|
+
"ws_id": first_response.get("ws_id"),
|
|
2192
|
+
"ok": ok,
|
|
2193
|
+
"status": status,
|
|
2194
|
+
"request_route": first_response.get("request_route"),
|
|
2195
|
+
"warnings": [],
|
|
2196
|
+
"output_profile": output_profile,
|
|
2197
|
+
"data": {
|
|
2198
|
+
"app_key": app_key,
|
|
2199
|
+
"mode": "batch",
|
|
2200
|
+
"dry_run": dry_run,
|
|
2201
|
+
"summary": summary,
|
|
2202
|
+
"items": batch_items,
|
|
2203
|
+
},
|
|
2204
|
+
"message": message,
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
def _record_update_batch_summary(self, responses: list[JSONObject]) -> JSONObject:
|
|
2208
|
+
summary: JSONObject = {
|
|
2209
|
+
"total": len(responses),
|
|
2210
|
+
"ready_count": 0,
|
|
2211
|
+
"blocked_count": 0,
|
|
2212
|
+
"confirmation_count": 0,
|
|
2213
|
+
"applied_count": 0,
|
|
2214
|
+
"verified_count": 0,
|
|
2215
|
+
"field_level_verified_count": 0,
|
|
2216
|
+
"failed_count": 0,
|
|
2217
|
+
}
|
|
2218
|
+
for response in responses:
|
|
2219
|
+
status = str(response.get("status") or "").lower()
|
|
2220
|
+
data = cast(JSONObject, response.get("data", {}))
|
|
2221
|
+
action = cast(JSONObject, data.get("action", {}))
|
|
2222
|
+
verification = cast(JSONObject, data.get("verification", {})) if isinstance(data.get("verification"), dict) else {}
|
|
2223
|
+
executed = bool(action.get("executed"))
|
|
2224
|
+
if status == "ready":
|
|
2225
|
+
summary["ready_count"] = int(summary["ready_count"]) + 1
|
|
2226
|
+
continue
|
|
2227
|
+
if status == "blocked":
|
|
2228
|
+
summary["blocked_count"] = int(summary["blocked_count"]) + 1
|
|
2229
|
+
continue
|
|
2230
|
+
if status == "needs_confirmation":
|
|
2231
|
+
summary["confirmation_count"] = int(summary["confirmation_count"]) + 1
|
|
2232
|
+
continue
|
|
2233
|
+
if executed:
|
|
2234
|
+
summary["applied_count"] = int(summary["applied_count"]) + 1
|
|
2235
|
+
if bool(verification.get("verified")):
|
|
2236
|
+
summary["verified_count"] = int(summary["verified_count"]) + 1
|
|
2237
|
+
if bool(verification.get("field_level_verified")):
|
|
2238
|
+
summary["field_level_verified_count"] = int(summary["field_level_verified_count"]) + 1
|
|
2239
|
+
if status not in {"success"}:
|
|
2240
|
+
summary["failed_count"] = int(summary["failed_count"]) + 1
|
|
2241
|
+
return summary
|
|
2242
|
+
|
|
2243
|
+
def _record_update_batch_envelope_status(self, *, summary: JSONObject, dry_run: bool) -> tuple[str, bool, str]:
|
|
2244
|
+
if int(summary["blocked_count"]) > 0:
|
|
2245
|
+
return "blocked", False, "batch update preflight blocked"
|
|
2246
|
+
if int(summary["confirmation_count"]) > 0:
|
|
2247
|
+
return "needs_confirmation", False, "batch update requires confirmation before write"
|
|
2248
|
+
if dry_run:
|
|
2249
|
+
return "ready", True, "batch update dry run ready"
|
|
2250
|
+
if int(summary["failed_count"]) > 0:
|
|
2251
|
+
return "partial_success", False, "batch update completed with partial failures"
|
|
2252
|
+
return "success", True, "batch update completed"
|
|
2253
|
+
|
|
2254
|
+
def _record_update_batch_item_from_response(self, response: JSONObject, *, output_profile: str) -> JSONObject:
|
|
2255
|
+
data = cast(JSONObject, response.get("data", {}))
|
|
2256
|
+
item: JSONObject = {
|
|
2257
|
+
"resource": data.get("resource"),
|
|
2258
|
+
"status": response.get("status"),
|
|
2259
|
+
"verification": data.get("verification"),
|
|
2260
|
+
"field_errors": cast(list[JSONObject], data.get("field_errors", [])),
|
|
2261
|
+
"confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
|
|
2262
|
+
"resolved_fields": cast(list[JSONObject], data.get("resolved_fields", [])),
|
|
2263
|
+
}
|
|
2264
|
+
blockers = data.get("blockers")
|
|
2265
|
+
if isinstance(blockers, list) and blockers:
|
|
2266
|
+
item["blockers"] = blockers
|
|
2267
|
+
warnings = response.get("warnings")
|
|
2268
|
+
if isinstance(warnings, list) and warnings:
|
|
2269
|
+
item["warnings"] = warnings
|
|
2270
|
+
error = data.get("error")
|
|
2271
|
+
if isinstance(error, dict):
|
|
2272
|
+
item["error"] = error
|
|
2273
|
+
if output_profile == "verbose" and isinstance(data.get("debug"), dict):
|
|
2274
|
+
item["debug"] = data.get("debug")
|
|
2275
|
+
return item
|
|
2276
|
+
|
|
1986
2277
|
def _preflight_record_update_with_auto_view(
|
|
1987
2278
|
self,
|
|
1988
2279
|
*,
|
|
@@ -7944,6 +8235,48 @@ class RecordTools(ToolBase):
|
|
|
7944
8235
|
}
|
|
7945
8236
|
return response
|
|
7946
8237
|
|
|
8238
|
+
def _record_write_ready_response(
|
|
8239
|
+
self,
|
|
8240
|
+
raw_preflight: JSONObject,
|
|
8241
|
+
*,
|
|
8242
|
+
operation: str,
|
|
8243
|
+
normalized_payload: JSONObject,
|
|
8244
|
+
output_profile: str,
|
|
8245
|
+
human_review: bool,
|
|
8246
|
+
target_resource: JSONObject,
|
|
8247
|
+
) -> JSONObject:
|
|
8248
|
+
plan_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
8249
|
+
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
8250
|
+
resolved_fields = cast(list[JSONObject], plan_data.get("lookup_resolved_fields", []))
|
|
8251
|
+
warnings_payload = validation.get("warnings", [])
|
|
8252
|
+
warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
|
|
8253
|
+
response: JSONObject = {
|
|
8254
|
+
"profile": raw_preflight.get("profile"),
|
|
8255
|
+
"ws_id": raw_preflight.get("ws_id"),
|
|
8256
|
+
"ok": True,
|
|
8257
|
+
"status": "ready",
|
|
8258
|
+
"request_route": raw_preflight.get("request_route"),
|
|
8259
|
+
"warnings": warnings,
|
|
8260
|
+
"output_profile": output_profile,
|
|
8261
|
+
"data": {
|
|
8262
|
+
"action": {"operation": operation, "executed": False},
|
|
8263
|
+
"resource": target_resource,
|
|
8264
|
+
"verification": None,
|
|
8265
|
+
"normalized_payload": normalized_payload,
|
|
8266
|
+
"blockers": [],
|
|
8267
|
+
"field_errors": [],
|
|
8268
|
+
"confirmation_requests": [],
|
|
8269
|
+
"resolved_fields": resolved_fields,
|
|
8270
|
+
"recommended_next_actions": cast(list[JSONValue], plan_data.get("recommended_next_actions", [])),
|
|
8271
|
+
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
8272
|
+
},
|
|
8273
|
+
}
|
|
8274
|
+
if output_profile == "verbose":
|
|
8275
|
+
response["data"]["debug"] = {
|
|
8276
|
+
"preflight": plan_data,
|
|
8277
|
+
}
|
|
8278
|
+
return response
|
|
8279
|
+
|
|
7947
8280
|
def _record_write_apply_response(
|
|
7948
8281
|
self,
|
|
7949
8282
|
raw_apply: JSONObject,
|
|
@@ -7994,6 +8327,67 @@ class RecordTools(ToolBase):
|
|
|
7994
8327
|
response["data"]["debug"] = debug
|
|
7995
8328
|
return response
|
|
7996
8329
|
|
|
8330
|
+
def _record_write_exception_response(
|
|
8331
|
+
self,
|
|
8332
|
+
exc: QingflowApiError | RuntimeError,
|
|
8333
|
+
*,
|
|
8334
|
+
operation: str,
|
|
8335
|
+
profile: str,
|
|
8336
|
+
app_key: str,
|
|
8337
|
+
record_id: int,
|
|
8338
|
+
output_profile: str,
|
|
8339
|
+
human_review: bool,
|
|
8340
|
+
) -> JSONObject:
|
|
8341
|
+
error_payload: JSONObject = {
|
|
8342
|
+
"error_code": "RECORD_WRITE_EXECUTION_FAILED",
|
|
8343
|
+
"message": str(exc),
|
|
8344
|
+
}
|
|
8345
|
+
request_route: JSONObject | None = None
|
|
8346
|
+
if isinstance(exc, QingflowApiError):
|
|
8347
|
+
error_payload["message"] = exc.message
|
|
8348
|
+
if exc.backend_code is not None:
|
|
8349
|
+
error_payload["backend_code"] = exc.backend_code
|
|
8350
|
+
if exc.request_id is not None:
|
|
8351
|
+
error_payload["request_id"] = exc.request_id
|
|
8352
|
+
else:
|
|
8353
|
+
try:
|
|
8354
|
+
parsed = json.loads(str(exc))
|
|
8355
|
+
except json.JSONDecodeError:
|
|
8356
|
+
parsed = None
|
|
8357
|
+
if isinstance(parsed, dict):
|
|
8358
|
+
error_payload["error_code"] = parsed.get("error_code") or cast(JSONObject, parsed.get("details", {})).get("error_code") or error_payload["error_code"]
|
|
8359
|
+
error_payload["message"] = parsed.get("message") or error_payload["message"]
|
|
8360
|
+
if parsed.get("backend_code") is not None:
|
|
8361
|
+
error_payload["backend_code"] = parsed.get("backend_code")
|
|
8362
|
+
if parsed.get("request_id") is not None:
|
|
8363
|
+
error_payload["request_id"] = parsed.get("request_id")
|
|
8364
|
+
if isinstance(parsed.get("request_route"), dict):
|
|
8365
|
+
request_route = cast(JSONObject, parsed.get("request_route"))
|
|
8366
|
+
response: JSONObject = {
|
|
8367
|
+
"profile": profile,
|
|
8368
|
+
"ws_id": None,
|
|
8369
|
+
"ok": False,
|
|
8370
|
+
"status": "failed",
|
|
8371
|
+
"request_route": request_route,
|
|
8372
|
+
"warnings": [],
|
|
8373
|
+
"output_profile": output_profile,
|
|
8374
|
+
"data": {
|
|
8375
|
+
"action": {"operation": operation, "executed": True},
|
|
8376
|
+
"resource": {"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
8377
|
+
"verification": None,
|
|
8378
|
+
"normalized_payload": None,
|
|
8379
|
+
"blockers": [],
|
|
8380
|
+
"field_errors": [],
|
|
8381
|
+
"confirmation_requests": [],
|
|
8382
|
+
"resolved_fields": [],
|
|
8383
|
+
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
8384
|
+
"error": error_payload,
|
|
8385
|
+
},
|
|
8386
|
+
}
|
|
8387
|
+
if output_profile == "verbose":
|
|
8388
|
+
response["data"]["debug"] = {"exception": error_payload}
|
|
8389
|
+
return response
|
|
8390
|
+
|
|
7997
8391
|
def _record_write_public_field_errors(self, plan_data: JSONObject) -> list[JSONObject]:
|
|
7998
8392
|
existing = plan_data.get("field_errors")
|
|
7999
8393
|
if isinstance(existing, list):
|
|
@@ -10221,12 +10615,36 @@ def _view_selection_supported_by_search_ids(view_selection: ViewSelection, searc
|
|
|
10221
10615
|
|
|
10222
10616
|
|
|
10223
10617
|
def _build_flat_row(answer_list: list[JSONValue], fields: list[FormField], *, apply_id: int | None) -> JSONObject:
|
|
10224
|
-
|
|
10618
|
+
public_record_id = _public_record_id_text(apply_id)
|
|
10619
|
+
row: JSONObject = {"apply_id": public_record_id, "record_id": public_record_id}
|
|
10225
10620
|
for field in fields:
|
|
10226
10621
|
row[field.que_title] = _extract_field_value(answer_list, field)
|
|
10227
10622
|
return row
|
|
10228
10623
|
|
|
10229
10624
|
|
|
10625
|
+
def _public_record_id_text(record_id: int | None) -> str | None:
|
|
10626
|
+
if record_id is None or record_id <= 0:
|
|
10627
|
+
return None
|
|
10628
|
+
return str(record_id)
|
|
10629
|
+
|
|
10630
|
+
|
|
10631
|
+
def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
|
|
10632
|
+
normalized_rows: list[JSONObject] = []
|
|
10633
|
+
for item in rows:
|
|
10634
|
+
if not isinstance(item, dict):
|
|
10635
|
+
continue
|
|
10636
|
+
row = dict(item)
|
|
10637
|
+
normalized_record_id = _coerce_count(row.get("apply_id"))
|
|
10638
|
+
if normalized_record_id is None:
|
|
10639
|
+
normalized_record_id = _coerce_count(row.get("record_id"))
|
|
10640
|
+
if normalized_record_id is not None and normalized_record_id > 0:
|
|
10641
|
+
public_record_id = _public_record_id_text(normalized_record_id)
|
|
10642
|
+
row["apply_id"] = public_record_id
|
|
10643
|
+
row["record_id"] = public_record_id
|
|
10644
|
+
normalized_rows.append(row)
|
|
10645
|
+
return normalized_rows
|
|
10646
|
+
|
|
10647
|
+
|
|
10230
10648
|
def _merge_answer_lists_by_field_id(existing: list[JSONValue], extra: list[JSONValue]) -> list[JSONValue]:
|
|
10231
10649
|
merged: dict[str, JSONValue] = {}
|
|
10232
10650
|
order: list[str] = []
|
|
@@ -104,22 +104,6 @@ class TaskContextTools(ToolBase):
|
|
|
104
104
|
fields=fields or {},
|
|
105
105
|
)
|
|
106
106
|
|
|
107
|
-
@mcp.tool()
|
|
108
|
-
def task_save_only(
|
|
109
|
-
profile: str = DEFAULT_PROFILE,
|
|
110
|
-
app_key: str = "",
|
|
111
|
-
record_id: int = 0,
|
|
112
|
-
workflow_node_id: int = 0,
|
|
113
|
-
fields: dict[str, Any] | None = None,
|
|
114
|
-
) -> dict[str, Any]:
|
|
115
|
-
return self.task_save_only(
|
|
116
|
-
profile=profile,
|
|
117
|
-
app_key=app_key,
|
|
118
|
-
record_id=record_id,
|
|
119
|
-
workflow_node_id=workflow_node_id,
|
|
120
|
-
fields=fields or {},
|
|
121
|
-
)
|
|
122
|
-
|
|
123
107
|
@mcp.tool()
|
|
124
108
|
def task_associated_report_detail_get(
|
|
125
109
|
profile: str = DEFAULT_PROFILE,
|