@qingflow-tech/qingflow-app-builder-mcp 1.0.35 → 1.0.37
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 +229 -12
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.37
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.37 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -9,7 +9,7 @@ import random
|
|
|
9
9
|
import re
|
|
10
10
|
import string
|
|
11
11
|
import tempfile
|
|
12
|
-
from typing import Any, cast
|
|
12
|
+
from typing import Any, NoReturn, cast
|
|
13
13
|
from urllib.parse import quote_plus, unquote_plus
|
|
14
14
|
from uuid import uuid4
|
|
15
15
|
|
|
@@ -160,6 +160,7 @@ MATCH_TYPE_ACCURACY = 1
|
|
|
160
160
|
MATCH_TYPE_QUESTION = 2
|
|
161
161
|
JUDGE_EQUAL = 0
|
|
162
162
|
JUDGE_UNEQUAL = 1
|
|
163
|
+
JUDGE_INCLUDE = 2
|
|
163
164
|
JUDGE_GREATER_OR_EQUAL = 5
|
|
164
165
|
JUDGE_LESS_OR_EQUAL = 7
|
|
165
166
|
JUDGE_EQUAL_ANY = 9
|
|
@@ -10125,14 +10126,31 @@ class AiBuilderFacade:
|
|
|
10125
10126
|
expected_filter_summary = _normalize_view_filter_groups_for_compare(expected_filters)
|
|
10126
10127
|
expected_data_scope = "CUSTOM" if expected_filter_summary else "ALL"
|
|
10127
10128
|
actual_data_scope = str(config_result.get("dataScope") or "").strip().upper() or None
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10129
|
+
(
|
|
10130
|
+
filters_semantically_equivalent,
|
|
10131
|
+
semantic_actual_filters,
|
|
10132
|
+
lossy_filter_readbacks,
|
|
10133
|
+
) = _view_filter_groups_semantic_readback(expected_filter_summary, actual_filters)
|
|
10134
|
+
filters_verified = filters_semantically_equivalent and actual_data_scope == expected_data_scope
|
|
10135
|
+
public_expected_filters = _public_view_filter_groups_from_match_rules(
|
|
10136
|
+
expected_filter_summary,
|
|
10137
|
+
current_fields_by_name=current_fields_by_name,
|
|
10138
|
+
)
|
|
10139
|
+
public_actual_filters = _public_view_filter_groups_from_match_rules(
|
|
10140
|
+
semantic_actual_filters,
|
|
10141
|
+
current_fields_by_name=current_fields_by_name,
|
|
10131
10142
|
)
|
|
10132
10143
|
verification_entry["filters_verified"] = filters_verified
|
|
10133
10144
|
verification_entry["view_key"] = verification_key
|
|
10134
|
-
verification_entry["expected_filters"] =
|
|
10135
|
-
verification_entry["actual_filters"] =
|
|
10145
|
+
verification_entry["expected_filters"] = public_expected_filters
|
|
10146
|
+
verification_entry["actual_filters"] = public_actual_filters
|
|
10147
|
+
verification_entry["expected_filters_raw"] = expected_filter_summary
|
|
10148
|
+
if lossy_filter_readbacks:
|
|
10149
|
+
verification_entry["actual_filters_raw"] = actual_filters
|
|
10150
|
+
verification_entry["filter_value_readback_degraded"] = True
|
|
10151
|
+
verification_entry["filter_value_readback_warnings"] = lossy_filter_readbacks
|
|
10152
|
+
elif public_actual_filters != semantic_actual_filters:
|
|
10153
|
+
verification_entry["actual_filters_raw"] = semantic_actual_filters
|
|
10136
10154
|
verification_entry["expected_data_scope"] = expected_data_scope
|
|
10137
10155
|
verification_entry["actual_data_scope"] = actual_data_scope
|
|
10138
10156
|
if not filters_verified:
|
|
@@ -10140,8 +10158,10 @@ class AiBuilderFacade:
|
|
|
10140
10158
|
{
|
|
10141
10159
|
"name": name,
|
|
10142
10160
|
"type": item.get("type"),
|
|
10143
|
-
"expected_filters":
|
|
10144
|
-
"actual_filters":
|
|
10161
|
+
"expected_filters": public_expected_filters,
|
|
10162
|
+
"actual_filters": public_actual_filters,
|
|
10163
|
+
"expected_filters_raw": expected_filter_summary,
|
|
10164
|
+
"actual_filters_raw": actual_filters if lossy_filter_readbacks else semantic_actual_filters,
|
|
10145
10165
|
"expected_data_scope": expected_data_scope,
|
|
10146
10166
|
"actual_data_scope": actual_data_scope,
|
|
10147
10167
|
}
|
|
@@ -14595,7 +14615,18 @@ def _build_qingbi_chart_field_lookup(
|
|
|
14595
14615
|
field_lookup: dict[str, dict[str, Any]],
|
|
14596
14616
|
) -> dict[str, Any]:
|
|
14597
14617
|
by_selector: dict[str, list[dict[str, Any]]] = {}
|
|
14598
|
-
form_by_que_id = field_lookup.get("by_que_id") or {}
|
|
14618
|
+
form_by_que_id = dict(field_lookup.get("by_que_id") or {})
|
|
14619
|
+
if not form_by_que_id:
|
|
14620
|
+
for bucket_name in ("by_name", "by_field_id"):
|
|
14621
|
+
bucket = field_lookup.get(bucket_name) or {}
|
|
14622
|
+
if not isinstance(bucket, dict):
|
|
14623
|
+
continue
|
|
14624
|
+
for form_field in bucket.values():
|
|
14625
|
+
if not isinstance(form_field, dict):
|
|
14626
|
+
continue
|
|
14627
|
+
que_id = _coerce_any_int(form_field.get("que_id"))
|
|
14628
|
+
if que_id is not None:
|
|
14629
|
+
form_by_que_id.setdefault(que_id, form_field)
|
|
14599
14630
|
|
|
14600
14631
|
def add_selector(key: Any, field: dict[str, Any]) -> None:
|
|
14601
14632
|
normalized = str(key or "").strip()
|
|
@@ -15075,13 +15106,109 @@ def _build_public_chart_filter_matrix(
|
|
|
15075
15106
|
"fieldName": qingbi_field.get("fieldName") or form_field.get("name") or field_id,
|
|
15076
15107
|
"fieldType": qingbi_field.get("fieldType") or _qingbi_field_type_from_public_field(str(form_field.get("type") or "")),
|
|
15077
15108
|
"judgeType": judge_map.get(operator, "equal"),
|
|
15078
|
-
"
|
|
15109
|
+
"judgeValue": _build_qingbi_chart_filter_judge_value(
|
|
15110
|
+
operator=operator,
|
|
15111
|
+
values=values,
|
|
15112
|
+
form_field=form_field,
|
|
15113
|
+
chart_type=chart_type,
|
|
15114
|
+
),
|
|
15115
|
+
"judgeValueDetailList": [],
|
|
15079
15116
|
"matchType": 1,
|
|
15080
15117
|
}
|
|
15081
15118
|
)
|
|
15082
15119
|
return [group] if group else []
|
|
15083
15120
|
|
|
15084
15121
|
|
|
15122
|
+
def _build_qingbi_chart_filter_judge_value(
|
|
15123
|
+
*,
|
|
15124
|
+
operator: str,
|
|
15125
|
+
values: list[Any],
|
|
15126
|
+
form_field: dict[str, Any],
|
|
15127
|
+
chart_type: str,
|
|
15128
|
+
) -> str | None:
|
|
15129
|
+
if operator in {ViewFilterOperator.is_empty.value, ViewFilterOperator.not_empty.value}:
|
|
15130
|
+
return None
|
|
15131
|
+
if operator == ViewFilterOperator.in_.value:
|
|
15132
|
+
return "<&&>".join(
|
|
15133
|
+
_qingbi_chart_filter_value_to_text(value=value, form_field=form_field, chart_type=chart_type) for value in values
|
|
15134
|
+
)
|
|
15135
|
+
if not values:
|
|
15136
|
+
return ""
|
|
15137
|
+
return _qingbi_chart_filter_value_to_text(value=values[0], form_field=form_field, chart_type=chart_type)
|
|
15138
|
+
|
|
15139
|
+
|
|
15140
|
+
def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any], chart_type: str) -> str:
|
|
15141
|
+
option_details = [
|
|
15142
|
+
item
|
|
15143
|
+
for item in (form_field.get("option_details") or [])
|
|
15144
|
+
if isinstance(item, dict) and item.get("id") is not None and item.get("value") is not None
|
|
15145
|
+
]
|
|
15146
|
+
if not option_details:
|
|
15147
|
+
if isinstance(value, dict):
|
|
15148
|
+
for key in ("value", "label", "name", "title"):
|
|
15149
|
+
raw = str(value.get(key) or "").strip()
|
|
15150
|
+
if raw:
|
|
15151
|
+
return raw
|
|
15152
|
+
if value.get("id") is not None:
|
|
15153
|
+
return str(value.get("id"))
|
|
15154
|
+
return _stringify_condition_value(value)
|
|
15155
|
+
|
|
15156
|
+
option_by_value = {str(item.get("value") or "").strip(): item for item in option_details if str(item.get("value") or "").strip()}
|
|
15157
|
+
option_by_id = {str(item.get("id")): item for item in option_details if item.get("id") is not None}
|
|
15158
|
+
|
|
15159
|
+
def resolve(raw: Any) -> str | None:
|
|
15160
|
+
text = _stringify_condition_value(raw).strip()
|
|
15161
|
+
if not text:
|
|
15162
|
+
return None
|
|
15163
|
+
matched = option_by_value.get(text) or option_by_id.get(text)
|
|
15164
|
+
if not matched:
|
|
15165
|
+
return None
|
|
15166
|
+
return str(matched.get("value") or "").strip()
|
|
15167
|
+
|
|
15168
|
+
def reject(raw: Any) -> NoReturn:
|
|
15169
|
+
allowed = [
|
|
15170
|
+
{"id": str(item.get("id")), "value": str(item.get("value"))}
|
|
15171
|
+
for item in option_details
|
|
15172
|
+
]
|
|
15173
|
+
_raise_chart_rule(
|
|
15174
|
+
rule_code="CHART_FILTER_OPTION_VALUE_UNSUPPORTED",
|
|
15175
|
+
chart_type=chart_type,
|
|
15176
|
+
message="chart filter value is not in the field option list",
|
|
15177
|
+
expected="Use an existing option label or option id from the target field schema.",
|
|
15178
|
+
actual={
|
|
15179
|
+
"field_name": form_field.get("name"),
|
|
15180
|
+
"field_id": form_field.get("que_id"),
|
|
15181
|
+
"value": raw,
|
|
15182
|
+
"allowed_values": allowed,
|
|
15183
|
+
},
|
|
15184
|
+
offending_fields=[{"field_name": form_field.get("name"), "field_id": form_field.get("que_id"), "options": allowed}],
|
|
15185
|
+
next_action="Read schema first, then pass one of the allowed option labels or option ids.",
|
|
15186
|
+
)
|
|
15187
|
+
|
|
15188
|
+
if isinstance(value, dict):
|
|
15189
|
+
for key in ("value", "label", "name", "title"):
|
|
15190
|
+
if value.get(key) is not None:
|
|
15191
|
+
resolved = resolve(value.get(key))
|
|
15192
|
+
if resolved is not None:
|
|
15193
|
+
return resolved
|
|
15194
|
+
reject(value.get(key))
|
|
15195
|
+
if value.get("id") is not None:
|
|
15196
|
+
resolved = resolve(value.get("id"))
|
|
15197
|
+
if resolved is not None:
|
|
15198
|
+
return resolved
|
|
15199
|
+
reject(value.get("id"))
|
|
15200
|
+
reject(value)
|
|
15201
|
+
if isinstance(value, int):
|
|
15202
|
+
resolved = resolve(value)
|
|
15203
|
+
if resolved is not None:
|
|
15204
|
+
return resolved
|
|
15205
|
+
reject(value)
|
|
15206
|
+
resolved = resolve(value)
|
|
15207
|
+
if resolved is not None:
|
|
15208
|
+
return resolved
|
|
15209
|
+
reject(value)
|
|
15210
|
+
|
|
15211
|
+
|
|
15085
15212
|
def _build_public_chart_config_payload(
|
|
15086
15213
|
*,
|
|
15087
15214
|
patch: ChartUpsertPatch,
|
|
@@ -22586,7 +22713,7 @@ def _translate_flow_condition_rule(*, field: dict[str, Any], rule: dict[str, Any
|
|
|
22586
22713
|
base["judgeType"] = JUDGE_INCLUDE_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES else JUDGE_EQUAL_ANY
|
|
22587
22714
|
base["judgeValues"] = [_stringify_condition_value(value) for value in values]
|
|
22588
22715
|
elif operator == "contains":
|
|
22589
|
-
base["judgeType"] =
|
|
22716
|
+
base["judgeType"] = JUDGE_INCLUDE
|
|
22590
22717
|
base["judgeValues"] = [_stringify_condition_value(values[0])]
|
|
22591
22718
|
elif operator == "gte":
|
|
22592
22719
|
base["judgeType"] = JUDGE_GREATER_OR_EQUAL
|
|
@@ -24330,7 +24457,7 @@ def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any])
|
|
|
24330
24457
|
payload["judgeType"] = JUDGE_INCLUDE_ANY if field_type in INCLUDE_ANY_FLOW_FIELD_TYPES else JUDGE_EQUAL_ANY
|
|
24331
24458
|
payload["judgeValues"] = judge_values
|
|
24332
24459
|
elif operator == "contains":
|
|
24333
|
-
payload["judgeType"] =
|
|
24460
|
+
payload["judgeType"] = JUDGE_INCLUDE
|
|
24334
24461
|
payload["judgeValues"] = judge_values[:1] if judge_values else []
|
|
24335
24462
|
elif operator == "gte":
|
|
24336
24463
|
payload["judgeType"] = JUDGE_GREATER_OR_EQUAL
|
|
@@ -24515,6 +24642,96 @@ def _view_filter_groups_equivalent(expected: Any, actual: Any) -> bool:
|
|
|
24515
24642
|
return _view_filter_groups_signature(expected) == _view_filter_groups_signature(actual)
|
|
24516
24643
|
|
|
24517
24644
|
|
|
24645
|
+
def _view_filter_groups_semantic_readback(expected: Any, actual: Any) -> tuple[bool, list[list[dict[str, Any]]], list[dict[str, Any]]]:
|
|
24646
|
+
expected_groups = _normalize_view_filter_groups_for_compare(expected)
|
|
24647
|
+
actual_groups = _normalize_view_filter_groups_for_compare(actual)
|
|
24648
|
+
if _view_filter_groups_signature(expected_groups) == _view_filter_groups_signature(actual_groups):
|
|
24649
|
+
return True, actual_groups, []
|
|
24650
|
+
if len(expected_groups) != len(actual_groups):
|
|
24651
|
+
return False, actual_groups, []
|
|
24652
|
+
semantic_groups: list[list[dict[str, Any]]] = []
|
|
24653
|
+
lossy_readbacks: list[dict[str, Any]] = []
|
|
24654
|
+
for group_index, (expected_group, actual_group) in enumerate(zip(expected_groups, actual_groups)):
|
|
24655
|
+
if len(expected_group) != len(actual_group):
|
|
24656
|
+
return False, actual_groups, []
|
|
24657
|
+
semantic_group: list[dict[str, Any]] = []
|
|
24658
|
+
for rule_index, (expected_rule, actual_rule) in enumerate(zip(expected_group, actual_group)):
|
|
24659
|
+
if expected_rule.get("queId") != actual_rule.get("queId") or expected_rule.get("judgeType") != actual_rule.get("judgeType"):
|
|
24660
|
+
return False, actual_groups, []
|
|
24661
|
+
expected_values = _view_filter_rule_values_for_signature(expected_rule)
|
|
24662
|
+
actual_values = _view_filter_rule_values_for_signature(actual_rule)
|
|
24663
|
+
if expected_values == actual_values:
|
|
24664
|
+
semantic_group.append(actual_rule)
|
|
24665
|
+
continue
|
|
24666
|
+
if (
|
|
24667
|
+
expected_values
|
|
24668
|
+
and not actual_values
|
|
24669
|
+
and actual_rule.get("judgeType") in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}
|
|
24670
|
+
):
|
|
24671
|
+
semantic_rule = deepcopy(actual_rule)
|
|
24672
|
+
semantic_rule["judgeValues"] = expected_values
|
|
24673
|
+
semantic_group.append(semantic_rule)
|
|
24674
|
+
lossy_readbacks.append(
|
|
24675
|
+
{
|
|
24676
|
+
"group_index": group_index,
|
|
24677
|
+
"rule_index": rule_index,
|
|
24678
|
+
"queId": actual_rule.get("queId"),
|
|
24679
|
+
"judgeType": actual_rule.get("judgeType"),
|
|
24680
|
+
"message": "view filter literal value was accepted by write path but omitted by raw viewConfig readback; semantic readback was reconstructed from the write payload",
|
|
24681
|
+
}
|
|
24682
|
+
)
|
|
24683
|
+
continue
|
|
24684
|
+
return False, actual_groups, []
|
|
24685
|
+
semantic_groups.append(semantic_group)
|
|
24686
|
+
return True, semantic_groups, lossy_readbacks
|
|
24687
|
+
|
|
24688
|
+
|
|
24689
|
+
def _public_view_filter_groups_from_match_rules(
|
|
24690
|
+
groups: Any,
|
|
24691
|
+
*,
|
|
24692
|
+
current_fields_by_name: dict[str, dict[str, Any]],
|
|
24693
|
+
) -> list[list[dict[str, Any]]]:
|
|
24694
|
+
fields_by_que_id = {
|
|
24695
|
+
_coerce_positive_int(field.get("que_id")): field
|
|
24696
|
+
for field in current_fields_by_name.values()
|
|
24697
|
+
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) is not None
|
|
24698
|
+
}
|
|
24699
|
+
public_groups: list[list[dict[str, Any]]] = []
|
|
24700
|
+
for group in _normalize_view_filter_groups_for_compare(groups):
|
|
24701
|
+
public_group: list[dict[str, Any]] = []
|
|
24702
|
+
for rule in group:
|
|
24703
|
+
que_id = _coerce_positive_int(rule.get("queId")) or 0
|
|
24704
|
+
field = fields_by_que_id.get(que_id) or {}
|
|
24705
|
+
values = _view_filter_rule_values_for_signature(rule)
|
|
24706
|
+
public_rule: dict[str, Any] = {
|
|
24707
|
+
"field_name": str(field.get("name") or rule.get("queTitle") or que_id),
|
|
24708
|
+
"operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType")),
|
|
24709
|
+
}
|
|
24710
|
+
if values:
|
|
24711
|
+
public_rule["values"] = values
|
|
24712
|
+
public_group.append(public_rule)
|
|
24713
|
+
if public_group:
|
|
24714
|
+
public_groups.append(public_group)
|
|
24715
|
+
return public_groups
|
|
24716
|
+
|
|
24717
|
+
|
|
24718
|
+
def _public_view_filter_operator_from_judge_type(judge_type: Any) -> str:
|
|
24719
|
+
normalized = _coerce_positive_int(judge_type)
|
|
24720
|
+
if normalized == JUDGE_EQUAL:
|
|
24721
|
+
return "eq"
|
|
24722
|
+
if normalized == JUDGE_UNEQUAL:
|
|
24723
|
+
return "neq"
|
|
24724
|
+
if normalized in {JUDGE_INCLUDE, JUDGE_FUZZY_MATCH}:
|
|
24725
|
+
return "contains"
|
|
24726
|
+
if normalized in {JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
|
|
24727
|
+
return "in"
|
|
24728
|
+
if normalized == JUDGE_GREATER_OR_EQUAL:
|
|
24729
|
+
return "gte"
|
|
24730
|
+
if normalized == JUDGE_LESS_OR_EQUAL:
|
|
24731
|
+
return "lte"
|
|
24732
|
+
return f"judge_type:{judge_type}"
|
|
24733
|
+
|
|
24734
|
+
|
|
24518
24735
|
def _infer_status_field_id(fields: list[dict[str, Any]]) -> str | None:
|
|
24519
24736
|
preferred_names = {"status", "状态", "订单状态", "审批状态", "流程状态"}
|
|
24520
24737
|
for field in fields:
|