@josephyan/qingflow-cli 0.2.0-beta.76 → 0.2.0-beta.78
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/models.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +121 -49
- 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/main.py +29 -0
- package/src/qingflow_mcp/public_surface.py +61 -52
- package/src/qingflow_mcp/server_app_builder.py +4 -4
- package/src/qingflow_mcp/tools/ai_builder_tools.py +14 -5
- package/src/qingflow_mcp/tools/record_tools.py +425 -7
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.78
|
|
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.78 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.78",
|
|
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
|
@@ -291,7 +291,7 @@ class FieldSelector(StrictModel):
|
|
|
291
291
|
|
|
292
292
|
@model_validator(mode="after")
|
|
293
293
|
def validate_selector(self) -> "FieldSelector":
|
|
294
|
-
if
|
|
294
|
+
if self.field_id is None and self.que_id is None and self.name is None:
|
|
295
295
|
raise ValueError("selector must include field_id, que_id, or name")
|
|
296
296
|
return self
|
|
297
297
|
|
|
@@ -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
|
|
|
@@ -8455,6 +8503,8 @@ def _hydrate_relation_field_configs(
|
|
|
8455
8503
|
for field in resolved_fields:
|
|
8456
8504
|
if not isinstance(field, dict) or field.get("type") != FieldType.relation.value:
|
|
8457
8505
|
continue
|
|
8506
|
+
if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict):
|
|
8507
|
+
continue
|
|
8458
8508
|
target_app_key = str(field.get("target_app_key") or "").strip()
|
|
8459
8509
|
if not target_app_key:
|
|
8460
8510
|
continue
|
|
@@ -8613,13 +8663,13 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
8613
8663
|
field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
|
|
8614
8664
|
refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
|
|
8615
8665
|
visible_fields: list[dict[str, Any]] = []
|
|
8616
|
-
display_field_que_id =
|
|
8666
|
+
display_field_que_id = _coerce_nonnegative_int(reference.get("referQueId"))
|
|
8617
8667
|
display_field_name: str | None = None
|
|
8618
8668
|
for item in refer_questions:
|
|
8619
8669
|
if not isinstance(item, dict):
|
|
8620
8670
|
continue
|
|
8621
8671
|
selector = {
|
|
8622
|
-
"que_id":
|
|
8672
|
+
"que_id": _coerce_nonnegative_int(item.get("queId")),
|
|
8623
8673
|
"name": str(item.get("queTitle") or "").strip() or None,
|
|
8624
8674
|
}
|
|
8625
8675
|
visible_fields.append(selector)
|
|
@@ -8634,6 +8684,8 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
8634
8684
|
}
|
|
8635
8685
|
field["visible_fields"] = visible_fields
|
|
8636
8686
|
field["field_name_show"] = bool(reference.get("fieldNameShow", True))
|
|
8687
|
+
field["_reference_config_template"] = deepcopy(reference)
|
|
8688
|
+
field["_relation_config_explicit"] = False
|
|
8637
8689
|
if field_type == FieldType.department:
|
|
8638
8690
|
department_scope = _normalize_department_scope_from_question(question)
|
|
8639
8691
|
if department_scope is not None:
|
|
@@ -9293,7 +9345,7 @@ def _build_selector_map(fields: list[dict[str, Any]]) -> dict[str, int]:
|
|
|
9293
9345
|
for index, field in enumerate(fields):
|
|
9294
9346
|
field_id = str(field.get("field_id") or "")
|
|
9295
9347
|
field_name = str(field.get("name") or "")
|
|
9296
|
-
que_id =
|
|
9348
|
+
que_id = _coerce_nonnegative_int(field.get("que_id"))
|
|
9297
9349
|
if field_id:
|
|
9298
9350
|
mapping[f"field_id:{field_id}"] = index
|
|
9299
9351
|
if field_name:
|
|
@@ -9308,7 +9360,7 @@ def _resolve_selector(selector_map: dict[str, int], selector: FieldSelector) ->
|
|
|
9308
9360
|
value = selector_map.get(f"field_id:{selector.field_id}")
|
|
9309
9361
|
if value is not None:
|
|
9310
9362
|
return value
|
|
9311
|
-
if selector.que_id:
|
|
9363
|
+
if selector.que_id is not None:
|
|
9312
9364
|
value = selector_map.get(f"que_id:{selector.que_id}")
|
|
9313
9365
|
if value is not None:
|
|
9314
9366
|
return value
|
|
@@ -9356,6 +9408,7 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
|
9356
9408
|
"que_id": None,
|
|
9357
9409
|
"default_type": 1,
|
|
9358
9410
|
"default_value": None,
|
|
9411
|
+
"_relation_config_explicit": patch.type == PublicFieldType.relation,
|
|
9359
9412
|
}
|
|
9360
9413
|
|
|
9361
9414
|
|
|
@@ -9426,7 +9479,7 @@ def _field_selector_payload_equal(left: Any, right: Any) -> bool:
|
|
|
9426
9479
|
return False
|
|
9427
9480
|
return (
|
|
9428
9481
|
str(left.get("field_id") or "") == str(right.get("field_id") or "")
|
|
9429
|
-
and
|
|
9482
|
+
and _coerce_nonnegative_int(left.get("que_id")) == _coerce_nonnegative_int(right.get("que_id"))
|
|
9430
9483
|
and str(left.get("name") or "") == str(right.get("name") or "")
|
|
9431
9484
|
)
|
|
9432
9485
|
|
|
@@ -9581,6 +9634,10 @@ def _code_block_binding_equal(left: Any, right: Any) -> bool:
|
|
|
9581
9634
|
|
|
9582
9635
|
def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
9583
9636
|
payload = mutation.model_dump(mode="json", exclude_none=True)
|
|
9637
|
+
relation_config_explicit = (
|
|
9638
|
+
payload.get("type") == FieldType.relation.value
|
|
9639
|
+
or any(key in payload for key in ("target_app_key", "display_field", "visible_fields", "relation_mode"))
|
|
9640
|
+
)
|
|
9584
9641
|
if "name" in payload:
|
|
9585
9642
|
field["name"] = payload["name"]
|
|
9586
9643
|
if "type" in payload:
|
|
@@ -9623,6 +9680,11 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
9623
9680
|
field["custom_button_text"] = payload["custom_button_text"]
|
|
9624
9681
|
if "subfields" in payload:
|
|
9625
9682
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
9683
|
+
if relation_config_explicit:
|
|
9684
|
+
field["_relation_config_explicit"] = True
|
|
9685
|
+
elif payload.get("type") and payload.get("type") != FieldType.relation.value:
|
|
9686
|
+
field.pop("_relation_config_explicit", None)
|
|
9687
|
+
field.pop("_reference_config_template", None)
|
|
9626
9688
|
|
|
9627
9689
|
|
|
9628
9690
|
def _resolve_field_selector_with_uniqueness(
|
|
@@ -9634,8 +9696,8 @@ def _resolve_field_selector_with_uniqueness(
|
|
|
9634
9696
|
selector = FieldSelector.model_validate(selector_payload)
|
|
9635
9697
|
if selector.field_id:
|
|
9636
9698
|
matched = [field for field in fields if str(field.get("field_id") or "") == str(selector.field_id)]
|
|
9637
|
-
elif selector.que_id:
|
|
9638
|
-
matched = [field for field in fields if
|
|
9699
|
+
elif selector.que_id is not None:
|
|
9700
|
+
matched = [field for field in fields if _coerce_nonnegative_int(field.get("que_id")) == _coerce_nonnegative_int(selector.que_id)]
|
|
9639
9701
|
elif selector.name:
|
|
9640
9702
|
name = str(selector.name or "").strip()
|
|
9641
9703
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
@@ -11094,7 +11156,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
11094
11156
|
},
|
|
11095
11157
|
temp_id,
|
|
11096
11158
|
)
|
|
11097
|
-
if field.get("que_id"):
|
|
11159
|
+
if _coerce_nonnegative_int(field.get("que_id")) is not None:
|
|
11098
11160
|
question["queId"] = field["que_id"]
|
|
11099
11161
|
else:
|
|
11100
11162
|
question["queTempId"] = temp_id
|
|
@@ -11104,13 +11166,23 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
11104
11166
|
if "default_value" in field:
|
|
11105
11167
|
question["queDefaultValue"] = field.get("default_value")
|
|
11106
11168
|
if field.get("type") == FieldType.relation.value:
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11169
|
+
preserved_reference = (
|
|
11170
|
+
deepcopy(field.get("_reference_config_template"))
|
|
11171
|
+
if not bool(field.get("_relation_config_explicit")) and isinstance(field.get("_reference_config_template"), dict)
|
|
11172
|
+
else None
|
|
11173
|
+
)
|
|
11174
|
+
if preserved_reference is not None:
|
|
11175
|
+
preserved_reference["referAppKey"] = field.get("target_app_key")
|
|
11176
|
+
preserved_reference["_targetEntityId"] = field.get("target_app_key")
|
|
11177
|
+
question["referenceConfig"] = preserved_reference
|
|
11178
|
+
else:
|
|
11179
|
+
reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
|
|
11180
|
+
reference["referAppKey"] = field.get("target_app_key")
|
|
11181
|
+
reference["_targetEntityId"] = field.get("target_app_key")
|
|
11182
|
+
if field.get("target_field_que_id") is not None:
|
|
11183
|
+
reference["referQueId"] = field.get("target_field_que_id")
|
|
11184
|
+
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
11185
|
+
question["referenceConfig"] = reference
|
|
11114
11186
|
if field.get("type") == FieldType.department.value:
|
|
11115
11187
|
scope_type, scope_payload = _serialize_department_scope_for_question(field.get("department_scope"))
|
|
11116
11188
|
question["deptSelectScopeType"] = scope_type
|
|
@@ -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,
|
|
@@ -6,6 +6,7 @@ import sys
|
|
|
6
6
|
from typing import Any, Callable, TextIO
|
|
7
7
|
|
|
8
8
|
from ..errors import QingflowApiError
|
|
9
|
+
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
9
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
10
11
|
from .context import CliContext, build_cli_context
|
|
11
12
|
from .formatters import emit_json_result, emit_text_result
|
|
@@ -49,6 +50,8 @@ def run(
|
|
|
49
50
|
return 2
|
|
50
51
|
context = context_factory()
|
|
51
52
|
try:
|
|
53
|
+
if not bool(args.json):
|
|
54
|
+
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
52
55
|
result = handler(args, context)
|
|
53
56
|
except RuntimeError as exc:
|
|
54
57
|
payload = trim_error_response(_parse_error_payload(exc))
|
|
@@ -145,5 +148,31 @@ def _result_exit_code(result: dict[str, Any]) -> int:
|
|
|
145
148
|
return 0
|
|
146
149
|
|
|
147
150
|
|
|
151
|
+
def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
|
|
152
|
+
spec = cli_public_tool_spec_from_namespace(args)
|
|
153
|
+
if spec is None or not spec.cli_show_effective_context:
|
|
154
|
+
return
|
|
155
|
+
sessions = getattr(context, "sessions", None)
|
|
156
|
+
if sessions is None or not hasattr(sessions, "get_profile"):
|
|
157
|
+
return
|
|
158
|
+
profile_name = str(getattr(args, "profile", "default") or "default")
|
|
159
|
+
try:
|
|
160
|
+
session_profile = sessions.get_profile(profile_name)
|
|
161
|
+
except Exception:
|
|
162
|
+
session_profile = None
|
|
163
|
+
workspace_id = getattr(session_profile, "selected_ws_id", None) if session_profile is not None else None
|
|
164
|
+
workspace_name = getattr(session_profile, "selected_ws_name", None) if session_profile is not None else None
|
|
165
|
+
if workspace_id is None:
|
|
166
|
+
workspace_label = "(not selected)"
|
|
167
|
+
elif workspace_name:
|
|
168
|
+
workspace_label = f"{workspace_name} ({workspace_id})"
|
|
169
|
+
else:
|
|
170
|
+
workspace_label = str(workspace_id)
|
|
171
|
+
lines = [f"Context: profile={profile_name} workspace={workspace_label}"]
|
|
172
|
+
if spec.cli_context_write and profile_name == "default":
|
|
173
|
+
lines.append("Warning: using default profile for a workspace-sensitive write command")
|
|
174
|
+
stream.write("\n".join(lines) + "\n")
|
|
175
|
+
|
|
176
|
+
|
|
148
177
|
if __name__ == "__main__":
|
|
149
178
|
main()
|