@josephyan/qingflow-cli 1.1.8 → 1.1.10

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.8
6
+ npm install @josephyan/qingflow-cli@1.1.10
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.1.8 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.10 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.8",
3
+ "version": "1.1.10",
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.8"
7
+ version = "1.1.10"
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]] = []
@@ -6065,6 +6079,21 @@ class AiBuilderFacade:
6065
6079
  details={"app_key": app_key},
6066
6080
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
6067
6081
  )
6082
+ match_mapping_errors = self._enrich_associated_resource_match_mappings(
6083
+ profile=profile,
6084
+ app_key=app_key,
6085
+ resources=resources,
6086
+ )
6087
+ if match_mapping_errors:
6088
+ verification["match_mappings_loaded"] = False
6089
+ warnings.append(
6090
+ _warning(
6091
+ "ASSOCIATED_RESOURCE_MATCH_MAPPINGS_PARTIAL",
6092
+ "associated resources were read, but some raw match rules could not be converted to semantic match_mappings",
6093
+ )
6094
+ )
6095
+ else:
6096
+ verification["match_mappings_loaded"] = True
6068
6097
  view_configs: list[dict[str, Any]] = []
6069
6098
  view_config_read_errors: list[dict[str, Any]] = []
6070
6099
  try:
@@ -6097,7 +6126,10 @@ class AiBuilderFacade:
6097
6126
  "view_config_count": len(view_configs),
6098
6127
  "warnings": warnings,
6099
6128
  "verification": verification,
6100
- "details": {"view_config_read_errors": view_config_read_errors} if view_config_read_errors else {},
6129
+ "details": {
6130
+ **({"view_config_read_errors": view_config_read_errors} if view_config_read_errors else {}),
6131
+ **({"match_mapping_errors": match_mapping_errors} if match_mapping_errors else {}),
6132
+ },
6101
6133
  }
6102
6134
 
6103
6135
  def flow_patch_nodes(
@@ -6525,6 +6557,7 @@ class AiBuilderFacade:
6525
6557
  charts = _summarize_charts(items)
6526
6558
  chart_visibility_read_errors: list[dict[str, Any]] = []
6527
6559
  chart_config_read_errors: list[dict[str, Any]] = []
6560
+ field_name_by_id, field_name_read_error = self._chart_filter_field_names_by_id(profile=profile, app_key=resolved_app_key)
6528
6561
  for chart in charts:
6529
6562
  chart_id = str(chart.get("chart_id") or "").strip()
6530
6563
  if not chart_id:
@@ -6564,7 +6597,7 @@ class AiBuilderFacade:
6564
6597
  if isinstance(config, dict):
6565
6598
  chart["group_by"] = _public_chart_group_by_from_qingbi_config(config)
6566
6599
  chart["metrics"] = _public_chart_metrics_from_qingbi_config(config)
6567
- chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config)
6600
+ chart["filters"] = _public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id)
6568
6601
  response = AppChartsReadResponse(
6569
6602
  app_key=resolved_app_key,
6570
6603
  charts=charts,
@@ -6581,6 +6614,7 @@ class AiBuilderFacade:
6581
6614
  "details": {
6582
6615
  **({"chart_visibility_read_errors": chart_visibility_read_errors} if chart_visibility_read_errors else {}),
6583
6616
  **({"chart_config_read_errors": chart_config_read_errors} if chart_config_read_errors else {}),
6617
+ **({"chart_filter_field_name_read_error": field_name_read_error} if field_name_read_error else {}),
6584
6618
  },
6585
6619
  "request_id": None,
6586
6620
  "suggested_next_call": None,
@@ -6597,6 +6631,11 @@ class AiBuilderFacade:
6597
6631
  if chart_config_read_errors
6598
6632
  else []
6599
6633
  )
6634
+ + (
6635
+ [_warning("CHART_FILTER_FIELD_NAMES_UNRESOLVED", "chart configs were read, but form fields could not be loaded to resolve filter field names")]
6636
+ if field_name_read_error
6637
+ else []
6638
+ )
6600
6639
  ),
6601
6640
  "verification": {
6602
6641
  "app_exists": True,
@@ -6604,8 +6643,9 @@ class AiBuilderFacade:
6604
6643
  "chart_list_source": list_source,
6605
6644
  "chart_visibility_readback_complete": not chart_visibility_read_errors,
6606
6645
  "chart_config_readback_complete": not chart_config_read_errors,
6646
+ "chart_filter_field_names_resolved": not field_name_read_error,
6607
6647
  },
6608
- "verified": not chart_config_read_errors,
6648
+ "verified": not chart_config_read_errors and not field_name_read_error,
6609
6649
  **response.model_dump(mode="json"),
6610
6650
  }
6611
6651
 
@@ -6755,6 +6795,64 @@ class AiBuilderFacade:
6755
6795
 
6756
6796
  return self.apps._run(profile, runner)
6757
6797
 
6798
+ def _match_field_index_for_app(self, *, profile: str, app_key: str) -> dict[int, dict[str, Any]]:
6799
+ state = self._load_base_schema_state(profile=profile, app_key=app_key)
6800
+ fields = cast(list[dict[str, Any]], state["parsed"]["fields"])
6801
+ indexed: dict[int, dict[str, Any]] = {
6802
+ -17: {"name": "数据ID", "que_id": -17, "type": "system"},
6803
+ 0: {"name": "编号", "que_id": 0, "type": "system"},
6804
+ }
6805
+ for field in fields:
6806
+ que_id = _coerce_any_int(field.get("que_id"))
6807
+ if que_id is not None:
6808
+ indexed[que_id] = field
6809
+ return indexed
6810
+
6811
+ def _enrich_associated_resource_match_mappings(
6812
+ self,
6813
+ *,
6814
+ profile: str,
6815
+ app_key: str,
6816
+ resources: list[dict[str, Any]],
6817
+ ) -> list[dict[str, Any]]:
6818
+ if not any(isinstance(resource, dict) and resource.get("match_rules") for resource in resources):
6819
+ return []
6820
+ errors: list[dict[str, Any]] = []
6821
+ source_fields: dict[int, dict[str, Any]] = {}
6822
+ try:
6823
+ source_fields = self._match_field_index_for_app(profile=profile, app_key=app_key)
6824
+ except (QingflowApiError, RuntimeError) as error:
6825
+ errors.append({"app_key": app_key, "resource": "source_fields", "transport_error": _transport_error_payload(_coerce_api_error(error))})
6826
+ target_fields_by_app: dict[str, dict[int, dict[str, Any]]] = {}
6827
+ for resource in resources:
6828
+ if not isinstance(resource, dict):
6829
+ continue
6830
+ raw_rules = resource.get("match_rules")
6831
+ if not raw_rules:
6832
+ continue
6833
+ target_app_key = str(resource.get("target_app_key") or app_key).strip()
6834
+ if target_app_key not in target_fields_by_app:
6835
+ try:
6836
+ target_fields_by_app[target_app_key] = self._match_field_index_for_app(profile=profile, app_key=target_app_key)
6837
+ except (QingflowApiError, RuntimeError) as error:
6838
+ errors.append(
6839
+ {
6840
+ "associated_item_id": resource.get("associated_item_id"),
6841
+ "target_app_key": target_app_key,
6842
+ "resource": "target_fields",
6843
+ "transport_error": _transport_error_payload(_coerce_api_error(error)),
6844
+ }
6845
+ )
6846
+ target_fields_by_app[target_app_key] = {}
6847
+ mappings = _public_associated_resource_match_mappings_from_rules(
6848
+ raw_rules if isinstance(raw_rules, list) else [],
6849
+ source_fields=source_fields,
6850
+ target_fields=target_fields_by_app.get(target_app_key, {}),
6851
+ )
6852
+ if mappings:
6853
+ resource["match_mappings"] = mappings
6854
+ return errors
6855
+
6758
6856
  def _associated_resource_create(
6759
6857
  self,
6760
6858
  *,
@@ -7803,11 +7901,24 @@ class AiBuilderFacade:
7803
7901
  suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
7804
7902
  )
7805
7903
 
7904
+ field_name_by_id: dict[str, str] = {}
7905
+ data_source = config.get("dataSource") if isinstance(config.get("dataSource"), dict) else {}
7906
+ data_source_app_key = str(data_source.get("dataSourceId") or config.get("dataSourceId") or "").strip()
7907
+ if data_source_app_key:
7908
+ field_name_by_id, field_name_error = self._chart_filter_field_names_by_id(profile=profile, app_key=data_source_app_key)
7909
+ if field_name_error:
7910
+ warnings.append(
7911
+ _warning(
7912
+ "CHART_FILTER_FIELD_NAMES_UNRESOLVED",
7913
+ "chart config was read, but form fields could not be loaded to resolve filter field names",
7914
+ **field_name_error,
7915
+ )
7916
+ )
7806
7917
  response = ChartGetResponse(
7807
7918
  chart_id=chart_id,
7808
7919
  base=deepcopy(base) if isinstance(base, dict) else {},
7809
7920
  visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
7810
- filters=_public_chart_filter_groups_from_qingbi_config(config) if isinstance(config, dict) else [],
7921
+ filters=_public_chart_filter_groups_from_qingbi_config(config, field_name_by_id=field_name_by_id) if isinstance(config, dict) else [],
7811
7922
  group_by=_public_chart_group_by_from_qingbi_config(config) if isinstance(config, dict) else [],
7812
7923
  metrics=_public_chart_metrics_from_qingbi_config(config) if isinstance(config, dict) else [],
7813
7924
  config=deepcopy(config) if isinstance(config, dict) else {},
@@ -16316,11 +16427,38 @@ def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any]
16316
16427
  reject(value)
16317
16428
 
16318
16429
 
16319
- def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> list[list[dict[str, Any]]]:
16430
+ def _chart_field_names_by_id_from_public_fields(*, app_key: str, fields: list[dict[str, Any]]) -> dict[str, str]:
16431
+ field_name_by_id: dict[str, str] = {}
16432
+ for field in fields:
16433
+ if not isinstance(field, dict):
16434
+ continue
16435
+ name = str(field.get("name") or "").strip()
16436
+ if not name:
16437
+ continue
16438
+ que_id = field.get("que_id")
16439
+ field_id = str(field.get("field_id") or "").strip()
16440
+ for raw_key in (
16441
+ que_id,
16442
+ field_id,
16443
+ f"{app_key}:{que_id}" if que_id is not None else None,
16444
+ f"{app_key}:{field_id}" if field_id else None,
16445
+ ):
16446
+ key = str(raw_key or "").strip()
16447
+ if key:
16448
+ field_name_by_id.setdefault(key, name)
16449
+ return field_name_by_id
16450
+
16451
+
16452
+ def _public_chart_filter_groups_from_qingbi_config(
16453
+ config: dict[str, Any],
16454
+ *,
16455
+ field_name_by_id: dict[str, str] | None = None,
16456
+ ) -> list[list[dict[str, Any]]]:
16320
16457
  groups: list[list[dict[str, Any]]] = []
16321
16458
  raw_groups = config.get("beforeAggregationFilterMatrix")
16322
16459
  if not isinstance(raw_groups, list):
16323
16460
  return groups
16461
+ resolved_field_name_by_id = field_name_by_id or {}
16324
16462
  for raw_group in raw_groups:
16325
16463
  if not isinstance(raw_group, list):
16326
16464
  continue
@@ -16330,19 +16468,26 @@ def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> li
16330
16468
  continue
16331
16469
  operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
16332
16470
  field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
16333
- field_name = (
16471
+ field_id_text = _stringify_condition_value(field_id).strip() if field_id is not None else ""
16472
+ raw_field_name = (
16334
16473
  raw_rule.get("fieldName")
16335
16474
  or raw_rule.get("field_name")
16336
16475
  or raw_rule.get("queTitle")
16337
16476
  or raw_rule.get("title")
16338
- or field_id
16477
+ )
16478
+ raw_field_name_text = _stringify_condition_value(raw_field_name).strip()
16479
+ field_name = (
16480
+ resolved_field_name_by_id.get(raw_field_name_text)
16481
+ or resolved_field_name_by_id.get(field_id_text)
16482
+ or raw_field_name_text
16483
+ or field_id_text
16339
16484
  )
16340
16485
  public_rule: dict[str, Any] = {
16341
- "field_name": _stringify_condition_value(field_name).strip(),
16486
+ "field_name": field_name,
16342
16487
  "operator": operator,
16343
16488
  }
16344
16489
  if field_id is not None:
16345
- public_rule["field_id"] = _stringify_condition_value(field_id).strip()
16490
+ public_rule["field_id"] = field_id_text
16346
16491
  values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
16347
16492
  if values:
16348
16493
  public_rule["values"] = values
@@ -25070,12 +25215,83 @@ def _normalize_associated_resource_item(raw_item: Any, *, include_raw: bool = Fa
25070
25215
  if chart_type is not None:
25071
25216
  item["chart_type"] = _public_chart_type_from_backend(chart_type)
25072
25217
  item["report_source"] = _public_report_source_from_backend_source(source_type)
25218
+ match_rules = _normalize_associated_resource_raw_match_rules(raw_item)
25219
+ if match_rules:
25220
+ item["match_rules"] = match_rules
25073
25221
  item = _compact_dict(item)
25074
25222
  if include_raw:
25075
25223
  item["_raw"] = deepcopy(raw_item)
25076
25224
  return item
25077
25225
 
25078
25226
 
25227
+ def _normalize_associated_resource_raw_match_rules(raw_item: dict[str, Any]) -> list[dict[str, Any]]:
25228
+ raw_match_rules = _first_present(raw_item, "match_rules", "matchRules", "que_relation", "queRelation")
25229
+ if not isinstance(raw_match_rules, list):
25230
+ return []
25231
+ flattened: list[dict[str, Any]] = []
25232
+ if raw_match_rules and all(isinstance(group, list) for group in raw_match_rules):
25233
+ for group in raw_match_rules:
25234
+ flattened.extend(item for item in group if isinstance(item, dict))
25235
+ else:
25236
+ flattened.extend(item for item in raw_match_rules if isinstance(item, dict))
25237
+ return [_normalize_custom_button_match_rule_for_public(rule) for rule in flattened]
25238
+
25239
+
25240
+ def _match_field_name(field_id: Any, *, fields_by_que_id: dict[int, dict[str, Any]], fallback: Any = None) -> str:
25241
+ normalized_id = _coerce_any_int(field_id)
25242
+ if normalized_id is not None:
25243
+ field = fields_by_que_id.get(normalized_id) or {}
25244
+ name = str(field.get("name") or field.get("title") or "").strip()
25245
+ if name:
25246
+ return name
25247
+ if normalized_id == -17:
25248
+ return "数据ID"
25249
+ if normalized_id == 0:
25250
+ return "编号"
25251
+ fallback_text = str(fallback or "").strip()
25252
+ return fallback_text or str(field_id or "").strip()
25253
+
25254
+
25255
+ def _public_associated_resource_match_mappings_from_rules(
25256
+ rules: list[dict[str, Any]],
25257
+ *,
25258
+ source_fields: dict[int, dict[str, Any]],
25259
+ target_fields: dict[int, dict[str, Any]],
25260
+ ) -> list[dict[str, Any]]:
25261
+ mappings: list[dict[str, Any]] = []
25262
+ for rule in rules:
25263
+ if not isinstance(rule, dict):
25264
+ continue
25265
+ target_field_id = _first_present(rule, "que_id", "queId")
25266
+ target_field = target_fields.get(_coerce_any_int(target_field_id) or 0) or {}
25267
+ target_name = _match_field_name(target_field_id, fields_by_que_id=target_fields, fallback=rule.get("que_title"))
25268
+ operator = _public_view_filter_operator_from_judge_type(rule.get("judge_type"))
25269
+ mapping: dict[str, Any] = {
25270
+ "target_field": target_name,
25271
+ "operator": operator,
25272
+ }
25273
+ if _coerce_any_int(rule.get("match_type")) == MATCH_TYPE_QUESTION:
25274
+ source_field_id = _first_present(rule, "judge_que_id", "judgeQueId")
25275
+ source_detail = rule.get("judge_que_detail") if isinstance(rule.get("judge_que_detail"), dict) else {}
25276
+ mapping["source_field"] = _match_field_name(
25277
+ source_field_id,
25278
+ fields_by_que_id=source_fields,
25279
+ fallback=source_detail.get("que_title"),
25280
+ )
25281
+ else:
25282
+ value_rule = {
25283
+ "judgeValues": rule.get("judge_values") or [],
25284
+ "judgeValueDetails": rule.get("judge_value_details") or [],
25285
+ }
25286
+ values = _public_view_filter_rule_values(value_rule, field=target_field)
25287
+ if len(values) == 1:
25288
+ mapping["value"] = values[0]
25289
+ elif values:
25290
+ mapping["values"] = values
25291
+ mappings.append(_compact_dict(mapping))
25292
+ return mappings
25293
+
25294
+
25079
25295
  def _normalize_associated_graph_type(raw_item: dict[str, Any], *, chart_key: Any, view_key: Any) -> str:
25080
25296
  raw_graph_type = str(_first_present(raw_item, "graph_type", "graphType", "resourceType", "type") or "").strip().lower()
25081
25297
  raw_source_type = str(_first_present(raw_item, "source_type", "sourceType") or "").strip().lower()