@qingflow-tech/qingflow-app-user-mcp 1.0.8 → 1.0.10
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/skills/qingflow-record-update/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +36 -2
- package/src/qingflow_mcp/builder_facade/service.py +476 -95
- package/src/qingflow_mcp/cli/commands/builder.py +40 -11
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/response_trim.py +15 -10
- package/src/qingflow_mcp/server_app_builder.py +29 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1199 -32
- package/src/qingflow_mcp/tools/record_tools.py +200 -6
|
@@ -3757,13 +3757,44 @@ class RecordTools(ToolBase):
|
|
|
3757
3757
|
"""执行内部辅助逻辑。"""
|
|
3758
3758
|
summary = self._record_update_batch_summary(responses)
|
|
3759
3759
|
batch_items = [self._record_update_batch_item_from_response(response, output_profile=output_profile) for response in responses]
|
|
3760
|
+
public_items = [self._record_update_public_batch_item(item, index=index) for index, item in enumerate(batch_items)]
|
|
3760
3761
|
status, ok, message = self._record_update_batch_envelope_status(summary=summary, dry_run=dry_run)
|
|
3761
3762
|
first_response = responses[0] if responses else {}
|
|
3763
|
+
applied_count = int(summary.get("applied_count") or 0)
|
|
3764
|
+
ready_count = int(summary.get("ready_count") or 0)
|
|
3765
|
+
verified_count = int(summary.get("verified_count") or 0)
|
|
3766
|
+
field_level_verified_count = int(summary.get("field_level_verified_count") or 0)
|
|
3767
|
+
confirmation_count = int(summary.get("confirmation_count") or 0)
|
|
3768
|
+
blocked_count = int(summary.get("blocked_count") or 0)
|
|
3769
|
+
failed_count = int(summary.get("failed_count") or 0)
|
|
3770
|
+
write_executed = applied_count > 0
|
|
3771
|
+
verification_status = "not_requested"
|
|
3772
|
+
if write_executed:
|
|
3773
|
+
verification_status = "verified" if verified_count == applied_count else "failed"
|
|
3774
|
+
updated_record_ids = [
|
|
3775
|
+
str(item.get("record_id"))
|
|
3776
|
+
for item in public_items
|
|
3777
|
+
if item.get("record_id") not in (None, "") and str(item.get("status") or "").lower() == "success"
|
|
3778
|
+
]
|
|
3762
3779
|
return {
|
|
3763
3780
|
"profile": first_response.get("profile", profile),
|
|
3764
3781
|
"ws_id": first_response.get("ws_id"),
|
|
3765
3782
|
"ok": ok,
|
|
3766
3783
|
"status": status,
|
|
3784
|
+
"mode": "batch",
|
|
3785
|
+
"dry_run": dry_run,
|
|
3786
|
+
"app_key": app_key,
|
|
3787
|
+
"total": int(summary.get("total") or 0),
|
|
3788
|
+
"succeeded": ready_count if dry_run else applied_count,
|
|
3789
|
+
"failed": blocked_count + failed_count,
|
|
3790
|
+
"needs_confirmation": confirmation_count,
|
|
3791
|
+
"updated_record_ids": updated_record_ids,
|
|
3792
|
+
"write_executed": write_executed,
|
|
3793
|
+
"safe_to_retry": not write_executed,
|
|
3794
|
+
"verification_status": verification_status,
|
|
3795
|
+
"field_level_verified_count": field_level_verified_count,
|
|
3796
|
+
"summary": summary,
|
|
3797
|
+
"items": public_items,
|
|
3767
3798
|
"request_route": first_response.get("request_route"),
|
|
3768
3799
|
"warnings": [],
|
|
3769
3800
|
"output_profile": output_profile,
|
|
@@ -3777,6 +3808,31 @@ class RecordTools(ToolBase):
|
|
|
3777
3808
|
"message": message,
|
|
3778
3809
|
}
|
|
3779
3810
|
|
|
3811
|
+
def _record_update_public_batch_item(self, item: JSONObject, *, index: int) -> JSONObject:
|
|
3812
|
+
"""执行内部辅助逻辑。"""
|
|
3813
|
+
public = dict(item)
|
|
3814
|
+
public.setdefault("index", index)
|
|
3815
|
+
public.setdefault("row_number", index + 1)
|
|
3816
|
+
resource = public.get("resource")
|
|
3817
|
+
if isinstance(resource, dict):
|
|
3818
|
+
record_id = resource.get("record_id")
|
|
3819
|
+
apply_id = resource.get("apply_id")
|
|
3820
|
+
if record_id not in (None, ""):
|
|
3821
|
+
public["record_id"] = str(record_id)
|
|
3822
|
+
if apply_id not in (None, ""):
|
|
3823
|
+
public["apply_id"] = str(apply_id)
|
|
3824
|
+
status = str(public.get("status") or "").lower()
|
|
3825
|
+
verification = public.get("verification")
|
|
3826
|
+
if isinstance(verification, dict):
|
|
3827
|
+
if bool(verification.get("verified")):
|
|
3828
|
+
public.setdefault("verification_status", "verified")
|
|
3829
|
+
elif status == "success":
|
|
3830
|
+
public.setdefault("verification_status", "failed")
|
|
3831
|
+
public.setdefault("write_executed", status == "success")
|
|
3832
|
+
public.setdefault("safe_to_retry", not bool(public.get("write_executed")))
|
|
3833
|
+
public.setdefault("verification_status", "not_requested")
|
|
3834
|
+
return public
|
|
3835
|
+
|
|
3780
3836
|
def _record_update_batch_summary(self, responses: list[JSONObject]) -> JSONObject:
|
|
3781
3837
|
"""执行内部辅助逻辑。"""
|
|
3782
3838
|
summary: JSONObject = {
|
|
@@ -4702,6 +4758,11 @@ class RecordTools(ToolBase):
|
|
|
4702
4758
|
delete_ids = [normalize_positive_id_int(record_id, field_name="record_id")]
|
|
4703
4759
|
if not delete_ids:
|
|
4704
4760
|
raise_tool_error(QingflowApiError.config_error("record_id or record_ids is required"))
|
|
4761
|
+
seen_delete_ids: set[int] = set()
|
|
4762
|
+
for item in delete_ids:
|
|
4763
|
+
if item in seen_delete_ids:
|
|
4764
|
+
raise_tool_error(QingflowApiError.config_error(f"duplicate record id in delete payload: {stringify_backend_id(item)}"))
|
|
4765
|
+
seen_delete_ids.add(item)
|
|
4705
4766
|
normalized_payload = {
|
|
4706
4767
|
"operation": "delete",
|
|
4707
4768
|
"record_id": stringify_backend_id(record_id) if record_id is not None else None,
|
|
@@ -4709,16 +4770,134 @@ class RecordTools(ToolBase):
|
|
|
4709
4770
|
"answers": [],
|
|
4710
4771
|
"submit_type": 1,
|
|
4711
4772
|
}
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4773
|
+
return self._record_delete_public_batch(
|
|
4774
|
+
profile=profile,
|
|
4775
|
+
app_key=app_key,
|
|
4776
|
+
delete_ids=delete_ids,
|
|
4716
4777
|
normalized_payload=normalized_payload,
|
|
4717
4778
|
output_profile=normalized_output_profile,
|
|
4718
|
-
human_review=True,
|
|
4719
|
-
preflight=None,
|
|
4720
4779
|
)
|
|
4721
4780
|
|
|
4781
|
+
def _record_delete_public_batch(
|
|
4782
|
+
self,
|
|
4783
|
+
*,
|
|
4784
|
+
profile: str,
|
|
4785
|
+
app_key: str,
|
|
4786
|
+
delete_ids: list[int],
|
|
4787
|
+
normalized_payload: JSONObject,
|
|
4788
|
+
output_profile: str,
|
|
4789
|
+
) -> JSONObject:
|
|
4790
|
+
items: list[JSONObject] = []
|
|
4791
|
+
request_route: JSONObject | None = None
|
|
4792
|
+
ws_id: object = None
|
|
4793
|
+
for index, delete_id in enumerate(delete_ids):
|
|
4794
|
+
record_id_text = stringify_backend_id(delete_id)
|
|
4795
|
+
try:
|
|
4796
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
|
|
4797
|
+
request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
|
|
4798
|
+
ws_id = raw_apply.get("ws_id", ws_id)
|
|
4799
|
+
single_payload = {
|
|
4800
|
+
"operation": "delete",
|
|
4801
|
+
"record_id": record_id_text,
|
|
4802
|
+
"record_ids": [record_id_text],
|
|
4803
|
+
"answers": [],
|
|
4804
|
+
"submit_type": 1,
|
|
4805
|
+
}
|
|
4806
|
+
single_response = self._record_write_apply_response(
|
|
4807
|
+
raw_apply,
|
|
4808
|
+
operation="delete",
|
|
4809
|
+
normalized_payload=single_payload,
|
|
4810
|
+
output_profile=output_profile,
|
|
4811
|
+
human_review=True,
|
|
4812
|
+
preflight=None,
|
|
4813
|
+
)
|
|
4814
|
+
item_status = str(single_response.get("status") or "success")
|
|
4815
|
+
item: JSONObject = {
|
|
4816
|
+
"index": index,
|
|
4817
|
+
"row_number": index + 1,
|
|
4818
|
+
"record_id": record_id_text,
|
|
4819
|
+
"status": item_status,
|
|
4820
|
+
"write_executed": bool(single_response.get("write_executed")),
|
|
4821
|
+
"verification_status": single_response.get("verification_status", "not_requested"),
|
|
4822
|
+
"safe_to_retry": bool(single_response.get("safe_to_retry", False)),
|
|
4823
|
+
}
|
|
4824
|
+
if item_status != "success":
|
|
4825
|
+
item["error"] = (single_response.get("data") or {}).get("error") if isinstance(single_response.get("data"), dict) else None
|
|
4826
|
+
items.append(item)
|
|
4827
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4828
|
+
error_response = self._record_write_exception_response(
|
|
4829
|
+
exc,
|
|
4830
|
+
operation="delete",
|
|
4831
|
+
profile=profile,
|
|
4832
|
+
app_key=app_key,
|
|
4833
|
+
record_id=record_id_text,
|
|
4834
|
+
output_profile=output_profile,
|
|
4835
|
+
human_review=True,
|
|
4836
|
+
write_executed=False,
|
|
4837
|
+
)
|
|
4838
|
+
request_route = cast(JSONObject, error_response.get("request_route")) if isinstance(error_response.get("request_route"), dict) else request_route
|
|
4839
|
+
item = {
|
|
4840
|
+
"index": index,
|
|
4841
|
+
"row_number": index + 1,
|
|
4842
|
+
"record_id": record_id_text,
|
|
4843
|
+
"status": "failed",
|
|
4844
|
+
"write_executed": False,
|
|
4845
|
+
"verification_status": "not_requested",
|
|
4846
|
+
"safe_to_retry": True,
|
|
4847
|
+
"error": (error_response.get("data") or {}).get("error") if isinstance(error_response.get("data"), dict) else {"message": str(exc)},
|
|
4848
|
+
}
|
|
4849
|
+
items.append(item)
|
|
4850
|
+
deleted_ids = [
|
|
4851
|
+
str(item["record_id"])
|
|
4852
|
+
for item in items
|
|
4853
|
+
if str(item.get("status") or "") == "success"
|
|
4854
|
+
]
|
|
4855
|
+
failed_ids = [
|
|
4856
|
+
str(item["record_id"])
|
|
4857
|
+
for item in items
|
|
4858
|
+
if str(item.get("status") or "") != "success"
|
|
4859
|
+
]
|
|
4860
|
+
total = len(items)
|
|
4861
|
+
succeeded = len(deleted_ids)
|
|
4862
|
+
failed = len(failed_ids)
|
|
4863
|
+
if succeeded and failed:
|
|
4864
|
+
status = "partial_success"
|
|
4865
|
+
ok = False
|
|
4866
|
+
elif succeeded:
|
|
4867
|
+
status = "success"
|
|
4868
|
+
ok = True
|
|
4869
|
+
else:
|
|
4870
|
+
status = "failed"
|
|
4871
|
+
ok = False
|
|
4872
|
+
write_executed = any(bool(item.get("write_executed")) for item in items)
|
|
4873
|
+
return {
|
|
4874
|
+
"profile": profile,
|
|
4875
|
+
"ws_id": ws_id,
|
|
4876
|
+
"ok": ok,
|
|
4877
|
+
"status": status,
|
|
4878
|
+
"mode": "batch",
|
|
4879
|
+
"total": total,
|
|
4880
|
+
"succeeded": succeeded,
|
|
4881
|
+
"failed": failed,
|
|
4882
|
+
"deleted_ids": deleted_ids,
|
|
4883
|
+
"failed_ids": failed_ids,
|
|
4884
|
+
"write_executed": write_executed,
|
|
4885
|
+
"verification_status": "not_requested",
|
|
4886
|
+
"safe_to_retry": False if write_executed else True,
|
|
4887
|
+
"request_route": request_route,
|
|
4888
|
+
"warnings": [],
|
|
4889
|
+
"output_profile": output_profile,
|
|
4890
|
+
"items": items,
|
|
4891
|
+
"data": {
|
|
4892
|
+
"action": {"operation": "delete", "executed": write_executed},
|
|
4893
|
+
"resource": {"type": "record", "app_key": app_key, "record_id": None, "record_ids": [stringify_backend_id(item) for item in delete_ids]},
|
|
4894
|
+
"normalized_payload": normalized_payload,
|
|
4895
|
+
"deleted_ids": deleted_ids,
|
|
4896
|
+
"failed_ids": failed_ids,
|
|
4897
|
+
"items": items,
|
|
4898
|
+
},
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4722
4901
|
@tool_cn_name("写入记录")
|
|
4723
4902
|
def record_write(
|
|
4724
4903
|
self,
|
|
@@ -7277,6 +7456,7 @@ class RecordTools(ToolBase):
|
|
|
7277
7456
|
field_index_override=index,
|
|
7278
7457
|
)
|
|
7279
7458
|
except RecordInputError as error:
|
|
7459
|
+
normalized_answers = list(lookup_resolution.normalized_answers)
|
|
7280
7460
|
invalid_fields.append(
|
|
7281
7461
|
{
|
|
7282
7462
|
"location": _stringify_json(error.details.get("location") if error.details else None),
|
|
@@ -18780,6 +18960,15 @@ def _write_format_for_field(field: FormField) -> JSONObject:
|
|
|
18780
18960
|
return _write_support_payload(support_level="full", kind="boolean_label", examples=["是", "否"])
|
|
18781
18961
|
if field.que_type in DATE_QUE_TYPES:
|
|
18782
18962
|
return _write_support_payload(support_level="full", kind="date_string", examples=["2026-03-13 10:00:00"])
|
|
18963
|
+
if field.que_type == 8:
|
|
18964
|
+
allow_decimal = bool((field.raw or {}).get("canDecimal"))
|
|
18965
|
+
payload = _write_support_payload(
|
|
18966
|
+
support_level="full",
|
|
18967
|
+
kind="amount_number",
|
|
18968
|
+
examples=[100.5 if allow_decimal else 100],
|
|
18969
|
+
)
|
|
18970
|
+
payload["allow_decimal"] = allow_decimal
|
|
18971
|
+
return payload
|
|
18783
18972
|
return _write_support_payload(support_level="full", kind="scalar_text")
|
|
18784
18973
|
|
|
18785
18974
|
|
|
@@ -18805,6 +18994,11 @@ def _ready_schema_format_hint(kind: str, write_format: JSONObject) -> str:
|
|
|
18805
18994
|
if kind == "date":
|
|
18806
18995
|
return "推荐传 'YYYY-MM-DD HH:MM:SS';只有日期时可传 'YYYY-MM-DD'。"
|
|
18807
18996
|
if kind == "number":
|
|
18997
|
+
write_kind = _normalize_optional_text(write_format.get("kind"))
|
|
18998
|
+
if write_kind == "amount_number":
|
|
18999
|
+
if bool(write_format.get("allow_decimal")):
|
|
19000
|
+
return "传数字或数字字符串,支持小数。"
|
|
19001
|
+
return "传整数或整数字符串;该字段后端不接受小数。"
|
|
18808
19002
|
return "传数字或数字字符串。"
|
|
18809
19003
|
if kind == "unsupported":
|
|
18810
19004
|
reason = _normalize_optional_text(write_format.get("reason"))
|