@qingflow-tech/qingflow-app-builder-mcp 1.0.37 → 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.37
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.37 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.37",
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.37"
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 {
@@ -11378,11 +11379,14 @@ class AiBuilderFacade:
11378
11379
 
11379
11380
  if failed_items:
11380
11381
  successful_changes = bool(created_ids or updated_ids or removed_ids or reordered)
11382
+ primary_failure = failed_items[0]
11383
+ primary_error_code = str(primary_failure.get("error_code") or "").strip()
11384
+ primary_message = str(primary_failure.get("message") or "").strip()
11381
11385
  return finalize({
11382
11386
  "status": "partial_success" if successful_changes else "failed",
11383
- "error_code": "CHART_APPLY_PARTIAL" if successful_changes else "CHART_APPLY_FAILED",
11387
+ "error_code": "CHART_APPLY_PARTIAL" if successful_changes else primary_error_code or "CHART_APPLY_FAILED",
11384
11388
  "recoverable": True,
11385
- "message": "applied some chart operations; at least one chart operation failed" if successful_changes else "one or more chart operations failed",
11389
+ "message": "applied some chart operations; at least one chart operation failed" if successful_changes else primary_message or "one or more chart operations failed",
11386
11390
  "normalized_args": normalized_args,
11387
11391
  "missing_fields": [],
11388
11392
  "allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
@@ -11609,12 +11613,12 @@ class AiBuilderFacade:
11609
11613
  base_payload=base_payload,
11610
11614
  )
11611
11615
  if sections_requested:
11612
- component_payload = self._build_portal_components_from_sections(
11616
+ component_payload, layout_metadata = self._build_portal_components_from_sections(
11613
11617
  profile=profile,
11614
11618
  sections=request.sections,
11615
11619
  layout_preset=request.layout_preset,
11616
11620
  )
11617
- layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload)
11621
+ layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
11618
11622
  update_payload["components"] = component_payload
11619
11623
  self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
11620
11624
  write_executed = True
@@ -12560,8 +12564,9 @@ class AiBuilderFacade:
12560
12564
  profile: str,
12561
12565
  sections: list[PortalSectionPatch],
12562
12566
  layout_preset: str | None = None,
12563
- ) -> list[dict[str, Any]]:
12567
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
12564
12568
  resolved_components: list[dict[str, Any]] = []
12569
+ layout_metadata: list[dict[str, Any]] = []
12565
12570
  pc_x = 0
12566
12571
  pc_y = 0
12567
12572
  pc_row_height = 0
@@ -12593,6 +12598,11 @@ class AiBuilderFacade:
12593
12598
  profile=profile,
12594
12599
  ref=section.chart_ref,
12595
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()
12596
12606
  chart_config = {
12597
12607
  "biChartId": resolved_chart["chart_id"],
12598
12608
  "chartComponentTitle": section.title,
@@ -12600,6 +12610,7 @@ class AiBuilderFacade:
12600
12610
  **deepcopy(section.config),
12601
12611
  }
12602
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})
12603
12614
  elif section.source_type == "view":
12604
12615
  resolved_view = _resolve_view_reference(
12605
12616
  facade=self,
@@ -12622,26 +12633,31 @@ class AiBuilderFacade:
12622
12633
  **deepcopy(section.config),
12623
12634
  }
12624
12635
  component = {"type": 10, "position": position_payload, "viewgraphConfig": _compact_dict(view_config)}
12636
+ layout_metadata.append({"source_type": section.source_type})
12625
12637
  elif section.source_type == "grid":
12626
12638
  component = {
12627
12639
  "type": 2,
12628
12640
  "position": position_payload,
12629
12641
  "gridConfig": _compact_dict({"gridTitle": section.title, "beingShowTitle": True, **deepcopy(section.config)}),
12630
12642
  }
12643
+ layout_metadata.append({"source_type": section.source_type})
12631
12644
  elif section.source_type == "filter":
12632
12645
  component = {"type": 6, "position": position_payload, "filterConfig": deepcopy(section.config)}
12646
+ layout_metadata.append({"source_type": section.source_type})
12633
12647
  elif section.source_type == "text":
12634
12648
  component = {"type": 5, "position": position_payload, "textConfig": {"text": section.text or "", **deepcopy(section.config)}}
12649
+ layout_metadata.append({"source_type": section.source_type})
12635
12650
  else:
12636
12651
  component = {
12637
12652
  "type": 4,
12638
12653
  "position": position_payload,
12639
12654
  "linkConfig": {"url": section.url or "", "beingLoginAuth": False, **deepcopy(section.config)},
12640
12655
  }
12656
+ layout_metadata.append({"source_type": section.source_type})
12641
12657
  if dash_style is not None:
12642
12658
  component["dashStyleConfigBO"] = dash_style
12643
12659
  resolved_components.append(component)
12644
- return resolved_components
12660
+ return resolved_components, layout_metadata
12645
12661
 
12646
12662
  def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
12647
12663
  session_profile = self.apps.sessions.get_profile(profile)
@@ -15209,6 +15225,103 @@ def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any]
15209
15225
  reject(value)
15210
15226
 
15211
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
+
15212
15325
  def _build_public_chart_config_payload(
15213
15326
  *,
15214
15327
  patch: ChartUpsertPatch,
@@ -15418,6 +15531,17 @@ def _extract_chart_identifier(chart: Any) -> str:
15418
15531
  ).strip()
15419
15532
 
15420
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
+
15421
15545
  def _normalize_backend_chart_type(value: Any) -> str:
15422
15546
  raw = str(value or "").strip()
15423
15547
  if not raw:
@@ -15534,7 +15658,7 @@ def _portal_component_position_public(
15534
15658
  rows = 2
15535
15659
  elif source_name == "view":
15536
15660
  cols = 24
15537
- rows = 8
15661
+ rows = 11
15538
15662
  elif source_name == "link":
15539
15663
  cols = 12
15540
15664
  rows = 2
@@ -15543,7 +15667,7 @@ def _portal_component_position_public(
15543
15667
  cols = 12
15544
15668
  else:
15545
15669
  cols = 8
15546
- rows = 6
15670
+ rows = 7
15547
15671
  if cols == 24:
15548
15672
  if pc_x != 0:
15549
15673
  pc_y += pc_row_height
@@ -15578,7 +15702,12 @@ def _empty_portal_layout_diagnostics() -> dict[str, Any]:
15578
15702
  }
15579
15703
 
15580
15704
 
15581
- 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]:
15582
15711
  diagnostics = _empty_portal_layout_diagnostics()
15583
15712
  diagnostics["section_count"] = len(sections)
15584
15713
  explicit_count = sum(1 for section in sections if section.position is not None)
@@ -15593,16 +15722,26 @@ def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: l
15593
15722
  if pc:
15594
15723
  pc_positions.append(pc)
15595
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 {}
15596
15726
  source_type = str(getattr(section, "source_type", "") or "").lower() if section is not None else ""
15597
15727
  title = str(getattr(section, "title", "") or "").strip() if section is not None else None
15598
15728
  cols = int(pc.get("cols") or 0)
15599
15729
  rows = int(pc.get("rows") or 0)
15600
- 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):
15601
15735
  warnings.append(_warning(
15602
15736
  "PORTAL_CHART_CARD_TOO_SMALL",
15603
- "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
+ ),
15604
15742
  section_index=index,
15605
15743
  title=title,
15744
+ chart_type=chart_type or None,
15606
15745
  pc=deepcopy(pc),
15607
15746
  ))
15608
15747
  if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
@@ -15645,7 +15784,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
15645
15784
  item_id = _extract_chart_identifier(item)
15646
15785
  item_name = str(item.get("chartName") or "").strip()
15647
15786
  if item_id == chart_id:
15648
- 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)}
15649
15788
  raise ValueError(f"chart ref chart_id '{chart_id}' could not be resolved under app '{app_key}'")
15650
15789
  matches = _find_charts_by_name(items, chart_name=chart_name)
15651
15790
  if len(matches) > 1:
@@ -15656,6 +15795,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
15656
15795
  "chart_id": _extract_chart_identifier(item),
15657
15796
  "chart_name": str(item.get("chartName") or "").strip(),
15658
15797
  "app_key": app_key,
15798
+ "chart_type": _chart_type_from_item(item),
15659
15799
  }
15660
15800
  raise ValueError(f"chart ref could not be resolved under app '{app_key}'")
15661
15801
 
@@ -24716,7 +24856,7 @@ def _public_view_filter_groups_from_match_rules(
24716
24856
 
24717
24857
 
24718
24858
  def _public_view_filter_operator_from_judge_type(judge_type: Any) -> str:
24719
- normalized = _coerce_positive_int(judge_type)
24859
+ normalized = _coerce_nonnegative_int(judge_type)
24720
24860
  if normalized == JUDGE_EQUAL:
24721
24861
  return "eq"
24722
24862
  if normalized == JUDGE_UNEQUAL: