@josephyan/qingflow-cli 1.1.9 → 1.1.11

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.9
6
+ npm install @josephyan/qingflow-cli@1.1.11
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@1.1.9 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@1.1.11 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.9",
3
+ "version": "1.1.11",
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.9"
7
+ version = "1.1.11"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -6079,6 +6079,21 @@ class AiBuilderFacade:
6079
6079
  details={"app_key": app_key},
6080
6080
  suggested_next_call={"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
6081
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
6082
6097
  view_configs: list[dict[str, Any]] = []
6083
6098
  view_config_read_errors: list[dict[str, Any]] = []
6084
6099
  try:
@@ -6111,7 +6126,10 @@ class AiBuilderFacade:
6111
6126
  "view_config_count": len(view_configs),
6112
6127
  "warnings": warnings,
6113
6128
  "verification": verification,
6114
- "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
+ },
6115
6133
  }
6116
6134
 
6117
6135
  def flow_patch_nodes(
@@ -6777,6 +6795,64 @@ class AiBuilderFacade:
6777
6795
 
6778
6796
  return self.apps._run(profile, runner)
6779
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
+
6780
6856
  def _associated_resource_create(
6781
6857
  self,
6782
6858
  *,
@@ -25139,12 +25215,83 @@ def _normalize_associated_resource_item(raw_item: Any, *, include_raw: bool = Fa
25139
25215
  if chart_type is not None:
25140
25216
  item["chart_type"] = _public_chart_type_from_backend(chart_type)
25141
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
25142
25221
  item = _compact_dict(item)
25143
25222
  if include_raw:
25144
25223
  item["_raw"] = deepcopy(raw_item)
25145
25224
  return item
25146
25225
 
25147
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
+
25148
25295
  def _normalize_associated_graph_type(raw_item: dict[str, Any], *, chart_key: Any, view_key: Any) -> str:
25149
25296
  raw_graph_type = str(_first_present(raw_item, "graph_type", "graphType", "resourceType", "type") or "").strip().lower()
25150
25297
  raw_source_type = str(_first_present(raw_item, "source_type", "sourceType") or "").strip().lower()
@@ -898,7 +898,11 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
898
898
  fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
899
899
  )
900
900
  sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
901
- patch_sections = load_list_arg(args.patch_sections_file, option_name="--patch-sections-file")
901
+ patch_sections = (
902
+ load_list_arg(args.patch_sections_file, option_name="--patch-sections-file")
903
+ if args.patch_sections_file
904
+ else None
905
+ )
902
906
  return context.builder.portal_apply(
903
907
  profile=args.profile,
904
908
  dash_key=args.dash_key,