@josephyan/qingflow-cli 1.1.7 → 1.1.9

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.7
6
+ npm install @josephyan/qingflow-cli@1.1.9
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.1.7 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.9 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.7",
3
+ "version": "1.1.9",
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.7"
7
+ version = "1.1.9"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5999,6 +5999,20 @@ class AiBuilderFacade:
5999
5999
  result["message"] = "read app chart config"
6000
6000
  return result
6001
6001
 
6002
+ def _chart_filter_field_names_by_id(
6003
+ self,
6004
+ *,
6005
+ profile: str,
6006
+ app_key: str,
6007
+ ) -> tuple[dict[str, str], dict[str, Any] | None]:
6008
+ try:
6009
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
6010
+ except (QingflowApiError, RuntimeError) as error:
6011
+ api_error = _coerce_api_error(error)
6012
+ return {}, _transport_error_payload(api_error)
6013
+ fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
6014
+ return _chart_field_names_by_id_from_public_fields(app_key=app_key, fields=fields), None
6015
+
6002
6016
  def app_get_buttons(self, *, profile: str, app_key: str) -> JSONObject:
6003
6017
  self.apps._require_app_key(app_key)
6004
6018
  warnings: list[dict[str, Any]] = []
@@ -6524,6 +6538,8 @@ class AiBuilderFacade:
6524
6538
  )
6525
6539
  charts = _summarize_charts(items)
6526
6540
  chart_visibility_read_errors: list[dict[str, Any]] = []
6541
+ chart_config_read_errors: list[dict[str, Any]] = []
6542
+ field_name_by_id, field_name_read_error = self._chart_filter_field_names_by_id(profile=profile, app_key=resolved_app_key)
6527
6543
  for chart in charts:
6528
6544
  chart_id = str(chart.get("chart_id") or "").strip()
6529
6545
  if not chart_id:
@@ -6546,6 +6562,24 @@ class AiBuilderFacade:
6546
6562
  base_info.get("visibleAuth") if isinstance(base_info, dict) else None
6547
6563
  )
6548
6564
  )
6565
+ try:
6566
+ config_response = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id)
6567
+ config = config_response.get("result") or {}
6568
+ except (QingflowApiError, RuntimeError) as error:
6569
+ api_error = _coerce_api_error(error)
6570
+ chart_config_read_errors.append(
6571
+ {
6572
+ "chart_id": chart_id,
6573
+ "request_id": api_error.request_id,
6574
+ "http_status": api_error.http_status,
6575
+ "backend_code": api_error.backend_code,
6576
+ }
6577
+ )
6578
+ continue
6579
+ if isinstance(config, dict):
6580
+ chart["group_by"] = _public_chart_group_by_from_qingbi_config(config)
6581
+ chart["metrics"] = _public_chart_metrics_from_qingbi_config(config)
6582
+ chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id)
6549
6583
  response = AppChartsReadResponse(
6550
6584
  app_key=resolved_app_key,
6551
6585
  charts=charts,
@@ -6559,7 +6593,11 @@ class AiBuilderFacade:
6559
6593
  "normalized_args": {"app_key": resolved_app_key},
6560
6594
  "missing_fields": [],
6561
6595
  "allowed_values": {},
6562
- "details": {"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {},
6596
+ "details": {
6597
+ **({"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {}),
6598
+ **({"chart_config_read_errors": chart_config_read_errors} if chart_config_read_errors else {}),
6599
+ **({"chart_filter_field_name_read_error": field_name_read_error} if field_name_read_error else {}),
6600
+ },
6563
6601
  "request_id": None,
6564
6602
  "suggested_next_call": None,
6565
6603
  "noop": False,
@@ -6570,14 +6608,26 @@ class AiBuilderFacade:
6570
6608
  if chart_visibility_read_errors
6571
6609
  else []
6572
6610
  )
6611
+ + (
6612
+ [_warning("CHART_CONFIG_READ_PARTIAL", "some chart configs could not be read back; metrics/group_by/filters are incomplete for those charts")]
6613
+ if chart_config_read_errors
6614
+ else []
6615
+ )
6616
+ + (
6617
+ [_warning("CHART_FILTER_FIELD_NAMES_UNRESOLVED", "chart configs were read, but form fields could not be loaded to resolve filter field names")]
6618
+ if field_name_read_error
6619
+ else []
6620
+ )
6573
6621
  ),
6574
6622
  "verification": {
6575
6623
  "app_exists": True,
6576
6624
  "chart_order_verified": list_source == "sorted",
6577
6625
  "chart_list_source": list_source,
6578
6626
  "chart_visibility_readback_complete": not chart_visibility_read_errors,
6627
+ "chart_config_readback_complete": not chart_config_read_errors,
6628
+ "chart_filter_field_names_resolved": not field_name_read_error,
6579
6629
  },
6580
- "verified": True,
6630
+ "verified": not chart_config_read_errors and not field_name_read_error,
6581
6631
  **response.model_dump(mode="json"),
6582
6632
  }
6583
6633
 
@@ -7775,11 +7825,24 @@ class AiBuilderFacade:
7775
7825
  suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
7776
7826
  )
7777
7827
 
7828
+ field_name_by_id: dict[str, str] = {}
7829
+ data_source = config.get("dataSource") if isinstance(config.get("dataSource"), dict) else {}
7830
+ data_source_app_key = str(data_source.get("dataSourceId") or config.get("dataSourceId") or "").strip()
7831
+ if data_source_app_key:
7832
+ field_name_by_id, field_name_error = self._chart_filter_field_names_by_id(profile=profile, app_key=data_source_app_key)
7833
+ if field_name_error:
7834
+ warnings.append(
7835
+ _warning(
7836
+ "CHART_FILTER_FIELD_NAMES_UNRESOLVED",
7837
+ "chart config was read, but form fields could not be loaded to resolve filter field names",
7838
+ **field_name_error,
7839
+ )
7840
+ )
7778
7841
  response = ChartGetResponse(
7779
7842
  chart_id=chart_id,
7780
7843
  base=deepcopy(base) if isinstance(base, dict) else {},
7781
7844
  visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
7782
- filters=_public_chart_filter_groups_from_qingbi_config(config) if isinstance(config, dict) else [],
7845
+ filters=_public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id) if isinstance(config, dict) else [],
7783
7846
  group_by=_public_chart_group_by_from_qingbi_config(config) if isinstance(config, dict) else [],
7784
7847
  metrics=_public_chart_metrics_from_qingbi_config(config) if isinstance(config, dict) else [],
7785
7848
  config=deepcopy(config) if isinstance(config, dict) else {},
@@ -16288,11 +16351,38 @@ def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any]
16288
16351
  reject(value)
16289
16352
 
16290
16353
 
16291
- def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> list[list[dict[str, Any]]]:
16354
+ def _chart_field_names_by_id_from_public_fields(*, app_key: str, fields: list[dict[str, Any]]) -> dict[str, str]:
16355
+ field_name_by_id: dict[str, str] = {}
16356
+ for field in fields:
16357
+ if not isinstance(field, dict):
16358
+ continue
16359
+ name = str(field.get("name") or "").strip()
16360
+ if not name:
16361
+ continue
16362
+ que_id = field.get("que_id")
16363
+ field_id = str(field.get("field_id") or "").strip()
16364
+ for raw_key in (
16365
+ que_id,
16366
+ field_id,
16367
+ f"{app_key}:{que_id}" if que_id is not None else None,
16368
+ f"{app_key}:{field_id}" if field_id else None,
16369
+ ):
16370
+ key = str(raw_key or "").strip()
16371
+ if key:
16372
+ field_name_by_id.setdefault(key, name)
16373
+ return field_name_by_id
16374
+
16375
+
16376
+ def _public_chart_filter_groups_from_qingbi_config(
16377
+ config: dict[str, Any],
16378
+ *,
16379
+ field_name_by_id: dict[str, str] | None = None,
16380
+ ) -> list[list[dict[str, Any]]]:
16292
16381
  groups: list[list[dict[str, Any]]] = []
16293
16382
  raw_groups = config.get("beforeAggregationFilterMatrix")
16294
16383
  if not isinstance(raw_groups, list):
16295
16384
  return groups
16385
+ resolved_field_name_by_id = field_name_by_id or {}
16296
16386
  for raw_group in raw_groups:
16297
16387
  if not isinstance(raw_group, list):
16298
16388
  continue
@@ -16302,19 +16392,26 @@ def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> li
16302
16392
  continue
16303
16393
  operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
16304
16394
  field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
16305
- field_name = (
16395
+ field_id_text = _stringify_condition_value(field_id).strip() if field_id is not None else ""
16396
+ raw_field_name = (
16306
16397
  raw_rule.get("fieldName")
16307
16398
  or raw_rule.get("field_name")
16308
16399
  or raw_rule.get("queTitle")
16309
16400
  or raw_rule.get("title")
16310
- or field_id
16401
+ )
16402
+ raw_field_name_text = _stringify_condition_value(raw_field_name).strip()
16403
+ field_name = (
16404
+ resolved_field_name_by_id.get(raw_field_name_text)
16405
+ or resolved_field_name_by_id.get(field_id_text)
16406
+ or raw_field_name_text
16407
+ or field_id_text
16311
16408
  )
16312
16409
  public_rule: dict[str, Any] = {
16313
- "field_name": _stringify_condition_value(field_name).strip(),
16410
+ "field_name": field_name,
16314
16411
  "operator": operator,
16315
16412
  }
16316
16413
  if field_id is not None:
16317
- public_rule["field_id"] = _stringify_condition_value(field_id).strip()
16414
+ public_rule["field_id"] = field_id_text
16318
16415
  values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
16319
16416
  if values:
16320
16417
  public_rule["values"] = values
@@ -23330,6 +23427,24 @@ def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
23330
23427
  "visible": visible,
23331
23428
  "display_order": display_order if display_order is not None else fallback_order,
23332
23429
  }
23430
+ option_details: list[dict[str, Any]] = []
23431
+ option_values: list[str] = []
23432
+ raw_options = item.get("options")
23433
+ if isinstance(raw_options, list):
23434
+ for raw_option in raw_options:
23435
+ if not isinstance(raw_option, dict):
23436
+ continue
23437
+ option_id = raw_option.get("optId")
23438
+ option_value = str(raw_option.get("optValue") or "").strip()
23439
+ if not option_value and option_id is None:
23440
+ continue
23441
+ if option_value:
23442
+ option_values.append(option_value)
23443
+ option_details.append({"id": option_id, "value": option_value or str(option_id)})
23444
+ if option_values:
23445
+ entry["options"] = option_values
23446
+ if option_details:
23447
+ entry["option_details"] = option_details
23333
23448
  width = _coerce_positive_int(item.get("width"))
23334
23449
  if width is not None:
23335
23450
  entry["width"] = width
@@ -23348,7 +23463,14 @@ def _view_field_lookup_from_question_entries(entries: list[dict[str, Any]]) -> d
23348
23463
  que_id = _coerce_positive_int(entry.get("field_id") or entry.get("que_id") or entry.get("queId"))
23349
23464
  if not name or que_id is None:
23350
23465
  continue
23351
- fields_by_name.setdefault(name, {"name": name, "que_id": que_id})
23466
+ field_entry: dict[str, Any] = {"name": name, "que_id": que_id}
23467
+ options = entry.get("options")
23468
+ if isinstance(options, list) and options:
23469
+ field_entry["options"] = [str(value) for value in options if str(value or "").strip()]
23470
+ option_details = entry.get("option_details")
23471
+ if isinstance(option_details, list) and option_details:
23472
+ field_entry["option_details"] = [deepcopy(value) for value in option_details if isinstance(value, dict)]
23473
+ fields_by_name.setdefault(name, field_entry)
23352
23474
  return fields_by_name
23353
23475
 
23354
23476
 
@@ -26464,6 +26586,38 @@ def _view_filter_rule_values_for_signature(rule: dict[str, Any]) -> list[str]:
26464
26586
  return fallback_values
26465
26587
 
26466
26588
 
26589
+ def _view_filter_option_value_by_id(field: dict[str, Any]) -> dict[str, str]:
26590
+ value_by_id: dict[str, str] = {}
26591
+ for detail in field.get("option_details") or []:
26592
+ if not isinstance(detail, dict):
26593
+ continue
26594
+ option_id = detail.get("id")
26595
+ option_value = str(detail.get("value") or "").strip()
26596
+ if option_id is None or not option_value:
26597
+ continue
26598
+ value_by_id[str(option_id)] = option_value
26599
+ return value_by_id
26600
+
26601
+
26602
+ def _public_view_filter_rule_values(rule: dict[str, Any], *, field: dict[str, Any]) -> list[str]:
26603
+ value_by_id = _view_filter_option_value_by_id(field)
26604
+ detail_value_by_id: dict[str, str] = {}
26605
+ ordered_detail_values: list[str] = []
26606
+ for detail in rule.get("judgeValueDetails") or []:
26607
+ if not isinstance(detail, dict):
26608
+ continue
26609
+ detail_value = str(detail.get("value") or "").strip()
26610
+ detail_id = detail.get("id")
26611
+ if detail_value:
26612
+ ordered_detail_values.append(detail_value)
26613
+ if detail_id is not None:
26614
+ detail_value_by_id[str(detail_id)] = detail_value
26615
+ values = [str(value) for value in (rule.get("judgeValues") or []) if str(value or "").strip()]
26616
+ if values:
26617
+ return [value_by_id.get(value) or detail_value_by_id.get(value) or value for value in values]
26618
+ return ordered_detail_values
26619
+
26620
+
26467
26621
  def _view_filter_groups_signature(groups: Any) -> list[list[dict[str, Any]]]:
26468
26622
  signature: list[list[dict[str, Any]]] = []
26469
26623
  for group in _normalize_view_filter_groups_for_compare(groups):
@@ -26545,7 +26699,7 @@ def _public_view_filter_groups_from_match_rules(
26545
26699
  for rule in group:
26546
26700
  que_id = _coerce_positive_int(rule.get("queId")) or 0
26547
26701
  field = fields_by_que_id.get(que_id) or {}
26548
- values = _view_filter_rule_values_for_signature(rule)
26702
+ values = _public_view_filter_rule_values(rule, field=field)
26549
26703
  public_rule: dict[str, Any] = {
26550
26704
  "field_name": str(field.get("name") or rule.get("queTitle") or que_id),
26551
26705
  "operator": _public_view_filter_operator_from_judge_type(rule.get("judgeType")),