@qingflow-tech/qingflow-app-builder-mcp 1.0.38 → 1.0.39

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.38
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.39
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.38 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.39 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.38",
3
+ "version": "1.0.39",
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.38"
7
+ version = "1.0.39"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2215,6 +2215,7 @@ class ChartGetResponse(StrictModel):
2215
2215
  chart_id: str
2216
2216
  base: dict[str, Any] = Field(default_factory=dict)
2217
2217
  visibility: dict[str, Any] = Field(default_factory=dict)
2218
+ filters: list[list[dict[str, Any]]] = Field(default_factory=list)
2218
2219
  config: dict[str, Any] = Field(default_factory=dict)
2219
2220
 
2220
2221
 
@@ -7184,6 +7184,7 @@ class AiBuilderFacade:
7184
7184
  chart_id=chart_id,
7185
7185
  base=deepcopy(base) if isinstance(base, dict) else {},
7186
7186
  visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
7187
+ filters=_public_chart_filter_groups_from_qingbi_config(config) if isinstance(config, dict) else [],
7187
7188
  config=deepcopy(config) if isinstance(config, dict) else {},
7188
7189
  )
7189
7190
  return {
@@ -11612,12 +11613,12 @@ class AiBuilderFacade:
11612
11613
  base_payload=base_payload,
11613
11614
  )
11614
11615
  if sections_requested:
11615
- component_payload = self._build_portal_components_from_sections(
11616
+ component_payload, layout_metadata = self._build_portal_components_from_sections(
11616
11617
  profile=profile,
11617
11618
  sections=request.sections,
11618
11619
  layout_preset=request.layout_preset,
11619
11620
  )
11620
- layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload)
11621
+ layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
11621
11622
  update_payload["components"] = component_payload
11622
11623
  self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
11623
11624
  write_executed = True
@@ -12563,8 +12564,9 @@ class AiBuilderFacade:
12563
12564
  profile: str,
12564
12565
  sections: list[PortalSectionPatch],
12565
12566
  layout_preset: str | None = None,
12566
- ) -> list[dict[str, Any]]:
12567
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
12567
12568
  resolved_components: list[dict[str, Any]] = []
12569
+ layout_metadata: list[dict[str, Any]] = []
12568
12570
  pc_x = 0
12569
12571
  pc_y = 0
12570
12572
  pc_row_height = 0
@@ -12596,6 +12598,11 @@ class AiBuilderFacade:
12596
12598
  profile=profile,
12597
12599
  ref=section.chart_ref,
12598
12600
  )
12601
+ chart_type = str(
12602
+ resolved_chart.get("chart_type")
12603
+ or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
12604
+ or ""
12605
+ ).strip()
12599
12606
  chart_config = {
12600
12607
  "biChartId": resolved_chart["chart_id"],
12601
12608
  "chartComponentTitle": section.title,
@@ -12603,6 +12610,7 @@ class AiBuilderFacade:
12603
12610
  **deepcopy(section.config),
12604
12611
  }
12605
12612
  component = {"type": 9, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
12613
+ layout_metadata.append({"source_type": section.source_type, "chart_type": chart_type})
12606
12614
  elif section.source_type == "view":
12607
12615
  resolved_view = _resolve_view_reference(
12608
12616
  facade=self,
@@ -12625,26 +12633,31 @@ class AiBuilderFacade:
12625
12633
  **deepcopy(section.config),
12626
12634
  }
12627
12635
  component = {"type": 10, "position": position_payload, "viewgraphConfig": _compact_dict(view_config)}
12636
+ layout_metadata.append({"source_type": section.source_type})
12628
12637
  elif section.source_type == "grid":
12629
12638
  component = {
12630
12639
  "type": 2,
12631
12640
  "position": position_payload,
12632
12641
  "gridConfig": _compact_dict({"gridTitle": section.title, "beingShowTitle": True, **deepcopy(section.config)}),
12633
12642
  }
12643
+ layout_metadata.append({"source_type": section.source_type})
12634
12644
  elif section.source_type == "filter":
12635
12645
  component = {"type": 6, "position": position_payload, "filterConfig": deepcopy(section.config)}
12646
+ layout_metadata.append({"source_type": section.source_type})
12636
12647
  elif section.source_type == "text":
12637
12648
  component = {"type": 5, "position": position_payload, "textConfig": {"text": section.text or "", **deepcopy(section.config)}}
12649
+ layout_metadata.append({"source_type": section.source_type})
12638
12650
  else:
12639
12651
  component = {
12640
12652
  "type": 4,
12641
12653
  "position": position_payload,
12642
12654
  "linkConfig": {"url": section.url or "", "beingLoginAuth": False, **deepcopy(section.config)},
12643
12655
  }
12656
+ layout_metadata.append({"source_type": section.source_type})
12644
12657
  if dash_style is not None:
12645
12658
  component["dashStyleConfigBO"] = dash_style
12646
12659
  resolved_components.append(component)
12647
- return resolved_components
12660
+ return resolved_components, layout_metadata
12648
12661
 
12649
12662
  def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
12650
12663
  session_profile = self.apps.sessions.get_profile(profile)
@@ -15212,6 +15225,103 @@ def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any]
15212
15225
  reject(value)
15213
15226
 
15214
15227
 
15228
+ def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> list[list[dict[str, Any]]]:
15229
+ groups: list[list[dict[str, Any]]] = []
15230
+ raw_groups = config.get("beforeAggregationFilterMatrix")
15231
+ if not isinstance(raw_groups, list):
15232
+ return groups
15233
+ for raw_group in raw_groups:
15234
+ if not isinstance(raw_group, list):
15235
+ continue
15236
+ group: list[dict[str, Any]] = []
15237
+ for raw_rule in raw_group:
15238
+ if not isinstance(raw_rule, dict):
15239
+ continue
15240
+ operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
15241
+ field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
15242
+ field_name = (
15243
+ raw_rule.get("fieldName")
15244
+ or raw_rule.get("field_name")
15245
+ or raw_rule.get("queTitle")
15246
+ or raw_rule.get("title")
15247
+ or field_id
15248
+ )
15249
+ public_rule: dict[str, Any] = {
15250
+ "field_name": _stringify_condition_value(field_name).strip(),
15251
+ "operator": operator,
15252
+ }
15253
+ if field_id is not None:
15254
+ public_rule["field_id"] = _stringify_condition_value(field_id).strip()
15255
+ values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
15256
+ if values:
15257
+ public_rule["values"] = values
15258
+ group.append(public_rule)
15259
+ if group:
15260
+ groups.append(group)
15261
+ return groups
15262
+
15263
+
15264
+ def _public_chart_filter_operator_from_judge_type(judge_type: Any) -> str:
15265
+ normalized = _stringify_condition_value(judge_type).strip()
15266
+ mapping = {
15267
+ "equal": "eq",
15268
+ "equals": "eq",
15269
+ "=": "eq",
15270
+ "unequal": "neq",
15271
+ "notEqual": "neq",
15272
+ "!=": "neq",
15273
+ "anyMatch": "in",
15274
+ "oneOf": "in",
15275
+ "include": "contains",
15276
+ "contains": "contains",
15277
+ "greaterOrEqual": "gte",
15278
+ "gte": "gte",
15279
+ "lessOrEqual": "lte",
15280
+ "lte": "lte",
15281
+ "isNull": "is_empty",
15282
+ "empty": "is_empty",
15283
+ "notNull": "not_empty",
15284
+ "not_empty": "not_empty",
15285
+ }
15286
+ if normalized in mapping:
15287
+ return mapping[normalized]
15288
+ numeric = _coerce_nonnegative_int(judge_type)
15289
+ if numeric is not None:
15290
+ return _public_view_filter_operator_from_judge_type(numeric)
15291
+ return f"judge_type:{judge_type}"
15292
+
15293
+
15294
+ def _public_chart_filter_values_from_rule(rule: dict[str, Any], *, operator: str) -> list[str]:
15295
+ if operator in {"is_empty", "not_empty"}:
15296
+ return []
15297
+ raw_value = rule.get("judgeValue")
15298
+ if raw_value is None and "judgeValues" in rule:
15299
+ raw_value = rule.get("judgeValues")
15300
+ values: list[str] = []
15301
+ if isinstance(raw_value, list):
15302
+ values = [_stringify_condition_value(value).strip() for value in raw_value]
15303
+ elif isinstance(raw_value, str) and "<&&>" in raw_value:
15304
+ values = [value.strip() for value in raw_value.split("<&&>")]
15305
+ elif raw_value is not None:
15306
+ values = [_stringify_condition_value(raw_value).strip()]
15307
+ values = [value for value in values if value]
15308
+ if values:
15309
+ return values
15310
+ for detail_key in ("judgeValueDetailList", "judgeValueDetails"):
15311
+ details = rule.get(detail_key)
15312
+ if not isinstance(details, list):
15313
+ continue
15314
+ for detail in details:
15315
+ if not isinstance(detail, dict):
15316
+ continue
15317
+ for value_key in ("value", "label", "name", "title", "dataValue", "id"):
15318
+ value = _stringify_condition_value(detail.get(value_key)).strip()
15319
+ if value:
15320
+ values.append(value)
15321
+ break
15322
+ return values
15323
+
15324
+
15215
15325
  def _build_public_chart_config_payload(
15216
15326
  *,
15217
15327
  patch: ChartUpsertPatch,
@@ -15421,6 +15531,17 @@ def _extract_chart_identifier(chart: Any) -> str:
15421
15531
  ).strip()
15422
15532
 
15423
15533
 
15534
+ def _chart_type_from_item(chart: Any) -> str:
15535
+ if not isinstance(chart, dict):
15536
+ return ""
15537
+ return _public_chart_type_from_backend(
15538
+ chart.get("chartType")
15539
+ or chart.get("chart_type")
15540
+ or chart.get("chartGraphType")
15541
+ or chart.get("type")
15542
+ )
15543
+
15544
+
15424
15545
  def _normalize_backend_chart_type(value: Any) -> str:
15425
15546
  raw = str(value or "").strip()
15426
15547
  if not raw:
@@ -15537,7 +15658,7 @@ def _portal_component_position_public(
15537
15658
  rows = 2
15538
15659
  elif source_name == "view":
15539
15660
  cols = 24
15540
- rows = 8
15661
+ rows = 11
15541
15662
  elif source_name == "link":
15542
15663
  cols = 12
15543
15664
  rows = 2
@@ -15546,7 +15667,7 @@ def _portal_component_position_public(
15546
15667
  cols = 12
15547
15668
  else:
15548
15669
  cols = 8
15549
- rows = 6
15670
+ rows = 7
15550
15671
  if cols == 24:
15551
15672
  if pc_x != 0:
15552
15673
  pc_y += pc_row_height
@@ -15581,7 +15702,12 @@ def _empty_portal_layout_diagnostics() -> dict[str, Any]:
15581
15702
  }
15582
15703
 
15583
15704
 
15584
- def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: list[dict[str, Any]]) -> dict[str, Any]:
15705
+ def _portal_layout_diagnostics(
15706
+ sections: list[PortalSectionPatch],
15707
+ components: list[dict[str, Any]],
15708
+ *,
15709
+ layout_metadata: list[dict[str, Any]] | None = None,
15710
+ ) -> dict[str, Any]:
15585
15711
  diagnostics = _empty_portal_layout_diagnostics()
15586
15712
  diagnostics["section_count"] = len(sections)
15587
15713
  explicit_count = sum(1 for section in sections if section.position is not None)
@@ -15596,16 +15722,26 @@ def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: l
15596
15722
  if pc:
15597
15723
  pc_positions.append(pc)
15598
15724
  section = sections[index] if index < len(sections) else None
15725
+ metadata = layout_metadata[index] if isinstance(layout_metadata, list) and index < len(layout_metadata) and isinstance(layout_metadata[index], dict) else {}
15599
15726
  source_type = str(getattr(section, "source_type", "") or "").lower() if section is not None else ""
15600
15727
  title = str(getattr(section, "title", "") or "").strip() if section is not None else None
15601
15728
  cols = int(pc.get("cols") or 0)
15602
15729
  rows = int(pc.get("rows") or 0)
15603
- if source_type == "chart" and (cols < 8 or rows < 5):
15730
+ chart_type = str(metadata.get("chart_type") or "").strip().lower()
15731
+ is_metric_chart = chart_type in {"target", "indicator"}
15732
+ min_chart_cols = 6 if is_metric_chart else 8
15733
+ min_chart_rows = 5 if is_metric_chart else 7
15734
+ if source_type == "chart" and (cols < min_chart_cols or rows < min_chart_rows):
15604
15735
  warnings.append(_warning(
15605
15736
  "PORTAL_CHART_CARD_TOO_SMALL",
15606
- "chart portal card is too small; use at least pc.cols >= 8 and pc.rows >= 5, preferably rows >= 6",
15737
+ (
15738
+ "metric chart portal card is too small; use at least pc.cols >= 6 and pc.rows >= 5"
15739
+ if is_metric_chart
15740
+ else "chart portal card is too small; use at least pc.cols >= 8 and pc.rows >= 7 for non-metric charts"
15741
+ ),
15607
15742
  section_index=index,
15608
15743
  title=title,
15744
+ chart_type=chart_type or None,
15609
15745
  pc=deepcopy(pc),
15610
15746
  ))
15611
15747
  if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
@@ -15648,7 +15784,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
15648
15784
  item_id = _extract_chart_identifier(item)
15649
15785
  item_name = str(item.get("chartName") or "").strip()
15650
15786
  if item_id == chart_id:
15651
- return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key}
15787
+ return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key, "chart_type": _chart_type_from_item(item)}
15652
15788
  raise ValueError(f"chart ref chart_id '{chart_id}' could not be resolved under app '{app_key}'")
15653
15789
  matches = _find_charts_by_name(items, chart_name=chart_name)
15654
15790
  if len(matches) > 1:
@@ -15659,6 +15795,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
15659
15795
  "chart_id": _extract_chart_identifier(item),
15660
15796
  "chart_name": str(item.get("chartName") or "").strip(),
15661
15797
  "app_key": app_key,
15798
+ "chart_type": _chart_type_from_item(item),
15662
15799
  }
15663
15800
  raise ValueError(f"chart ref could not be resolved under app '{app_key}'")
15664
15801
 
@@ -24719,7 +24856,7 @@ def _public_view_filter_groups_from_match_rules(
24719
24856
 
24720
24857
 
24721
24858
  def _public_view_filter_operator_from_judge_type(judge_type: Any) -> str:
24722
- normalized = _coerce_positive_int(judge_type)
24859
+ normalized = _coerce_nonnegative_int(judge_type)
24723
24860
  if normalized == JUDGE_EQUAL:
24724
24861
  return "eq"
24725
24862
  if normalized == JUDGE_UNEQUAL: