@josephyan/qingflow-app-builder-mcp 0.2.0-beta.49 → 0.2.0-beta.50
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/models.py +61 -5
- package/src/qingflow_mcp/builder_facade/service.py +525 -20
- package/src/qingflow_mcp/tools/ai_builder_tools.py +51 -0
- package/src/qingflow_mcp/tools/app_tools.py +36 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +18 -3
- package/src/qingflow_mcp/tools/record_tools.py +735 -90
|
@@ -12,6 +12,7 @@ from uuid import uuid4
|
|
|
12
12
|
from ..backend_client import BackendRequestContext
|
|
13
13
|
from ..errors import QingflowApiError
|
|
14
14
|
from ..json_types import JSONObject
|
|
15
|
+
from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_TYPE
|
|
15
16
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
16
17
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
17
18
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
@@ -51,6 +52,7 @@ from .models import (
|
|
|
51
52
|
PortalApplyRequest,
|
|
52
53
|
PortalSectionPatch,
|
|
53
54
|
PublicFieldType,
|
|
55
|
+
PublicRelationMode,
|
|
54
56
|
PublicChartType,
|
|
55
57
|
PublicViewType,
|
|
56
58
|
SchemaPlanRequest,
|
|
@@ -1110,17 +1112,7 @@ class AiBuilderFacade:
|
|
|
1110
1112
|
parsed = state["parsed"]
|
|
1111
1113
|
response = AppFieldsReadResponse(
|
|
1112
1114
|
app_key=app_key,
|
|
1113
|
-
fields=[
|
|
1114
|
-
{
|
|
1115
|
-
"field_id": field.get("field_id"),
|
|
1116
|
-
"que_id": field.get("que_id"),
|
|
1117
|
-
"name": field.get("name"),
|
|
1118
|
-
"type": field.get("type"),
|
|
1119
|
-
"required": bool(field.get("required")),
|
|
1120
|
-
"section_id": _find_field_section_id(parsed["layout"], str(field.get("name") or "")),
|
|
1121
|
-
}
|
|
1122
|
-
for field in parsed["fields"]
|
|
1123
|
-
],
|
|
1115
|
+
fields=[_compact_public_field_read(field=field, layout=parsed["layout"]) for field in parsed["fields"]],
|
|
1124
1116
|
field_count=len(parsed["fields"]),
|
|
1125
1117
|
)
|
|
1126
1118
|
return {
|
|
@@ -1198,10 +1190,26 @@ class AiBuilderFacade:
|
|
|
1198
1190
|
details={"app_key": app_key},
|
|
1199
1191
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1200
1192
|
)
|
|
1193
|
+
summarized_views, config_read_errors = _summarize_views_with_config(self.views, profile=profile, views=views)
|
|
1201
1194
|
response = AppViewsReadResponse(
|
|
1202
1195
|
app_key=app_key,
|
|
1203
|
-
views=
|
|
1196
|
+
views=summarized_views,
|
|
1204
1197
|
)
|
|
1198
|
+
warnings = []
|
|
1199
|
+
if response.views:
|
|
1200
|
+
warnings.append(
|
|
1201
|
+
_warning(
|
|
1202
|
+
"VIEW_FILTERS_UNVERIFIED",
|
|
1203
|
+
"view summary does not verify saved filter behavior; use apply verification or explicit reads before relying on filters",
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
if config_read_errors:
|
|
1207
|
+
warnings.append(
|
|
1208
|
+
_warning(
|
|
1209
|
+
"VIEW_CONFIG_READ_PARTIAL",
|
|
1210
|
+
"some view configs could not be read back; field order and display order may be incomplete for those views",
|
|
1211
|
+
)
|
|
1212
|
+
)
|
|
1205
1213
|
return {
|
|
1206
1214
|
"status": "success",
|
|
1207
1215
|
"error_code": None,
|
|
@@ -1210,12 +1218,16 @@ class AiBuilderFacade:
|
|
|
1210
1218
|
"normalized_args": {"app_key": app_key},
|
|
1211
1219
|
"missing_fields": [],
|
|
1212
1220
|
"allowed_values": {},
|
|
1213
|
-
"details": {},
|
|
1221
|
+
"details": {"view_config_read_errors": config_read_errors} if config_read_errors else {},
|
|
1214
1222
|
"request_id": None,
|
|
1215
1223
|
"suggested_next_call": None,
|
|
1216
1224
|
"noop": False,
|
|
1217
|
-
"warnings":
|
|
1218
|
-
"verification": {
|
|
1225
|
+
"warnings": warnings,
|
|
1226
|
+
"verification": {
|
|
1227
|
+
"app_exists": True,
|
|
1228
|
+
"view_filters_verified": False,
|
|
1229
|
+
"view_display_readback_complete": not config_read_errors,
|
|
1230
|
+
},
|
|
1219
1231
|
"verified": True,
|
|
1220
1232
|
**response.model_dump(mode="json"),
|
|
1221
1233
|
}
|
|
@@ -2752,6 +2764,8 @@ class AiBuilderFacade:
|
|
|
2752
2764
|
matched_existing_view = name_matches[0]
|
|
2753
2765
|
existing_key = _extract_view_key(matched_existing_view)
|
|
2754
2766
|
created_key: str | None = None
|
|
2767
|
+
system_view_list_type = _resolve_system_view_list_type(view_key=existing_key, view_name=patch.name) if existing_key else None
|
|
2768
|
+
operation_phase = "view_update" if existing_key else "view_create"
|
|
2755
2769
|
try:
|
|
2756
2770
|
if existing_key:
|
|
2757
2771
|
payload = _build_view_update_payload(
|
|
@@ -2763,6 +2777,41 @@ class AiBuilderFacade:
|
|
|
2763
2777
|
view_filters=translated_filters,
|
|
2764
2778
|
)
|
|
2765
2779
|
self.views.view_update(profile=profile, viewgraph_key=existing_key, payload=payload)
|
|
2780
|
+
system_view_sync: dict[str, Any] | None = None
|
|
2781
|
+
if system_view_list_type is not None and patch.type.value == "table":
|
|
2782
|
+
operation_phase = "default_view_apply_config_sync"
|
|
2783
|
+
system_view_sync = self._sync_system_view_apply_config(
|
|
2784
|
+
profile=profile,
|
|
2785
|
+
app_key=app_key,
|
|
2786
|
+
list_type=system_view_list_type,
|
|
2787
|
+
schema=schema,
|
|
2788
|
+
visible_field_names=patch.columns,
|
|
2789
|
+
)
|
|
2790
|
+
if not bool(system_view_sync.get("verified")):
|
|
2791
|
+
failure_entry = {
|
|
2792
|
+
"name": patch.name,
|
|
2793
|
+
"view_key": existing_key,
|
|
2794
|
+
"type": patch.type.value,
|
|
2795
|
+
"status": "failed",
|
|
2796
|
+
"error_code": "SYSTEM_VIEW_ORDER_SYNC_FAILED",
|
|
2797
|
+
"message": "default view column order did not verify through app apply/baseInfo readback",
|
|
2798
|
+
"request_id": None,
|
|
2799
|
+
"backend_code": None,
|
|
2800
|
+
"http_status": None,
|
|
2801
|
+
"operation": "sync_default_view",
|
|
2802
|
+
"details": {
|
|
2803
|
+
"app_key": app_key,
|
|
2804
|
+
"view_name": patch.name,
|
|
2805
|
+
"view_key": existing_key,
|
|
2806
|
+
"view_type": patch.type.value,
|
|
2807
|
+
"list_type": system_view_list_type,
|
|
2808
|
+
"expected_visible_order": system_view_sync.get("expected_visible_order"),
|
|
2809
|
+
"actual_visible_order": system_view_sync.get("actual_visible_order"),
|
|
2810
|
+
},
|
|
2811
|
+
}
|
|
2812
|
+
failed_views.append(failure_entry)
|
|
2813
|
+
view_results.append(failure_entry)
|
|
2814
|
+
continue
|
|
2766
2815
|
updated.append(patch.name)
|
|
2767
2816
|
view_results.append(
|
|
2768
2817
|
{
|
|
@@ -2771,6 +2820,7 @@ class AiBuilderFacade:
|
|
|
2771
2820
|
"type": patch.type.value,
|
|
2772
2821
|
"status": "updated",
|
|
2773
2822
|
"expected_filters": deepcopy(translated_filters),
|
|
2823
|
+
"system_view_sync": system_view_sync,
|
|
2774
2824
|
}
|
|
2775
2825
|
)
|
|
2776
2826
|
else:
|
|
@@ -2816,8 +2866,9 @@ class AiBuilderFacade:
|
|
|
2816
2866
|
)
|
|
2817
2867
|
except (QingflowApiError, RuntimeError) as error:
|
|
2818
2868
|
api_error = _coerce_api_error(error)
|
|
2819
|
-
should_retry_minimal =
|
|
2820
|
-
|
|
2869
|
+
should_retry_minimal = operation_phase != "default_view_apply_config_sync" and (
|
|
2870
|
+
api_error.backend_code == 48104
|
|
2871
|
+
or (patch.type.value == "table" and bool(patch.filters) and api_error.http_status is not None and api_error.http_status >= 500)
|
|
2821
2872
|
)
|
|
2822
2873
|
if should_retry_minimal:
|
|
2823
2874
|
try:
|
|
@@ -2893,7 +2944,7 @@ class AiBuilderFacade:
|
|
|
2893
2944
|
"request_id": api_error.request_id,
|
|
2894
2945
|
"backend_code": api_error.backend_code,
|
|
2895
2946
|
"http_status": None if api_error.http_status == 404 else api_error.http_status,
|
|
2896
|
-
"operation": "update" if existing_key or created_key else "create",
|
|
2947
|
+
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
2897
2948
|
"details": {
|
|
2898
2949
|
"app_key": app_key,
|
|
2899
2950
|
"view_name": patch.name,
|
|
@@ -2904,7 +2955,8 @@ class AiBuilderFacade:
|
|
|
2904
2955
|
"start_field": patch.start_field,
|
|
2905
2956
|
"end_field": patch.end_field,
|
|
2906
2957
|
"title_field": patch.title_field,
|
|
2907
|
-
"operation": "update" if existing_key or created_key else "create",
|
|
2958
|
+
"operation": "sync_default_view" if operation_phase == "default_view_apply_config_sync" else ("update" if existing_key or created_key else "create"),
|
|
2959
|
+
"list_type": system_view_list_type,
|
|
2908
2960
|
"transport_error": {
|
|
2909
2961
|
"http_status": api_error.http_status,
|
|
2910
2962
|
"backend_code": api_error.backend_code,
|
|
@@ -2966,6 +3018,9 @@ class AiBuilderFacade:
|
|
|
2966
3018
|
"status": status,
|
|
2967
3019
|
"present_in_readback": present_in_readback,
|
|
2968
3020
|
}
|
|
3021
|
+
system_view_sync = item.get("system_view_sync")
|
|
3022
|
+
if isinstance(system_view_sync, dict):
|
|
3023
|
+
verification_entry["system_view_sync"] = deepcopy(system_view_sync)
|
|
2969
3024
|
expected_filters = item.get("expected_filters") or []
|
|
2970
3025
|
if expected_filters:
|
|
2971
3026
|
if verified_views_unavailable or not present_in_readback:
|
|
@@ -3843,6 +3898,40 @@ class AiBuilderFacade:
|
|
|
3843
3898
|
"schema_source": schema_source,
|
|
3844
3899
|
}
|
|
3845
3900
|
|
|
3901
|
+
def _sync_system_view_apply_config(
|
|
3902
|
+
self,
|
|
3903
|
+
*,
|
|
3904
|
+
profile: str,
|
|
3905
|
+
app_key: str,
|
|
3906
|
+
list_type: int,
|
|
3907
|
+
schema: dict[str, Any],
|
|
3908
|
+
visible_field_names: list[str],
|
|
3909
|
+
) -> dict[str, Any]:
|
|
3910
|
+
base_info_response = self.apps.app_get_apply_base_info(profile=profile, app_key=app_key, list_type=list_type)
|
|
3911
|
+
base_info_result = base_info_response.get("result") if isinstance(base_info_response.get("result"), dict) else {}
|
|
3912
|
+
current_que_base_infos = base_info_result.get("queBaseInfos") if isinstance(base_info_result.get("queBaseInfos"), list) else []
|
|
3913
|
+
change_ques = _build_apply_config_change_ques(
|
|
3914
|
+
current_que_base_infos=current_que_base_infos,
|
|
3915
|
+
schema=schema,
|
|
3916
|
+
visible_field_names=visible_field_names,
|
|
3917
|
+
)
|
|
3918
|
+
self.apps.app_update_apply_config(
|
|
3919
|
+
profile=profile,
|
|
3920
|
+
app_key=app_key,
|
|
3921
|
+
payload={"type": list_type, "changeQues": change_ques},
|
|
3922
|
+
)
|
|
3923
|
+
readback_response = self.apps.app_get_apply_base_info(profile=profile, app_key=app_key, list_type=list_type)
|
|
3924
|
+
readback_result = readback_response.get("result") if isinstance(readback_response.get("result"), dict) else {}
|
|
3925
|
+
actual_visible_order = _extract_apply_visible_field_names(readback_result.get("queBaseInfos"))
|
|
3926
|
+
expected_visible_order = [name for name in visible_field_names if name]
|
|
3927
|
+
verified = actual_visible_order[: len(expected_visible_order)] == expected_visible_order
|
|
3928
|
+
return {
|
|
3929
|
+
"list_type": list_type,
|
|
3930
|
+
"expected_visible_order": expected_visible_order,
|
|
3931
|
+
"actual_visible_order": actual_visible_order,
|
|
3932
|
+
"verified": verified,
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3846
3935
|
def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
|
|
3847
3936
|
try:
|
|
3848
3937
|
views = self.views.view_list_flat(profile=profile, app_key=app_key)
|
|
@@ -4402,6 +4491,23 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
4402
4491
|
return None
|
|
4403
4492
|
|
|
4404
4493
|
|
|
4494
|
+
def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
4495
|
+
if isinstance(value, bool) or value is None:
|
|
4496
|
+
return None
|
|
4497
|
+
if isinstance(value, int):
|
|
4498
|
+
return value if value >= 0 else None
|
|
4499
|
+
if isinstance(value, float):
|
|
4500
|
+
parsed = int(value)
|
|
4501
|
+
return parsed if parsed >= 0 else None
|
|
4502
|
+
if isinstance(value, str) and value.strip():
|
|
4503
|
+
try:
|
|
4504
|
+
parsed = int(value)
|
|
4505
|
+
except ValueError:
|
|
4506
|
+
return None
|
|
4507
|
+
return parsed if parsed >= 0 else None
|
|
4508
|
+
return None
|
|
4509
|
+
|
|
4510
|
+
|
|
4405
4511
|
def _coerce_int_list(values: Any) -> list[int]:
|
|
4406
4512
|
if not isinstance(values, list):
|
|
4407
4513
|
return []
|
|
@@ -5035,6 +5141,143 @@ def _extract_view_key(view: dict[str, Any]) -> str:
|
|
|
5035
5141
|
return str(view.get("viewgraphKey") or view.get("viewKey") or "").strip()
|
|
5036
5142
|
|
|
5037
5143
|
|
|
5144
|
+
def _resolve_system_view_list_type(*, view_key: str | None, view_name: str | None) -> int | None:
|
|
5145
|
+
normalized_key = str(view_key or "").strip()
|
|
5146
|
+
if normalized_key in SYSTEM_VIEW_ID_TO_LIST_TYPE:
|
|
5147
|
+
return SYSTEM_VIEW_ID_TO_LIST_TYPE[normalized_key]
|
|
5148
|
+
numeric_key = _coerce_positive_int(normalized_key)
|
|
5149
|
+
if numeric_key is not None and numeric_key in RECORD_LIST_TYPE_LABELS:
|
|
5150
|
+
return numeric_key
|
|
5151
|
+
normalized_name = str(view_name or "").strip()
|
|
5152
|
+
if not normalized_name:
|
|
5153
|
+
return None
|
|
5154
|
+
if normalized_name == "全部数据":
|
|
5155
|
+
return 8
|
|
5156
|
+
if normalized_name == "我发起的":
|
|
5157
|
+
return 14
|
|
5158
|
+
if normalized_name == "待办":
|
|
5159
|
+
return 1
|
|
5160
|
+
if normalized_name == "已办":
|
|
5161
|
+
return 2
|
|
5162
|
+
if normalized_name in {"抄送", "抄送我的"}:
|
|
5163
|
+
return 12
|
|
5164
|
+
return None
|
|
5165
|
+
|
|
5166
|
+
|
|
5167
|
+
def _build_apply_config_change_ques(
|
|
5168
|
+
*,
|
|
5169
|
+
current_que_base_infos: list[dict[str, Any]],
|
|
5170
|
+
schema: dict[str, Any],
|
|
5171
|
+
visible_field_names: list[str],
|
|
5172
|
+
) -> list[dict[str, Any]]:
|
|
5173
|
+
parsed_schema = _parse_schema(schema)
|
|
5174
|
+
fields_by_name = {
|
|
5175
|
+
str(field.get("name") or ""): field
|
|
5176
|
+
for field in parsed_schema.get("fields", [])
|
|
5177
|
+
if isinstance(field, dict) and str(field.get("name") or "")
|
|
5178
|
+
}
|
|
5179
|
+
current_items: list[dict[str, Any]] = [item for item in current_que_base_infos if isinstance(item, dict)]
|
|
5180
|
+
current_by_que_id = {
|
|
5181
|
+
_coerce_positive_int(item.get("queId")): item
|
|
5182
|
+
for item in current_items
|
|
5183
|
+
if _coerce_positive_int(item.get("queId")) is not None
|
|
5184
|
+
}
|
|
5185
|
+
ordered_visible_que_ids: list[int] = []
|
|
5186
|
+
for field_name in visible_field_names:
|
|
5187
|
+
field = fields_by_name.get(str(field_name or "").strip())
|
|
5188
|
+
que_id = _coerce_positive_int(field.get("que_id")) if isinstance(field, dict) else None
|
|
5189
|
+
if que_id is not None and que_id not in ordered_visible_que_ids:
|
|
5190
|
+
ordered_visible_que_ids.append(que_id)
|
|
5191
|
+
selected_que_ids = set(ordered_visible_que_ids)
|
|
5192
|
+
ordered_que_ids: list[int] = list(ordered_visible_que_ids)
|
|
5193
|
+
for item in current_items:
|
|
5194
|
+
que_id = _coerce_positive_int(item.get("queId"))
|
|
5195
|
+
if que_id is not None and que_id not in selected_que_ids and que_id not in ordered_que_ids:
|
|
5196
|
+
ordered_que_ids.append(que_id)
|
|
5197
|
+
for field in parsed_schema.get("fields", []):
|
|
5198
|
+
if not isinstance(field, dict):
|
|
5199
|
+
continue
|
|
5200
|
+
que_id = _coerce_positive_int(field.get("que_id"))
|
|
5201
|
+
if que_id is not None and que_id not in ordered_que_ids:
|
|
5202
|
+
ordered_que_ids.append(que_id)
|
|
5203
|
+
return [
|
|
5204
|
+
_normalize_apply_change_que(
|
|
5205
|
+
current_by_que_id.get(que_id),
|
|
5206
|
+
fallback_field=next(
|
|
5207
|
+
(
|
|
5208
|
+
field
|
|
5209
|
+
for field in parsed_schema.get("fields", [])
|
|
5210
|
+
if isinstance(field, dict) and _coerce_positive_int(field.get("que_id")) == que_id
|
|
5211
|
+
),
|
|
5212
|
+
None,
|
|
5213
|
+
),
|
|
5214
|
+
visible=que_id in selected_que_ids,
|
|
5215
|
+
)
|
|
5216
|
+
for que_id in ordered_que_ids
|
|
5217
|
+
]
|
|
5218
|
+
|
|
5219
|
+
|
|
5220
|
+
def _normalize_apply_change_que(
|
|
5221
|
+
item: dict[str, Any] | None,
|
|
5222
|
+
*,
|
|
5223
|
+
fallback_field: dict[str, Any] | None,
|
|
5224
|
+
visible: bool,
|
|
5225
|
+
) -> dict[str, Any]:
|
|
5226
|
+
que_id = _coerce_positive_int((item or {}).get("queId"))
|
|
5227
|
+
if que_id is None and isinstance(fallback_field, dict):
|
|
5228
|
+
que_id = _coerce_positive_int(fallback_field.get("que_id"))
|
|
5229
|
+
payload: dict[str, Any] = {
|
|
5230
|
+
"queId": que_id,
|
|
5231
|
+
"beingVisible": visible,
|
|
5232
|
+
}
|
|
5233
|
+
width = _coerce_positive_int((item or {}).get("width"))
|
|
5234
|
+
if width is not None:
|
|
5235
|
+
payload["width"] = width
|
|
5236
|
+
current_sub_ques = (item or {}).get("subQues")
|
|
5237
|
+
if isinstance(current_sub_ques, list):
|
|
5238
|
+
payload["subQues"] = [
|
|
5239
|
+
_normalize_apply_sub_change_que(sub_item, visible=visible)
|
|
5240
|
+
for sub_item in current_sub_ques
|
|
5241
|
+
if isinstance(sub_item, dict)
|
|
5242
|
+
]
|
|
5243
|
+
elif isinstance(fallback_field, dict) and isinstance(fallback_field.get("subfields"), list):
|
|
5244
|
+
payload["subQues"] = [
|
|
5245
|
+
{
|
|
5246
|
+
"queId": _coerce_positive_int(subfield.get("que_id")),
|
|
5247
|
+
"beingVisible": visible,
|
|
5248
|
+
}
|
|
5249
|
+
for subfield in fallback_field.get("subfields", [])
|
|
5250
|
+
if isinstance(subfield, dict) and _coerce_positive_int(subfield.get("que_id")) is not None
|
|
5251
|
+
]
|
|
5252
|
+
return payload
|
|
5253
|
+
|
|
5254
|
+
|
|
5255
|
+
def _normalize_apply_sub_change_que(item: dict[str, Any], *, visible: bool) -> dict[str, Any]:
|
|
5256
|
+
payload: dict[str, Any] = {
|
|
5257
|
+
"queId": _coerce_positive_int(item.get("queId")),
|
|
5258
|
+
"beingVisible": visible and bool(item.get("beingVisible", True)),
|
|
5259
|
+
}
|
|
5260
|
+
width = _coerce_positive_int(item.get("width"))
|
|
5261
|
+
if width is not None:
|
|
5262
|
+
payload["width"] = width
|
|
5263
|
+
return payload
|
|
5264
|
+
|
|
5265
|
+
|
|
5266
|
+
def _extract_apply_visible_field_names(que_base_infos: Any) -> list[str]:
|
|
5267
|
+
if not isinstance(que_base_infos, list):
|
|
5268
|
+
return []
|
|
5269
|
+
visible_names: list[str] = []
|
|
5270
|
+
for item in que_base_infos:
|
|
5271
|
+
if not isinstance(item, dict):
|
|
5272
|
+
continue
|
|
5273
|
+
if not bool(item.get("beingVisible", True)):
|
|
5274
|
+
continue
|
|
5275
|
+
name = str(item.get("queTitle") or "").strip()
|
|
5276
|
+
if name:
|
|
5277
|
+
visible_names.append(name)
|
|
5278
|
+
return visible_names
|
|
5279
|
+
|
|
5280
|
+
|
|
5038
5281
|
def _empty_schema_result(title: str) -> dict[str, Any]:
|
|
5039
5282
|
return {
|
|
5040
5283
|
"formTitle": title,
|
|
@@ -5090,6 +5333,33 @@ def _resolve_relation_target_field(
|
|
|
5090
5333
|
return target_fields[match_index]
|
|
5091
5334
|
|
|
5092
5335
|
|
|
5336
|
+
def _relation_mode_from_optional_data_num(value: Any) -> str:
|
|
5337
|
+
return PublicRelationMode.multiple.value if _relation_mode_to_optional_data_num(value) == 0 else PublicRelationMode.single.value
|
|
5338
|
+
|
|
5339
|
+
|
|
5340
|
+
def _relation_mode_to_optional_data_num(value: Any) -> int:
|
|
5341
|
+
if isinstance(value, bool):
|
|
5342
|
+
return 0 if value else 1
|
|
5343
|
+
if isinstance(value, int):
|
|
5344
|
+
return 0 if value == 0 else 1
|
|
5345
|
+
if isinstance(value, str):
|
|
5346
|
+
normalized = value.strip().lower()
|
|
5347
|
+
if normalized in {
|
|
5348
|
+
PublicRelationMode.multiple.value,
|
|
5349
|
+
"multi",
|
|
5350
|
+
"multi_select",
|
|
5351
|
+
"multi-select",
|
|
5352
|
+
"many",
|
|
5353
|
+
"0",
|
|
5354
|
+
}:
|
|
5355
|
+
return 0
|
|
5356
|
+
return 1
|
|
5357
|
+
|
|
5358
|
+
|
|
5359
|
+
def _normalize_relation_mode(value: Any) -> str:
|
|
5360
|
+
return _relation_mode_from_optional_data_num(value)
|
|
5361
|
+
|
|
5362
|
+
|
|
5093
5363
|
def _hydrate_relation_field_configs(
|
|
5094
5364
|
facade: "AiBuilderFacade",
|
|
5095
5365
|
*,
|
|
@@ -5104,6 +5374,11 @@ def _hydrate_relation_field_configs(
|
|
|
5104
5374
|
target_app_key = str(field.get("target_app_key") or "").strip()
|
|
5105
5375
|
if not target_app_key:
|
|
5106
5376
|
continue
|
|
5377
|
+
config = deepcopy(field.get("config") or {})
|
|
5378
|
+
relation_mode = _normalize_relation_mode(field.get("relation_mode", config.get("optional_data_num")))
|
|
5379
|
+
config["optional_data_num"] = _relation_mode_to_optional_data_num(relation_mode)
|
|
5380
|
+
field["relation_mode"] = relation_mode
|
|
5381
|
+
field["config"] = config
|
|
5107
5382
|
display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
|
|
5108
5383
|
visible_selector_payloads = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
|
|
5109
5384
|
if display_selector is None and not visible_selector_payloads:
|
|
@@ -5132,7 +5407,6 @@ def _hydrate_relation_field_configs(
|
|
|
5132
5407
|
for item in visible_fields
|
|
5133
5408
|
):
|
|
5134
5409
|
visible_fields = [display_field, *visible_fields]
|
|
5135
|
-
config = deepcopy(field.get("config") or {})
|
|
5136
5410
|
config["target_field_label"] = display_field.get("name")
|
|
5137
5411
|
config["target_field_type"] = display_field.get("type")
|
|
5138
5412
|
config["target_field_que_id"] = display_field.get("que_id")
|
|
@@ -5217,6 +5491,7 @@ def _parse_field(question: dict[str, Any], *, field_id_hint: str | None = None)
|
|
|
5217
5491
|
if field_type == FieldType.relation:
|
|
5218
5492
|
reference = question.get("referenceConfig") if isinstance(question.get("referenceConfig"), dict) else {}
|
|
5219
5493
|
field["target_app_key"] = reference.get("referAppKey")
|
|
5494
|
+
field["relation_mode"] = _relation_mode_from_optional_data_num(reference.get("optionalDataNum"))
|
|
5220
5495
|
refer_questions = reference.get("referQuestions") if isinstance(reference.get("referQuestions"), list) else []
|
|
5221
5496
|
visible_fields: list[dict[str, Any]] = []
|
|
5222
5497
|
display_field_que_id = _coerce_positive_int(reference.get("referQueId"))
|
|
@@ -5367,6 +5642,23 @@ def _resolve_layout_field_name(
|
|
|
5367
5642
|
return None
|
|
5368
5643
|
|
|
5369
5644
|
|
|
5645
|
+
def _compact_public_field_read(*, field: dict[str, Any], layout: dict[str, Any]) -> dict[str, Any]:
|
|
5646
|
+
payload = {
|
|
5647
|
+
"field_id": field.get("field_id"),
|
|
5648
|
+
"que_id": field.get("que_id"),
|
|
5649
|
+
"name": field.get("name"),
|
|
5650
|
+
"type": field.get("type"),
|
|
5651
|
+
"required": bool(field.get("required")),
|
|
5652
|
+
"section_id": _find_field_section_id(layout, str(field.get("name") or "")),
|
|
5653
|
+
}
|
|
5654
|
+
if field.get("type") == FieldType.relation.value:
|
|
5655
|
+
payload["target_app_key"] = field.get("target_app_key")
|
|
5656
|
+
payload["relation_mode"] = _normalize_relation_mode(field.get("relation_mode"))
|
|
5657
|
+
payload["display_field"] = deepcopy(field.get("display_field"))
|
|
5658
|
+
payload["visible_fields"] = deepcopy(field.get("visible_fields") or [])
|
|
5659
|
+
return payload
|
|
5660
|
+
|
|
5661
|
+
|
|
5370
5662
|
def _build_selector_map(fields: list[dict[str, Any]]) -> dict[str, int]:
|
|
5371
5663
|
mapping: dict[str, int] = {}
|
|
5372
5664
|
for index, field in enumerate(fields):
|
|
@@ -5415,6 +5707,7 @@ def _field_patch_to_internal(patch: FieldPatch) -> dict[str, Any]:
|
|
|
5415
5707
|
"target_app_key": patch.target_app_key,
|
|
5416
5708
|
"display_field": patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None,
|
|
5417
5709
|
"visible_fields": [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields],
|
|
5710
|
+
"relation_mode": patch.relation_mode.value if patch.relation_mode is not None else None,
|
|
5418
5711
|
"subfields": [_field_patch_to_internal(subfield) for subfield in patch.subfields],
|
|
5419
5712
|
"que_id": None,
|
|
5420
5713
|
}
|
|
@@ -5430,6 +5723,7 @@ def _field_matches_patch(field: dict[str, Any], patch: FieldPatch) -> bool:
|
|
|
5430
5723
|
and (field.get("target_app_key") or None) == patch.target_app_key
|
|
5431
5724
|
and _field_selector_payload_equal(field.get("display_field"), patch.display_field.model_dump(mode="json", exclude_none=True) if patch.display_field is not None else None)
|
|
5432
5725
|
and _field_selector_list_equal(field.get("visible_fields"), [selector.model_dump(mode="json", exclude_none=True) for selector in patch.visible_fields])
|
|
5726
|
+
and _normalize_relation_mode(field.get("relation_mode")) == _normalize_relation_mode(patch.relation_mode.value if patch.relation_mode is not None else None)
|
|
5433
5727
|
and len(field.get("subfields") or []) == len(patch.subfields)
|
|
5434
5728
|
)
|
|
5435
5729
|
|
|
@@ -5472,6 +5766,8 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
5472
5766
|
field["display_field"] = payload["display_field"]
|
|
5473
5767
|
if "visible_fields" in payload:
|
|
5474
5768
|
field["visible_fields"] = list(payload["visible_fields"])
|
|
5769
|
+
if "relation_mode" in payload:
|
|
5770
|
+
field["relation_mode"] = payload["relation_mode"]
|
|
5475
5771
|
if "subfields" in payload:
|
|
5476
5772
|
field["subfields"] = [_field_patch_to_internal(item) for item in payload["subfields"]]
|
|
5477
5773
|
|
|
@@ -5956,6 +6252,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
|
|
|
5956
6252
|
reference["_targetEntityId"] = field.get("target_app_key")
|
|
5957
6253
|
if field.get("target_field_que_id"):
|
|
5958
6254
|
reference["referQueId"] = field.get("target_field_que_id")
|
|
6255
|
+
reference["optionalDataNum"] = _relation_mode_to_optional_data_num(field.get("relation_mode"))
|
|
5959
6256
|
question["referenceConfig"] = reference
|
|
5960
6257
|
return question
|
|
5961
6258
|
|
|
@@ -6188,6 +6485,214 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
6188
6485
|
return items
|
|
6189
6486
|
|
|
6190
6487
|
|
|
6488
|
+
def _summarize_views_with_config(views_tool: ViewTools, *, profile: str, views: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
6489
|
+
base_items = _summarize_views(views)
|
|
6490
|
+
if not base_items:
|
|
6491
|
+
return [], []
|
|
6492
|
+
config_read_errors: list[dict[str, Any]] = []
|
|
6493
|
+
enriched_items: list[dict[str, Any]] = []
|
|
6494
|
+
for item in base_items:
|
|
6495
|
+
view_key = str(item.get("view_key") or "").strip()
|
|
6496
|
+
if not view_key:
|
|
6497
|
+
enriched_items.append(item)
|
|
6498
|
+
continue
|
|
6499
|
+
try:
|
|
6500
|
+
config_response = views_tool.view_get_config(profile=profile, viewgraph_key=view_key)
|
|
6501
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
6502
|
+
api_error = _coerce_api_error(error)
|
|
6503
|
+
config_read_errors.append(
|
|
6504
|
+
{
|
|
6505
|
+
"view_key": view_key,
|
|
6506
|
+
"name": item.get("name"),
|
|
6507
|
+
"request_id": api_error.request_id,
|
|
6508
|
+
"http_status": api_error.http_status,
|
|
6509
|
+
"backend_code": api_error.backend_code,
|
|
6510
|
+
"category": api_error.category,
|
|
6511
|
+
}
|
|
6512
|
+
)
|
|
6513
|
+
enriched_items.append(item)
|
|
6514
|
+
continue
|
|
6515
|
+
config = config_response.get("result") if isinstance(config_response.get("result"), dict) else {}
|
|
6516
|
+
enriched_items.append(_merge_view_summary_with_config(item, config=config))
|
|
6517
|
+
return enriched_items, config_read_errors
|
|
6518
|
+
|
|
6519
|
+
|
|
6520
|
+
def _merge_view_summary_with_config(base: dict[str, Any], *, config: dict[str, Any]) -> dict[str, Any]:
|
|
6521
|
+
summary = deepcopy(base)
|
|
6522
|
+
if not isinstance(config, dict) or not config:
|
|
6523
|
+
return summary
|
|
6524
|
+
legacy_columns = [str(value) for value in (summary.get("columns") or []) if str(value or "").strip()]
|
|
6525
|
+
question_entries = _extract_view_question_entries(config.get("viewgraphQuestions"))
|
|
6526
|
+
question_entries_by_id = {
|
|
6527
|
+
field_id: entry
|
|
6528
|
+
for entry in question_entries
|
|
6529
|
+
if (field_id := _coerce_nonnegative_int(entry.get("field_id"))) is not None
|
|
6530
|
+
}
|
|
6531
|
+
configured_column_ids = [
|
|
6532
|
+
field_id
|
|
6533
|
+
for field_id in (_coerce_nonnegative_int(value) for value in (config.get("viewgraphQueIds") or []))
|
|
6534
|
+
if field_id is not None
|
|
6535
|
+
]
|
|
6536
|
+
configured_columns = _resolve_view_column_names(
|
|
6537
|
+
configured_column_ids,
|
|
6538
|
+
question_entries_by_id=question_entries_by_id,
|
|
6539
|
+
legacy_columns=legacy_columns,
|
|
6540
|
+
)
|
|
6541
|
+
config_enriched = False
|
|
6542
|
+
display_entries = _sort_view_question_entries(
|
|
6543
|
+
[entry for entry in question_entries if bool(entry.get("visible", True))],
|
|
6544
|
+
)
|
|
6545
|
+
display_column_ids = [
|
|
6546
|
+
field_id
|
|
6547
|
+
for field_id in (_coerce_nonnegative_int(entry.get("field_id")) for entry in display_entries)
|
|
6548
|
+
if field_id is not None
|
|
6549
|
+
]
|
|
6550
|
+
display_columns = [
|
|
6551
|
+
str(entry.get("name") or "").strip()
|
|
6552
|
+
for entry in display_entries
|
|
6553
|
+
if str(entry.get("name") or "").strip()
|
|
6554
|
+
]
|
|
6555
|
+
if not display_columns and configured_columns:
|
|
6556
|
+
display_columns = configured_columns
|
|
6557
|
+
display_column_ids = list(configured_column_ids)
|
|
6558
|
+
if display_columns:
|
|
6559
|
+
summary["columns"] = display_columns
|
|
6560
|
+
summary["display_columns"] = display_columns
|
|
6561
|
+
summary["display_column_ids"] = display_column_ids
|
|
6562
|
+
config_enriched = True
|
|
6563
|
+
elif configured_columns:
|
|
6564
|
+
summary["columns"] = configured_columns
|
|
6565
|
+
config_enriched = True
|
|
6566
|
+
if configured_columns:
|
|
6567
|
+
summary["configured_columns"] = configured_columns
|
|
6568
|
+
summary["configured_column_ids"] = configured_column_ids
|
|
6569
|
+
config_enriched = True
|
|
6570
|
+
if question_entries:
|
|
6571
|
+
summary["column_details"] = display_entries or _sort_view_question_entries(question_entries)
|
|
6572
|
+
config_enriched = True
|
|
6573
|
+
display_config = _extract_view_display_config(
|
|
6574
|
+
config,
|
|
6575
|
+
question_entries_by_id=question_entries_by_id,
|
|
6576
|
+
fallback_group_by=str(summary.get("group_by") or "").strip() or None,
|
|
6577
|
+
)
|
|
6578
|
+
if display_config:
|
|
6579
|
+
summary["display_config"] = display_config
|
|
6580
|
+
if not summary.get("group_by") and display_config.get("group_by"):
|
|
6581
|
+
summary["group_by"] = display_config.get("group_by")
|
|
6582
|
+
config_enriched = True
|
|
6583
|
+
if config_enriched:
|
|
6584
|
+
summary["read_source"] = "view_config"
|
|
6585
|
+
return summary
|
|
6586
|
+
|
|
6587
|
+
|
|
6588
|
+
def _extract_view_question_entries(questions: Any) -> list[dict[str, Any]]:
|
|
6589
|
+
if not isinstance(questions, list):
|
|
6590
|
+
return []
|
|
6591
|
+
entries: list[dict[str, Any]] = []
|
|
6592
|
+
for index, item in enumerate(questions, start=1):
|
|
6593
|
+
if not isinstance(item, dict):
|
|
6594
|
+
continue
|
|
6595
|
+
field_id = _coerce_nonnegative_int(item.get("queId"))
|
|
6596
|
+
name = str(item.get("queTitle") or "").strip() or None
|
|
6597
|
+
visible_raw = item.get("beingListDisplay")
|
|
6598
|
+
if visible_raw is None:
|
|
6599
|
+
visible_raw = item.get("beingVisible")
|
|
6600
|
+
visible = bool(visible_raw) if visible_raw is not None else True
|
|
6601
|
+
display_order = _coerce_positive_int(item.get("displayOrdinal"))
|
|
6602
|
+
entry: dict[str, Any] = {
|
|
6603
|
+
"field_id": field_id,
|
|
6604
|
+
"name": name,
|
|
6605
|
+
"visible": visible,
|
|
6606
|
+
"display_order": display_order if display_order is not None else index,
|
|
6607
|
+
}
|
|
6608
|
+
width = _coerce_positive_int(item.get("width"))
|
|
6609
|
+
if width is not None:
|
|
6610
|
+
entry["width"] = width
|
|
6611
|
+
entries.append(entry)
|
|
6612
|
+
return entries
|
|
6613
|
+
|
|
6614
|
+
|
|
6615
|
+
def _sort_view_question_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
6616
|
+
return sorted(
|
|
6617
|
+
entries,
|
|
6618
|
+
key=lambda entry: (
|
|
6619
|
+
_coerce_positive_int(entry.get("display_order")) if _coerce_positive_int(entry.get("display_order")) is not None else 10**9,
|
|
6620
|
+
str(entry.get("name") or ""),
|
|
6621
|
+
),
|
|
6622
|
+
)
|
|
6623
|
+
|
|
6624
|
+
|
|
6625
|
+
def _resolve_view_column_names(
|
|
6626
|
+
field_ids: list[int],
|
|
6627
|
+
*,
|
|
6628
|
+
question_entries_by_id: dict[int, dict[str, Any]],
|
|
6629
|
+
legacy_columns: list[str],
|
|
6630
|
+
) -> list[str]:
|
|
6631
|
+
names: list[str] = []
|
|
6632
|
+
for index, field_id in enumerate(field_ids):
|
|
6633
|
+
entry = question_entries_by_id.get(field_id) or {}
|
|
6634
|
+
name = str(entry.get("name") or "").strip()
|
|
6635
|
+
if not name and index < len(legacy_columns):
|
|
6636
|
+
name = str(legacy_columns[index] or "").strip()
|
|
6637
|
+
if name:
|
|
6638
|
+
names.append(name)
|
|
6639
|
+
return names
|
|
6640
|
+
|
|
6641
|
+
|
|
6642
|
+
def _extract_view_display_config(
|
|
6643
|
+
config: dict[str, Any],
|
|
6644
|
+
*,
|
|
6645
|
+
question_entries_by_id: dict[int, dict[str, Any]],
|
|
6646
|
+
fallback_group_by: str | None,
|
|
6647
|
+
) -> dict[str, Any]:
|
|
6648
|
+
if not isinstance(config, dict):
|
|
6649
|
+
return {}
|
|
6650
|
+
display_config: dict[str, Any] = {}
|
|
6651
|
+
for source_key, public_key in (
|
|
6652
|
+
("defaultRowHigh", "row_height"),
|
|
6653
|
+
("sortType", "sort_type"),
|
|
6654
|
+
("beingPinNavigate", "pin_navigation"),
|
|
6655
|
+
("beingShowCover", "show_cover"),
|
|
6656
|
+
("beingShowTitleQue", "show_title_field"),
|
|
6657
|
+
):
|
|
6658
|
+
if source_key in config:
|
|
6659
|
+
display_config[public_key] = config.get(source_key)
|
|
6660
|
+
title_field_id = _coerce_positive_int(config.get("titleQue"))
|
|
6661
|
+
if title_field_id is not None:
|
|
6662
|
+
display_config["title_field_id"] = title_field_id
|
|
6663
|
+
title_field = str((question_entries_by_id.get(title_field_id) or {}).get("name") or "").strip()
|
|
6664
|
+
if title_field:
|
|
6665
|
+
display_config["title_field"] = title_field
|
|
6666
|
+
group_field_id = _coerce_positive_int(config.get("groupQueId"))
|
|
6667
|
+
if group_field_id is not None:
|
|
6668
|
+
display_config["group_by_field_id"] = group_field_id
|
|
6669
|
+
group_by = str((question_entries_by_id.get(group_field_id) or {}).get("name") or "").strip()
|
|
6670
|
+
if group_by:
|
|
6671
|
+
display_config["group_by"] = group_by
|
|
6672
|
+
elif fallback_group_by:
|
|
6673
|
+
display_config["group_by"] = fallback_group_by
|
|
6674
|
+
raw_sorts = config.get("viewgraphSorts")
|
|
6675
|
+
if isinstance(raw_sorts, list):
|
|
6676
|
+
sorts: list[dict[str, Any]] = []
|
|
6677
|
+
for item in raw_sorts:
|
|
6678
|
+
if not isinstance(item, dict):
|
|
6679
|
+
continue
|
|
6680
|
+
sort_field_id = _coerce_positive_int(item.get("queId"))
|
|
6681
|
+
if sort_field_id is None:
|
|
6682
|
+
continue
|
|
6683
|
+
sort_entry: dict[str, Any] = {
|
|
6684
|
+
"field_id": sort_field_id,
|
|
6685
|
+
"ascending": bool(item.get("beingSortAscend", True)),
|
|
6686
|
+
}
|
|
6687
|
+
sort_name = str((question_entries_by_id.get(sort_field_id) or {}).get("name") or "").strip()
|
|
6688
|
+
if sort_name:
|
|
6689
|
+
sort_entry["field"] = sort_name
|
|
6690
|
+
sorts.append(sort_entry)
|
|
6691
|
+
if sorts:
|
|
6692
|
+
display_config["sorts"] = sorts
|
|
6693
|
+
return display_config
|
|
6694
|
+
|
|
6695
|
+
|
|
6191
6696
|
def _summarize_charts(result: Any) -> list[dict[str, Any]]:
|
|
6192
6697
|
if not isinstance(result, list):
|
|
6193
6698
|
return []
|