@josephyan/qingflow-cli 1.1.6 → 1.1.8

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 @josephyan/qingflow-cli@1.1.6
6
+ npm install @josephyan/qingflow-cli@1.1.8
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.1.6 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.8 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
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.1.6"
7
+ version = "1.1.8"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -6524,6 +6524,7 @@ class AiBuilderFacade:
6524
6524
  )
6525
6525
  charts = _summarize_charts(items)
6526
6526
  chart_visibility_read_errors: list[dict[str, Any]] = []
6527
+ chart_config_read_errors: list[dict[str, Any]] = []
6527
6528
  for chart in charts:
6528
6529
  chart_id = str(chart.get("chart_id") or "").strip()
6529
6530
  if not chart_id:
@@ -6546,6 +6547,24 @@ class AiBuilderFacade:
6546
6547
  base_info.get("visibleAuth") if isinstance(base_info, dict) else None
6547
6548
  )
6548
6549
  )
6550
+ try:
6551
+ config_response = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id)
6552
+ config = config_response.get("result") or {}
6553
+ except (QingflowApiError, RuntimeError) as error:
6554
+ api_error = _coerce_api_error(error)
6555
+ chart_config_read_errors.append(
6556
+ {
6557
+ "chart_id": chart_id,
6558
+ "request_id": api_error.request_id,
6559
+ "http_status": api_error.http_status,
6560
+ "backend_code": api_error.backend_code,
6561
+ }
6562
+ )
6563
+ continue
6564
+ if isinstance(config, dict):
6565
+ chart["group_by"] = _public_chart_group_by_from_qingbi_config(config)
6566
+ chart["metrics"] = _public_chart_metrics_from_qingbi_config(config)
6567
+ chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config)
6549
6568
  response = AppChartsReadResponse(
6550
6569
  app_key=resolved_app_key,
6551
6570
  charts=charts,
@@ -6559,7 +6578,10 @@ class AiBuilderFacade:
6559
6578
  "normalized_args": {"app_key": resolved_app_key},
6560
6579
  "missing_fields": [],
6561
6580
  "allowed_values": {},
6562
- "details": {"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {},
6581
+ "details": {
6582
+ **({"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {}),
6583
+ **({"chart_config_read_errors": chart_config_read_errors} if chart_config_read_errors else {}),
6584
+ },
6563
6585
  "request_id": None,
6564
6586
  "suggested_next_call": None,
6565
6587
  "noop": False,
@@ -6570,14 +6592,20 @@ class AiBuilderFacade:
6570
6592
  if chart_visibility_read_errors
6571
6593
  else []
6572
6594
  )
6595
+ + (
6596
+ [_warning("CHART_CONFIG_READ_PARTIAL", "some chart configs could not be read back; metrics/group_by/filters are incomplete for those charts")]
6597
+ if chart_config_read_errors
6598
+ else []
6599
+ )
6573
6600
  ),
6574
6601
  "verification": {
6575
6602
  "app_exists": True,
6576
6603
  "chart_order_verified": list_source == "sorted",
6577
6604
  "chart_list_source": list_source,
6578
6605
  "chart_visibility_readback_complete": not chart_visibility_read_errors,
6606
+ "chart_config_readback_complete": not chart_config_read_errors,
6579
6607
  },
6580
- "verified": True,
6608
+ "verified": not chart_config_read_errors,
6581
6609
  **response.model_dump(mode="json"),
6582
6610
  }
6583
6611
 
@@ -23264,6 +23292,15 @@ def _merge_view_summary_with_config(
23264
23292
  question_entries_by_id=query_question_entries_by_id,
23265
23293
  )
23266
23294
  config_enriched = True
23295
+ if "viewgraphLimit" in config:
23296
+ field_lookup = _view_field_lookup_from_question_entries(
23297
+ [*question_entries, *canonical_question_entries]
23298
+ )
23299
+ summary["filters"] = _public_view_filter_groups_from_match_rules(
23300
+ config.get("viewgraphLimit"),
23301
+ current_fields_by_name=field_lookup,
23302
+ )
23303
+ config_enriched = True
23267
23304
  if any(key in config for key in ("asosChartVisible", "asosChartConfig", "asosChartIdList", "limitType")):
23268
23305
  summary["associated_resources_config"] = _extract_view_associated_resources_config(config)
23269
23306
  config_enriched = True
@@ -23321,6 +23358,24 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
23321
23358
  "visible": visible,
23322
23359
  "display_order": display_order if display_order is not None else fallback_order,
23323
23360
  }
23361
+ option_details: list[dict[str, Any]] = []
23362
+ option_values: list[str] = []
23363
+ raw_options = item.get("options")
23364
+ if isinstance(raw_options, list):
23365
+ for raw_option in raw_options:
23366
+ if not isinstance(raw_option, dict):
23367
+ continue
23368
+ option_id = raw_option.get("optId")
23369
+ option_value = str(raw_option.get("optValue") or "").strip()
23370
+ if not option_value and option_id is None:
23371
+ continue
23372
+ if option_value:
23373
+ option_values.append(option_value)
23374
+ option_details.append({"id": option_id, "value": option_value or str(option_id)})
23375
+ if option_values:
23376
+ entry["options"] = option_values
23377
+ if option_details:
23378
+ entry["option_details"] = option_details
23324
23379
  width = _coerce_positive_int(item.get("width"))
23325
23380
  if width is not None:
23326
23381
  entry["width"] = width
@@ -23330,6 +23385,43 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
23330
23385
  return entries
23331
23386
 
23332
23387
 
23388
+ def _view_field_lookup_from_question_entries(entries: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
23389
+ fields_by_name: dict[str, dict[str, Any]] = {}
23390
+ for entry in entries:
23391
+ if not isinstance(entry, dict):
23392
+ continue
23393
+ name = str(entry.get("name") or entry.get("title") or "").strip()
23394
+ que_id = _coerce_positive_int(entry.get("field_id") or entry.get("que_id") or entry.get("queId"))
23395
+ if not name or que_id is None:
23396
+ continue
23397
+ field_entry: dict[str, Any] = {"name": name, "que_id": que_id}
23398
+ options = entry.get("options")
23399
+ if isinstance(options, list) and options:
23400
+ field_entry["options"] = [str(value) for value in options if str(value or "").strip()]
23401
+ option_details = entry.get("option_details")
23402
+ if isinstance(option_details, list) and option_details:
23403
+ field_entry["option_details"] = [deepcopy(value) for value in option_details if isinstance(value, dict)]
23404
+ fields_by_name.setdefault(name, field_entry)
23405
+ return fields_by_name
23406
+
23407
+
23408
+ def _view_field_lookup_from_summary(view: dict[str, Any]) -> dict[str, dict[str, Any]]:
23409
+ entries = view.get("column_details")
23410
+ if isinstance(entries, list):
23411
+ return _view_field_lookup_from_question_entries([entry for entry in entries if isinstance(entry, dict)])
23412
+ fields_by_name: dict[str, dict[str, Any]] = {}
23413
+ names = view.get("columns")
23414
+ ids = view.get("display_column_ids") or view.get("configured_column_ids") or []
23415
+ if isinstance(names, list):
23416
+ for index, raw_name in enumerate(names):
23417
+ name = str(raw_name or "").strip()
23418
+ if not name:
23419
+ continue
23420
+ que_id = _coerce_positive_int(ids[index] if isinstance(ids, list) and index < len(ids) else None)
23421
+ fields_by_name.setdefault(name, {"name": name, "que_id": que_id})
23422
+ return fields_by_name
23423
+
23424
+
23333
23425
  def _filter_public_view_display_entries(
23334
23426
  entries: list[dict[str, Any]],
23335
23427
  *,
@@ -24146,6 +24238,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
24146
24238
  raw_buttons = view.get("buttons")
24147
24239
  if not isinstance(raw_buttons, list):
24148
24240
  continue
24241
+ field_lookup = _view_field_lookup_from_summary(view)
24149
24242
  buttons: list[dict[str, Any]] = []
24150
24243
  for entry in raw_buttons:
24151
24244
  if not isinstance(entry, dict):
@@ -24153,6 +24246,12 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
24153
24246
  if _normalize_view_button_type(entry.get("button_type")) != PublicViewButtonType.custom.value:
24154
24247
  continue
24155
24248
  placement = _public_view_button_placement(entry.get("config_type")) or entry.get("placement")
24249
+ raw_button_limit = deepcopy(entry.get("button_limit") or [])
24250
+ public_button_limit = (
24251
+ _public_view_filter_groups_from_match_rules(raw_button_limit, current_fields_by_name=field_lookup)
24252
+ if raw_button_limit
24253
+ else []
24254
+ )
24156
24255
  button = _compact_dict(
24157
24256
  {
24158
24257
  "button_ref": _coerce_positive_int(entry.get("button_id")),
@@ -24160,7 +24259,7 @@ def _custom_button_view_configs_from_view_summaries(views: list[dict[str, Any]])
24160
24259
  "button_text": entry.get("button_text"),
24161
24260
  "placement": placement,
24162
24261
  "primary": bool(entry.get("being_main", False)),
24163
- "button_limit": deepcopy(entry.get("button_limit") or []),
24262
+ "button_limit": public_button_limit,
24164
24263
  "button_formula": entry.get("button_formula"),
24165
24264
  "button_formula_type": _coerce_positive_int(entry.get("button_formula_type")),
24166
24265
  "print_tpls": deepcopy(entry.get("print_tpls") or []),
@@ -26418,6 +26517,38 @@ def _view_filter_rule_values_for_signature(rule: dict[str, Any]) -> list[str]:
26418
26517
  return fallback_values
26419
26518
 
26420
26519
 
26520
+ def _view_filter_option_value_by_id(field: dict[str, Any]) -> dict[str, str]:
26521
+ value_by_id: dict[str, str] = {}
26522
+ for detail in field.get("option_details") or []:
26523
+ if not isinstance(detail, dict):
26524
+ continue
26525
+ option_id = detail.get("id")
26526
+ option_value = str(detail.get("value") or "").strip()
26527
+ if option_id is None or not option_value:
26528
+ continue
26529
+ value_by_id[str(option_id)] = option_value
26530
+ return value_by_id
26531
+
26532
+
26533
+ def _public_view_filter_rule_values(rule: dict[str, Any], *, field: dict[str, Any]) -> list[str]:
26534
+ value_by_id = _view_filter_option_value_by_id(field)
26535
+ detail_value_by_id: dict[str, str] = {}
26536
+ ordered_detail_values: list[str] = []
26537
+ for detail in rule.get("judgeValueDetails") or []:
26538
+ if not isinstance(detail, dict):
26539
+ continue
26540
+ detail_value = str(detail.get("value") or "").strip()
26541
+ detail_id = detail.get("id")
26542
+ if detail_value:
26543
+ ordered_detail_values.append(detail_value)
26544
+ if detail_id is not None:
26545
+ detail_value_by_id[str(detail_id)] = detail_value
26546
+ values = [str(value) for value in (rule.get("judgeValues") or []) if str(value or "").strip()]
26547
+ if values:
26548
+ return [value_by_id.get(value) or detail_value_by_id.get(value) or value for value in values]
26549
+ return ordered_detail_values
26550
+
26551
+
26421
26552
  def _view_filter_groups_signature(groups: Any) -> list[list[dict[str, Any]]]:
26422
26553
  signature: list[list[dict[str, Any]]] = []
26423
26554
  for group in _normalize_view_filter_groups_for_compare(groups):
@@ -26499,7 +26630,7 @@ def _public_view_filter_groups_from_match_rules(
26499
26630
  for rule in group:
26500
26631
  que_id = _coerce_positive_int(rule.get("queId")) or 0
26501
26632
  field = fields_by_que_id.get(que_id) or {}
26502
- values = _view_filter_rule_values_for_signature(rule)
26633
+ values = _public_view_filter_rule_values(rule, field=field)
26503
26634
  public_rule: dict[str, Any] = {
26504
26635
  "field_name": str(field.get("name") or rule.get("queTitle") or que_id),
26505
26636
  "operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType")),