@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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +225 -9
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
|
+
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.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@1.1.10 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -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": {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
16486
|
+
"field_name": field_name,
|
|
16342
16487
|
"operator": operator,
|
|
16343
16488
|
}
|
|
16344
16489
|
if field_id is not None:
|
|
16345
|
-
public_rule["field_id"] =
|
|
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()
|