@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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.35
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.35 qingflow-app-builder-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.35"
7
+ version = "1.0.37"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- filters_verified = (
10129
- _view_filter_groups_equivalent(expected_filter_summary, actual_filters)
10130
- and actual_data_scope == expected_data_scope
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"] = expected_filter_summary
10135
- verification_entry["actual_filters"] = 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": expected_filter_summary,
10144
- "actual_filters": 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
- "judgeValues": values,
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"] = JUDGE_FUZZY_MATCH
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"] = JUDGE_FUZZY_MATCH
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: