@josephyan/qingflow-app-builder-mcp 0.2.0-beta.39 → 0.2.0-beta.40
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/skills/qingflow-app-builder/SKILL.md +17 -8
- package/skills/qingflow-app-builder/references/tool-selection.md +6 -4
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +19 -0
- package/src/qingflow_mcp/builder_facade/service.py +544 -49
- package/src/qingflow_mcp/server_app_builder.py +16 -4
- package/src/qingflow_mcp/tools/ai_builder_tools.py +151 -14
- package/src/qingflow_mcp/tools/record_tools.py +249 -3
|
@@ -29,9 +29,11 @@ from ..tools.solution_tools import SolutionTools
|
|
|
29
29
|
from ..tools.view_tools import ViewTools
|
|
30
30
|
from ..tools.workflow_tools import WorkflowTools
|
|
31
31
|
from .models import (
|
|
32
|
+
AppChartsReadResponse,
|
|
32
33
|
AppFieldsReadResponse,
|
|
33
34
|
AppFlowReadResponse,
|
|
34
35
|
AppLayoutReadResponse,
|
|
36
|
+
PortalReadSummaryResponse,
|
|
35
37
|
AppReadSummaryResponse,
|
|
36
38
|
AppViewsReadResponse,
|
|
37
39
|
ChartApplyRequest,
|
|
@@ -1048,7 +1050,13 @@ class AiBuilderFacade:
|
|
|
1048
1050
|
"request_id": None,
|
|
1049
1051
|
"suggested_next_call": None,
|
|
1050
1052
|
"noop": False,
|
|
1051
|
-
"
|
|
1053
|
+
"warnings": _warnings_from_verification_hints(verification_hints),
|
|
1054
|
+
"verification": {
|
|
1055
|
+
"app_exists": True,
|
|
1056
|
+
"views_read_unavailable": views_unavailable,
|
|
1057
|
+
"workflow_read_unavailable": workflow_unavailable,
|
|
1058
|
+
},
|
|
1059
|
+
"verified": not views_unavailable and not workflow_unavailable,
|
|
1052
1060
|
**response.model_dump(mode="json"),
|
|
1053
1061
|
}
|
|
1054
1062
|
|
|
@@ -1092,7 +1100,9 @@ class AiBuilderFacade:
|
|
|
1092
1100
|
"request_id": None,
|
|
1093
1101
|
"suggested_next_call": None,
|
|
1094
1102
|
"noop": False,
|
|
1103
|
+
"warnings": [],
|
|
1095
1104
|
"verification": {"app_exists": True},
|
|
1105
|
+
"verified": True,
|
|
1096
1106
|
**response.model_dump(mode="json"),
|
|
1097
1107
|
}
|
|
1098
1108
|
|
|
@@ -1128,7 +1138,9 @@ class AiBuilderFacade:
|
|
|
1128
1138
|
"request_id": None,
|
|
1129
1139
|
"suggested_next_call": None,
|
|
1130
1140
|
"noop": False,
|
|
1141
|
+
"warnings": _layout_read_warnings(response.unplaced_fields),
|
|
1131
1142
|
"verification": {"app_exists": True},
|
|
1143
|
+
"verified": True,
|
|
1132
1144
|
**response.model_dump(mode="json"),
|
|
1133
1145
|
}
|
|
1134
1146
|
|
|
@@ -1160,7 +1172,9 @@ class AiBuilderFacade:
|
|
|
1160
1172
|
"request_id": None,
|
|
1161
1173
|
"suggested_next_call": None,
|
|
1162
1174
|
"noop": False,
|
|
1175
|
+
"warnings": [],
|
|
1163
1176
|
"verification": {"app_exists": True},
|
|
1177
|
+
"verified": True,
|
|
1164
1178
|
**response.model_dump(mode="json"),
|
|
1165
1179
|
}
|
|
1166
1180
|
|
|
@@ -1194,7 +1208,99 @@ class AiBuilderFacade:
|
|
|
1194
1208
|
"request_id": None,
|
|
1195
1209
|
"suggested_next_call": None,
|
|
1196
1210
|
"noop": False,
|
|
1211
|
+
"warnings": [_warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable")] if workflow_unavailable else [],
|
|
1197
1212
|
"verification": {"app_exists": True, "workflow_read_unavailable": workflow_unavailable},
|
|
1213
|
+
"verified": not workflow_unavailable,
|
|
1214
|
+
**response.model_dump(mode="json"),
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
def app_read_charts_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
1218
|
+
try:
|
|
1219
|
+
app_result = self.app_resolve(profile=profile, app_key=app_key)
|
|
1220
|
+
if app_result.get("status") != "success":
|
|
1221
|
+
return app_result
|
|
1222
|
+
resolved_app_key = str(app_result.get("app_key") or app_key)
|
|
1223
|
+
items = self.charts.qingbi_report_list(profile=profile, app_key=resolved_app_key).get("items") or []
|
|
1224
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1225
|
+
api_error = _coerce_api_error(error)
|
|
1226
|
+
return _failed_from_api_error(
|
|
1227
|
+
"CHARTS_READ_FAILED",
|
|
1228
|
+
api_error,
|
|
1229
|
+
normalized_args={"app_key": app_key},
|
|
1230
|
+
details={"app_key": app_key},
|
|
1231
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1232
|
+
)
|
|
1233
|
+
charts = _summarize_charts(items)
|
|
1234
|
+
response = AppChartsReadResponse(
|
|
1235
|
+
app_key=resolved_app_key,
|
|
1236
|
+
charts=charts,
|
|
1237
|
+
chart_count=len(charts),
|
|
1238
|
+
)
|
|
1239
|
+
return {
|
|
1240
|
+
"status": "success",
|
|
1241
|
+
"error_code": None,
|
|
1242
|
+
"recoverable": False,
|
|
1243
|
+
"message": "read app charts summary",
|
|
1244
|
+
"normalized_args": {"app_key": resolved_app_key},
|
|
1245
|
+
"missing_fields": [],
|
|
1246
|
+
"allowed_values": {},
|
|
1247
|
+
"details": {},
|
|
1248
|
+
"request_id": None,
|
|
1249
|
+
"suggested_next_call": None,
|
|
1250
|
+
"noop": False,
|
|
1251
|
+
"warnings": [],
|
|
1252
|
+
"verification": {"app_exists": True},
|
|
1253
|
+
"verified": True,
|
|
1254
|
+
**response.model_dump(mode="json"),
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
1258
|
+
try:
|
|
1259
|
+
result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft).get("result") or {}
|
|
1260
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
1261
|
+
api_error = _coerce_api_error(error)
|
|
1262
|
+
return _failed_from_api_error(
|
|
1263
|
+
"PORTAL_READ_FAILED",
|
|
1264
|
+
api_error,
|
|
1265
|
+
normalized_args={"dash_key": dash_key, "being_draft": being_draft},
|
|
1266
|
+
details={"dash_key": dash_key, "being_draft": being_draft},
|
|
1267
|
+
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, "dash_key": dash_key, "being_draft": being_draft}},
|
|
1268
|
+
)
|
|
1269
|
+
response = PortalReadSummaryResponse(
|
|
1270
|
+
dash_key=dash_key,
|
|
1271
|
+
being_draft=being_draft,
|
|
1272
|
+
dash_name=str(result.get("dashName") or "").strip() or None,
|
|
1273
|
+
package_tag_ids=[
|
|
1274
|
+
tag_id
|
|
1275
|
+
for tag_id in (
|
|
1276
|
+
_coerce_positive_int((item or {}).get("tagId"))
|
|
1277
|
+
for item in (result.get("tags") or [])
|
|
1278
|
+
if isinstance(item, dict)
|
|
1279
|
+
)
|
|
1280
|
+
if tag_id is not None
|
|
1281
|
+
],
|
|
1282
|
+
dash_icon=str(result.get("dashIcon") or "").strip() or None,
|
|
1283
|
+
hide_copyright=bool(result.get("hideCopyright")) if "hideCopyright" in result else None,
|
|
1284
|
+
config_keys=sorted(str(key) for key in (result.get("config") or {}).keys()) if isinstance(result.get("config"), dict) else [],
|
|
1285
|
+
dash_global_config_keys=sorted(str(key) for key in (result.get("dashGlobalConfig") or {}).keys()) if isinstance(result.get("dashGlobalConfig"), dict) else [],
|
|
1286
|
+
section_count=len(result.get("components") or []) if isinstance(result.get("components"), list) else 0,
|
|
1287
|
+
sections=_summarize_portal_sections(result.get("components")),
|
|
1288
|
+
)
|
|
1289
|
+
return {
|
|
1290
|
+
"status": "success",
|
|
1291
|
+
"error_code": None,
|
|
1292
|
+
"recoverable": False,
|
|
1293
|
+
"message": "read portal summary",
|
|
1294
|
+
"normalized_args": {"dash_key": dash_key, "being_draft": being_draft},
|
|
1295
|
+
"missing_fields": [],
|
|
1296
|
+
"allowed_values": {},
|
|
1297
|
+
"details": {},
|
|
1298
|
+
"request_id": None,
|
|
1299
|
+
"suggested_next_call": None,
|
|
1300
|
+
"noop": False,
|
|
1301
|
+
"warnings": [],
|
|
1302
|
+
"verification": {"portal_exists": True, "being_draft": being_draft},
|
|
1303
|
+
"verified": True,
|
|
1198
1304
|
**response.model_dump(mode="json"),
|
|
1199
1305
|
}
|
|
1200
1306
|
|
|
@@ -2843,6 +2949,7 @@ class AiBuilderFacade:
|
|
|
2843
2949
|
"request_id": None,
|
|
2844
2950
|
"suggested_next_call": None,
|
|
2845
2951
|
"noop": True,
|
|
2952
|
+
"warnings": [],
|
|
2846
2953
|
"verification": {"published": True, "package_attached": package_already_attached, "views_ok": True},
|
|
2847
2954
|
"app_key": app_key,
|
|
2848
2955
|
"published": True,
|
|
@@ -2892,6 +2999,11 @@ class AiBuilderFacade:
|
|
|
2892
2999
|
views = views or []
|
|
2893
3000
|
views_ok = isinstance(views, list) and not views_unavailable
|
|
2894
3001
|
verified = bool(base.get("appPublishStatus") in {1, 2}) and (package_attached is not False) and views_ok
|
|
3002
|
+
warnings = _publish_verify_warnings(
|
|
3003
|
+
package_attached=package_attached,
|
|
3004
|
+
views_unavailable=views_unavailable,
|
|
3005
|
+
verified=verified,
|
|
3006
|
+
)
|
|
2895
3007
|
return {
|
|
2896
3008
|
"status": "success" if verified else "partial_success",
|
|
2897
3009
|
"error_code": None if not views_unavailable else "VIEWS_READBACK_PENDING",
|
|
@@ -2909,6 +3021,7 @@ class AiBuilderFacade:
|
|
|
2909
3021
|
"arguments": {"profile": profile, "tag_id": expected_package_tag_id, "app_key": app_key},
|
|
2910
3022
|
},
|
|
2911
3023
|
"noop": False,
|
|
3024
|
+
"warnings": warnings,
|
|
2912
3025
|
"verification": {"published": bool(base.get("appPublishStatus") in {1, 2}), "package_attached": package_attached, "views_ok": views_ok, "views_read_unavailable": views_unavailable},
|
|
2913
3026
|
"app_key": app_key,
|
|
2914
3027
|
"published": bool(base.get("appPublishStatus") in {1, 2}),
|
|
@@ -2937,7 +3050,7 @@ class AiBuilderFacade:
|
|
|
2937
3050
|
api_error,
|
|
2938
3051
|
normalized_args=normalized_args,
|
|
2939
3052
|
details={"app_key": app_key},
|
|
2940
|
-
suggested_next_call={"tool_name": "
|
|
3053
|
+
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2941
3054
|
)
|
|
2942
3055
|
|
|
2943
3056
|
field_lookup = _build_public_field_lookup(fields)
|
|
@@ -2951,11 +3064,14 @@ class AiBuilderFacade:
|
|
|
2951
3064
|
for item in existing_chart_items
|
|
2952
3065
|
if isinstance(item, dict) and _extract_chart_identifier(item)
|
|
2953
3066
|
}
|
|
2954
|
-
existing_by_name = {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3067
|
+
existing_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
3068
|
+
for item in existing_chart_items:
|
|
3069
|
+
if not isinstance(item, dict):
|
|
3070
|
+
continue
|
|
3071
|
+
item_name = str(item.get("chartName") or "").strip()
|
|
3072
|
+
if not item_name:
|
|
3073
|
+
continue
|
|
3074
|
+
existing_by_name.setdefault(item_name, []).append(deepcopy(item))
|
|
2959
3075
|
|
|
2960
3076
|
chart_results: list[dict[str, Any]] = []
|
|
2961
3077
|
created_ids: list[str] = []
|
|
@@ -2968,15 +3084,24 @@ class AiBuilderFacade:
|
|
|
2968
3084
|
existing = None
|
|
2969
3085
|
if patch.chart_id:
|
|
2970
3086
|
existing = existing_by_id.get(str(patch.chart_id))
|
|
3087
|
+
if existing is None:
|
|
3088
|
+
raise ValueError(f"chart_id '{patch.chart_id}' was not found under app '{app_key}'")
|
|
2971
3089
|
if existing is None:
|
|
2972
|
-
|
|
2973
|
-
|
|
3090
|
+
name_matches = list(existing_by_name.get(patch.name) or [])
|
|
3091
|
+
if len(name_matches) > 1:
|
|
3092
|
+
raise ValueError(
|
|
3093
|
+
f"chart name '{patch.name}' is ambiguous under app '{app_key}'; supply chart_id to target an exact chart"
|
|
3094
|
+
)
|
|
3095
|
+
if name_matches:
|
|
3096
|
+
existing = deepcopy(name_matches[0])
|
|
3097
|
+
chart_id = _extract_chart_identifier(existing or {}) or str(patch.chart_id or "")
|
|
2974
3098
|
existing_name = str((existing or {}).get("chartName") or "").strip()
|
|
2975
3099
|
existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
|
|
2976
3100
|
target_type = _map_public_chart_type_to_backend(patch.chart_type)
|
|
2977
3101
|
if existing is None:
|
|
3102
|
+
temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
|
|
2978
3103
|
create_payload = {
|
|
2979
|
-
"chartId":
|
|
3104
|
+
"chartId": temp_chart_id,
|
|
2980
3105
|
"chartName": patch.name,
|
|
2981
3106
|
"chartType": target_type,
|
|
2982
3107
|
"dataSourceType": "qingflow",
|
|
@@ -2992,15 +3117,30 @@ class AiBuilderFacade:
|
|
|
2992
3117
|
created_chart_id = _extract_chart_identifier(create_result or {})
|
|
2993
3118
|
if not created_chart_id:
|
|
2994
3119
|
refreshed_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
2995
|
-
|
|
3120
|
+
refreshed_matches = _find_charts_by_name(
|
|
2996
3121
|
refreshed_items,
|
|
2997
3122
|
chart_name=patch.name,
|
|
2998
3123
|
chart_type=target_type,
|
|
2999
3124
|
)
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3125
|
+
if len(refreshed_matches) == 1:
|
|
3126
|
+
created_chart_id = _extract_chart_identifier(refreshed_matches[0])
|
|
3127
|
+
elif len(refreshed_matches) > 1:
|
|
3128
|
+
raise ValueError(
|
|
3129
|
+
f"created chart '{patch.name}' could not be uniquely resolved from readback; supply chart_id on the next update"
|
|
3130
|
+
)
|
|
3131
|
+
if not created_chart_id:
|
|
3132
|
+
raise ValueError(
|
|
3133
|
+
f"created chart '{patch.name}' did not return a real chart_id and could not be confirmed from readback"
|
|
3134
|
+
)
|
|
3135
|
+
chart_id = created_chart_id
|
|
3003
3136
|
created_ids.append(chart_id)
|
|
3137
|
+
created_chart = {
|
|
3138
|
+
"chartId": chart_id,
|
|
3139
|
+
"chartName": patch.name,
|
|
3140
|
+
"chartType": target_type,
|
|
3141
|
+
}
|
|
3142
|
+
existing_by_id[chart_id] = deepcopy(created_chart)
|
|
3143
|
+
existing_by_name.setdefault(patch.name, []).append(deepcopy(created_chart))
|
|
3004
3144
|
elif existing_name != patch.name or existing_type != target_type:
|
|
3005
3145
|
self.charts.qingbi_report_update_base(
|
|
3006
3146
|
profile=profile,
|
|
@@ -3008,6 +3148,19 @@ class AiBuilderFacade:
|
|
|
3008
3148
|
payload={"chartName": patch.name, "chartType": target_type},
|
|
3009
3149
|
)
|
|
3010
3150
|
updated_ids.append(chart_id)
|
|
3151
|
+
updated_chart = deepcopy(existing or {})
|
|
3152
|
+
updated_chart["chartId"] = chart_id
|
|
3153
|
+
updated_chart["chartName"] = patch.name
|
|
3154
|
+
updated_chart["chartType"] = target_type
|
|
3155
|
+
existing_by_id[chart_id] = deepcopy(updated_chart)
|
|
3156
|
+
old_name = existing_name
|
|
3157
|
+
if old_name and old_name in existing_by_name:
|
|
3158
|
+
existing_by_name[old_name] = [
|
|
3159
|
+
item for item in existing_by_name[old_name] if _extract_chart_identifier(item) != chart_id
|
|
3160
|
+
]
|
|
3161
|
+
if not existing_by_name[old_name]:
|
|
3162
|
+
existing_by_name.pop(old_name, None)
|
|
3163
|
+
existing_by_name.setdefault(patch.name, []).append(deepcopy(updated_chart))
|
|
3011
3164
|
|
|
3012
3165
|
config_payload = _build_public_chart_config_payload(
|
|
3013
3166
|
patch=patch,
|
|
@@ -3127,37 +3280,54 @@ class AiBuilderFacade:
|
|
|
3127
3280
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3128
3281
|
"details": {"per_chart_results": chart_results},
|
|
3129
3282
|
"request_id": failed_items[0].get("request_id"),
|
|
3130
|
-
"suggested_next_call": {"tool_name": "
|
|
3283
|
+
"suggested_next_call": {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3131
3284
|
"backend_code": failed_items[0].get("backend_code"),
|
|
3132
3285
|
"http_status": failed_items[0].get("http_status"),
|
|
3133
3286
|
"noop": noop,
|
|
3287
|
+
"warnings": _chart_apply_warnings(
|
|
3288
|
+
failed_items=failed_items,
|
|
3289
|
+
readback_unavailable=readback_unavailable,
|
|
3290
|
+
verified=False if failed_items else verified,
|
|
3291
|
+
),
|
|
3134
3292
|
"verification": {"charts_verified": False if failed_items else verified, "readback_unavailable": readback_unavailable},
|
|
3135
3293
|
"app_key": app_key,
|
|
3136
3294
|
"chart_results": chart_results,
|
|
3137
3295
|
"verified": False if failed_items else verified,
|
|
3138
3296
|
}
|
|
3297
|
+
result_verified = verified or noop
|
|
3139
3298
|
return {
|
|
3140
|
-
"status": "success" if
|
|
3141
|
-
"error_code": None if
|
|
3142
|
-
"recoverable": not
|
|
3143
|
-
"message": "applied chart operations" if verified
|
|
3299
|
+
"status": "success" if result_verified else "partial_success",
|
|
3300
|
+
"error_code": None if result_verified else "CHART_READBACK_PENDING",
|
|
3301
|
+
"recoverable": not result_verified,
|
|
3302
|
+
"message": "no chart changes requested" if noop else ("applied chart operations" if verified else "applied chart operations; readback pending"),
|
|
3144
3303
|
"normalized_args": normalized_args,
|
|
3145
3304
|
"missing_fields": [],
|
|
3146
3305
|
"allowed_values": {"chart.chart_type": [member.value for member in PublicChartType], "chart.filter.operator": [member.value for member in ViewFilterOperator]},
|
|
3147
3306
|
"details": {},
|
|
3148
3307
|
"request_id": None,
|
|
3149
|
-
"suggested_next_call": None if
|
|
3308
|
+
"suggested_next_call": None if result_verified else {"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3150
3309
|
"noop": noop,
|
|
3151
|
-
"
|
|
3310
|
+
"warnings": _chart_apply_warnings(
|
|
3311
|
+
failed_items=[],
|
|
3312
|
+
readback_unavailable=False if noop else readback_unavailable,
|
|
3313
|
+
verified=result_verified,
|
|
3314
|
+
),
|
|
3315
|
+
"verification": {"charts_verified": result_verified, "readback_unavailable": False if noop else readback_unavailable},
|
|
3152
3316
|
"app_key": app_key,
|
|
3153
3317
|
"chart_results": chart_results,
|
|
3154
|
-
"verified":
|
|
3318
|
+
"verified": result_verified,
|
|
3155
3319
|
}
|
|
3156
3320
|
|
|
3157
3321
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
3158
3322
|
normalized_args = request.model_dump(mode="json")
|
|
3159
3323
|
dash_key = str(request.dash_key or "").strip()
|
|
3160
3324
|
creating = not dash_key
|
|
3325
|
+
verify_dash_name = creating or request.dash_name is not None
|
|
3326
|
+
verify_dash_icon = bool(request.icon or request.color)
|
|
3327
|
+
verify_auth = request.auth is not None
|
|
3328
|
+
verify_hide_copyright = request.hide_copyright is not None
|
|
3329
|
+
verify_dash_global_config = request.dash_global_config is not None
|
|
3330
|
+
verify_tags = creating or request.package_tag_id is not None
|
|
3161
3331
|
try:
|
|
3162
3332
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
3163
3333
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -3258,16 +3428,59 @@ class AiBuilderFacade:
|
|
|
3258
3428
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
3259
3429
|
expected_count = len(request.sections)
|
|
3260
3430
|
draft_verified = isinstance(draft_components, list) and len(draft_components) == expected_count
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3431
|
+
draft_meta_verified, draft_meta_mismatches = _verify_portal_readback(
|
|
3432
|
+
actual=draft_result,
|
|
3433
|
+
expected_payload=update_payload,
|
|
3434
|
+
expected_section_count=expected_count,
|
|
3435
|
+
requested_config_keys=set((request.config or {}).keys()),
|
|
3436
|
+
verify_dash_name=verify_dash_name,
|
|
3437
|
+
verify_dash_icon=verify_dash_icon,
|
|
3438
|
+
verify_auth=verify_auth,
|
|
3439
|
+
verify_hide_copyright=verify_hide_copyright,
|
|
3440
|
+
verify_dash_global_config=verify_dash_global_config,
|
|
3441
|
+
verify_tags=verify_tags,
|
|
3442
|
+
)
|
|
3443
|
+
live_verified: bool | None = None
|
|
3444
|
+
live_meta_verified: bool | None = None
|
|
3445
|
+
live_meta_mismatches: list[str] = []
|
|
3446
|
+
if request.publish:
|
|
3447
|
+
live_verified = (
|
|
3448
|
+
isinstance(live_result, dict)
|
|
3449
|
+
and isinstance(live_result.get("components"), list)
|
|
3450
|
+
and len(live_result.get("components")) == expected_count
|
|
3451
|
+
)
|
|
3452
|
+
live_meta_verified, live_meta_mismatches = _verify_portal_readback(
|
|
3453
|
+
actual=live_result,
|
|
3454
|
+
expected_payload=update_payload,
|
|
3455
|
+
expected_section_count=expected_count,
|
|
3456
|
+
requested_config_keys=set((request.config or {}).keys()),
|
|
3457
|
+
verify_dash_name=verify_dash_name,
|
|
3458
|
+
verify_dash_icon=verify_dash_icon,
|
|
3459
|
+
verify_auth=verify_auth,
|
|
3460
|
+
verify_hide_copyright=verify_hide_copyright,
|
|
3461
|
+
verify_dash_global_config=verify_dash_global_config,
|
|
3462
|
+
verify_tags=verify_tags,
|
|
3463
|
+
)
|
|
3464
|
+
verified = (
|
|
3465
|
+
draft_verified
|
|
3466
|
+
and draft_meta_verified
|
|
3467
|
+
and (published if request.publish else True)
|
|
3468
|
+
and (live_verified if request.publish else True)
|
|
3469
|
+
and (live_meta_verified if request.publish else True)
|
|
3470
|
+
and not publish_failed
|
|
3265
3471
|
)
|
|
3266
|
-
verified = draft_verified and live_verified and (published if request.publish else True) and not publish_failed
|
|
3267
3472
|
status = "success" if verified else "partial_success"
|
|
3268
3473
|
error_code = None if verified else "PORTAL_READBACK_PENDING"
|
|
3269
3474
|
if publish_failed:
|
|
3270
3475
|
error_code = "PORTAL_PUBLISH_FAILED"
|
|
3476
|
+
warnings = _portal_apply_warnings(
|
|
3477
|
+
publish_failed=publish_failed,
|
|
3478
|
+
draft_verified=draft_verified,
|
|
3479
|
+
draft_meta_verified=draft_meta_verified,
|
|
3480
|
+
live_verified=live_verified,
|
|
3481
|
+
live_meta_verified=live_meta_verified,
|
|
3482
|
+
publish_requested=request.publish,
|
|
3483
|
+
)
|
|
3271
3484
|
return {
|
|
3272
3485
|
"status": status,
|
|
3273
3486
|
"error_code": error_code,
|
|
@@ -3276,13 +3489,21 @@ class AiBuilderFacade:
|
|
|
3276
3489
|
"normalized_args": normalized_args,
|
|
3277
3490
|
"missing_fields": [],
|
|
3278
3491
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
3279
|
-
"details": {
|
|
3492
|
+
"details": {
|
|
3493
|
+
"verification_mismatches": {
|
|
3494
|
+
"draft": draft_meta_mismatches,
|
|
3495
|
+
"live": live_meta_mismatches,
|
|
3496
|
+
}
|
|
3497
|
+
},
|
|
3280
3498
|
"request_id": None,
|
|
3281
3499
|
"suggested_next_call": None if verified else {"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3282
3500
|
"noop": False,
|
|
3501
|
+
"warnings": warnings,
|
|
3283
3502
|
"verification": {
|
|
3284
3503
|
"draft_verified": draft_verified,
|
|
3504
|
+
"draft_metadata_verified": draft_meta_verified,
|
|
3285
3505
|
"live_verified": live_verified,
|
|
3506
|
+
"live_metadata_verified": live_meta_verified,
|
|
3286
3507
|
"published": published,
|
|
3287
3508
|
"publish_failed": publish_failed,
|
|
3288
3509
|
},
|
|
@@ -3321,9 +3542,11 @@ class AiBuilderFacade:
|
|
|
3321
3542
|
"request_id": None,
|
|
3322
3543
|
"suggested_next_call": None,
|
|
3323
3544
|
"noop": False,
|
|
3545
|
+
"warnings": [],
|
|
3324
3546
|
"verification": {"published": True},
|
|
3325
3547
|
"app_key": app_key,
|
|
3326
3548
|
"published": True,
|
|
3549
|
+
"verified": True,
|
|
3327
3550
|
}
|
|
3328
3551
|
|
|
3329
3552
|
def _resolve_form_edit_version(self, *, profile: str, app_key: str, current_schema: dict[str, Any]) -> int:
|
|
@@ -3338,6 +3561,10 @@ class AiBuilderFacade:
|
|
|
3338
3561
|
if not isinstance(verification, dict):
|
|
3339
3562
|
verification = {}
|
|
3340
3563
|
response["verification"] = verification
|
|
3564
|
+
warnings = response.get("warnings")
|
|
3565
|
+
if not isinstance(warnings, list):
|
|
3566
|
+
warnings = []
|
|
3567
|
+
response["warnings"] = warnings
|
|
3341
3568
|
if bool(response.get("noop")):
|
|
3342
3569
|
response["publish_requested"] = False
|
|
3343
3570
|
response["publish_skipped"] = bool(publish)
|
|
@@ -3354,6 +3581,7 @@ class AiBuilderFacade:
|
|
|
3354
3581
|
response["published"] = bool(publish_result.get("published"))
|
|
3355
3582
|
verification["published"] = bool(publish_result.get("published"))
|
|
3356
3583
|
if publish_result.get("status") == "failed":
|
|
3584
|
+
warnings.append(_warning("PUBLISH_FAILED", "publish step failed after apply succeeded"))
|
|
3357
3585
|
response["status"] = "partial_success"
|
|
3358
3586
|
response["error_code"] = response.get("error_code") or publish_result.get("error_code")
|
|
3359
3587
|
response["recoverable"] = True
|
|
@@ -3766,7 +3994,9 @@ def _failed(
|
|
|
3766
3994
|
"backend_code": backend_code,
|
|
3767
3995
|
"http_status": http_status,
|
|
3768
3996
|
"noop": False,
|
|
3997
|
+
"warnings": [],
|
|
3769
3998
|
"verification": {},
|
|
3999
|
+
"verified": False,
|
|
3770
4000
|
}
|
|
3771
4001
|
|
|
3772
4002
|
|
|
@@ -3838,6 +4068,10 @@ def _from_stage_failure(stage: JSONObject, *, fallback_tool: str) -> JSONObject:
|
|
|
3838
4068
|
"request_id": stage.get("request_id"),
|
|
3839
4069
|
"backend_code": stage.get("backend_code"),
|
|
3840
4070
|
"http_status": stage.get("http_status"),
|
|
4071
|
+
"noop": False,
|
|
4072
|
+
"warnings": [],
|
|
4073
|
+
"verification": {},
|
|
4074
|
+
"verified": False,
|
|
3841
4075
|
}
|
|
3842
4076
|
|
|
3843
4077
|
|
|
@@ -4310,12 +4544,12 @@ def _normalize_backend_chart_type(value: Any) -> str:
|
|
|
4310
4544
|
return by_code.get(raw, raw.lower())
|
|
4311
4545
|
|
|
4312
4546
|
|
|
4313
|
-
def
|
|
4547
|
+
def _find_charts_by_name(items: Any, *, chart_name: str, chart_type: str | None = None) -> list[dict[str, Any]]:
|
|
4314
4548
|
target_name = str(chart_name or "").strip()
|
|
4315
4549
|
target_type = _normalize_backend_chart_type(chart_type)
|
|
4316
4550
|
candidates: list[dict[str, Any]] = []
|
|
4317
4551
|
if not isinstance(items, list) or not target_name:
|
|
4318
|
-
return
|
|
4552
|
+
return []
|
|
4319
4553
|
for item in items:
|
|
4320
4554
|
if not isinstance(item, dict):
|
|
4321
4555
|
continue
|
|
@@ -4325,7 +4559,12 @@ def _find_chart_by_name(items: Any, *, chart_name: str, chart_type: str | None =
|
|
|
4325
4559
|
item_type = _normalize_backend_chart_type(item.get("chartType"))
|
|
4326
4560
|
if target_type and item_type and item_type != target_type:
|
|
4327
4561
|
continue
|
|
4328
|
-
candidates.append(item)
|
|
4562
|
+
candidates.append(deepcopy(item))
|
|
4563
|
+
return candidates
|
|
4564
|
+
|
|
4565
|
+
|
|
4566
|
+
def _find_chart_by_name(items: Any, *, chart_name: str, chart_type: str | None = None) -> dict[str, Any] | None:
|
|
4567
|
+
candidates = _find_charts_by_name(items, chart_name=chart_name, chart_type=chart_type)
|
|
4329
4568
|
if not candidates:
|
|
4330
4569
|
return None
|
|
4331
4570
|
return deepcopy(candidates[-1])
|
|
@@ -4402,15 +4641,25 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
|
|
|
4402
4641
|
chart_id = str(getattr(ref, "chart_id", "") or "").strip()
|
|
4403
4642
|
chart_name = str(getattr(ref, "chart_name", "") or "").strip()
|
|
4404
4643
|
items = charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4644
|
+
if chart_id:
|
|
4645
|
+
for item in items:
|
|
4646
|
+
if not isinstance(item, dict):
|
|
4647
|
+
continue
|
|
4648
|
+
item_id = _extract_chart_identifier(item)
|
|
4649
|
+
item_name = str(item.get("chartName") or "").strip()
|
|
4650
|
+
if item_id == chart_id:
|
|
4651
|
+
return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key}
|
|
4652
|
+
raise ValueError(f"chart ref chart_id '{chart_id}' could not be resolved under app '{app_key}'")
|
|
4653
|
+
matches = _find_charts_by_name(items, chart_name=chart_name)
|
|
4654
|
+
if len(matches) > 1:
|
|
4655
|
+
raise ValueError(f"chart ref chart_name '{chart_name}' is ambiguous under app '{app_key}'; supply chart_id")
|
|
4656
|
+
if len(matches) == 1:
|
|
4657
|
+
item = matches[0]
|
|
4658
|
+
return {
|
|
4659
|
+
"chart_id": _extract_chart_identifier(item),
|
|
4660
|
+
"chart_name": str(item.get("chartName") or "").strip(),
|
|
4661
|
+
"app_key": app_key,
|
|
4662
|
+
}
|
|
4414
4663
|
raise ValueError(f"chart ref could not be resolved under app '{app_key}'")
|
|
4415
4664
|
|
|
4416
4665
|
|
|
@@ -4419,18 +4668,98 @@ def _resolve_view_reference(*, facade: AiBuilderFacade, profile: str, ref: Any)
|
|
|
4419
4668
|
view_key = str(getattr(ref, "view_key", "") or "").strip()
|
|
4420
4669
|
view_name = str(getattr(ref, "view_name", "") or "").strip()
|
|
4421
4670
|
views, _ = facade._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4671
|
+
if view_key:
|
|
4672
|
+
for item in views or []:
|
|
4673
|
+
if not isinstance(item, dict):
|
|
4674
|
+
continue
|
|
4675
|
+
item_key = _extract_view_key(item)
|
|
4676
|
+
item_name = _extract_view_name(item)
|
|
4677
|
+
if item_key == view_key:
|
|
4678
|
+
return {"view_key": item_key, "view_name": item_name, "app_key": app_key, "view_type": _normalize_portal_view_type(item)}
|
|
4679
|
+
raise ValueError(f"view ref view_key '{view_key}' could not be resolved under app '{app_key}'")
|
|
4680
|
+
matched_items = [
|
|
4681
|
+
item
|
|
4682
|
+
for item in (views or [])
|
|
4683
|
+
if isinstance(item, dict) and _extract_view_name(item) == view_name
|
|
4684
|
+
]
|
|
4685
|
+
if len(matched_items) > 1:
|
|
4686
|
+
raise ValueError(f"view ref view_name '{view_name}' is ambiguous under app '{app_key}'; supply view_key")
|
|
4687
|
+
if len(matched_items) == 1:
|
|
4688
|
+
item = matched_items[0]
|
|
4689
|
+
return {
|
|
4690
|
+
"view_key": _extract_view_key(item),
|
|
4691
|
+
"view_name": _extract_view_name(item),
|
|
4692
|
+
"app_key": app_key,
|
|
4693
|
+
"view_type": _normalize_portal_view_type(item),
|
|
4694
|
+
}
|
|
4431
4695
|
raise ValueError(f"view ref could not be resolved under app '{app_key}'")
|
|
4432
4696
|
|
|
4433
4697
|
|
|
4698
|
+
def _verify_portal_readback(
|
|
4699
|
+
*,
|
|
4700
|
+
actual: Any,
|
|
4701
|
+
expected_payload: dict[str, Any],
|
|
4702
|
+
expected_section_count: int,
|
|
4703
|
+
requested_config_keys: set[str],
|
|
4704
|
+
verify_dash_name: bool,
|
|
4705
|
+
verify_dash_icon: bool,
|
|
4706
|
+
verify_auth: bool,
|
|
4707
|
+
verify_hide_copyright: bool,
|
|
4708
|
+
verify_dash_global_config: bool,
|
|
4709
|
+
verify_tags: bool,
|
|
4710
|
+
) -> tuple[bool, list[str]]:
|
|
4711
|
+
mismatches: list[str] = []
|
|
4712
|
+
if not isinstance(actual, dict):
|
|
4713
|
+
return False, ["portal readback payload is unavailable"]
|
|
4714
|
+
components = actual.get("components")
|
|
4715
|
+
if not isinstance(components, list) or len(components) != expected_section_count:
|
|
4716
|
+
mismatches.append(f"components expected {expected_section_count}, got {len(components) if isinstance(components, list) else 'unavailable'}")
|
|
4717
|
+
if verify_dash_name and str(actual.get("dashName") or "").strip() != str(expected_payload.get("dashName") or "").strip():
|
|
4718
|
+
mismatches.append("dash_name")
|
|
4719
|
+
if verify_dash_icon and str(actual.get("dashIcon") or "") != str(expected_payload.get("dashIcon") or ""):
|
|
4720
|
+
mismatches.append("dash_icon")
|
|
4721
|
+
if verify_auth and not _mapping_contains(actual.get("auth"), expected_payload.get("auth")):
|
|
4722
|
+
mismatches.append("auth")
|
|
4723
|
+
if verify_hide_copyright and bool(actual.get("hideCopyright", False)) != bool(expected_payload.get("hideCopyright", False)):
|
|
4724
|
+
mismatches.append("hide_copyright")
|
|
4725
|
+
if verify_dash_global_config and not _mapping_contains(actual.get("dashGlobalConfig") or {}, expected_payload.get("dashGlobalConfig") or {}):
|
|
4726
|
+
mismatches.append("dash_global_config")
|
|
4727
|
+
if verify_tags:
|
|
4728
|
+
expected_tags = [
|
|
4729
|
+
_coerce_positive_int((item or {}).get("tagId"))
|
|
4730
|
+
for item in (expected_payload.get("tags") or [])
|
|
4731
|
+
if isinstance(item, dict)
|
|
4732
|
+
]
|
|
4733
|
+
actual_tags = [
|
|
4734
|
+
_coerce_positive_int((item or {}).get("tagId"))
|
|
4735
|
+
for item in (actual.get("tags") or [])
|
|
4736
|
+
if isinstance(item, dict)
|
|
4737
|
+
]
|
|
4738
|
+
if [item for item in expected_tags if item is not None] != [item for item in actual_tags if item is not None]:
|
|
4739
|
+
mismatches.append("tags")
|
|
4740
|
+
for key in sorted(requested_config_keys):
|
|
4741
|
+
if deepcopy(actual.get(key)) != deepcopy(expected_payload.get(key)):
|
|
4742
|
+
mismatches.append(f"config.{key}")
|
|
4743
|
+
return not mismatches, mismatches
|
|
4744
|
+
|
|
4745
|
+
|
|
4746
|
+
def _mapping_contains(actual: Any, expected: Any) -> bool:
|
|
4747
|
+
if isinstance(expected, dict):
|
|
4748
|
+
if not isinstance(actual, dict):
|
|
4749
|
+
return False
|
|
4750
|
+
for key, expected_value in expected.items():
|
|
4751
|
+
if key not in actual:
|
|
4752
|
+
return False
|
|
4753
|
+
if not _mapping_contains(actual.get(key), expected_value):
|
|
4754
|
+
return False
|
|
4755
|
+
return True
|
|
4756
|
+
if isinstance(expected, list):
|
|
4757
|
+
if not isinstance(actual, list) or len(actual) < len(expected):
|
|
4758
|
+
return False
|
|
4759
|
+
return all(_mapping_contains(actual[index], expected[index]) for index in range(len(expected)))
|
|
4760
|
+
return deepcopy(actual) == deepcopy(expected)
|
|
4761
|
+
|
|
4762
|
+
|
|
4434
4763
|
def _normalize_portal_view_type(view: dict[str, Any]) -> str:
|
|
4435
4764
|
raw = str(view.get("viewgraphType") or view.get("type") or "").strip()
|
|
4436
4765
|
mapping = {
|
|
@@ -4895,6 +5224,70 @@ def _build_verification_hints(
|
|
|
4895
5224
|
return hints
|
|
4896
5225
|
|
|
4897
5226
|
|
|
5227
|
+
def _warning(code: str, message: str, **extra: Any) -> dict[str, Any]:
|
|
5228
|
+
warning = {"code": code, "message": message}
|
|
5229
|
+
warning.update({key: value for key, value in extra.items() if value is not None})
|
|
5230
|
+
return warning
|
|
5231
|
+
|
|
5232
|
+
|
|
5233
|
+
def _warnings_from_verification_hints(hints: list[str]) -> list[dict[str, Any]]:
|
|
5234
|
+
mapping = {
|
|
5235
|
+
"package attachment not verified": _warning("PACKAGE_ATTACHMENT_UNVERIFIED", "package attachment is not verified"),
|
|
5236
|
+
"layout has unplaced fields": _warning("LAYOUT_HAS_UNPLACED_FIELDS", "layout still contains unplaced fields"),
|
|
5237
|
+
"no public views detected": _warning("NO_PUBLIC_VIEWS", "no public views were detected"),
|
|
5238
|
+
"views_read_unavailable": _warning("VIEWS_READ_UNAVAILABLE", "views summary readback is unavailable"),
|
|
5239
|
+
"workflow_read_unavailable": _warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable"),
|
|
5240
|
+
}
|
|
5241
|
+
return [deepcopy(mapping[hint]) for hint in hints if hint in mapping]
|
|
5242
|
+
|
|
5243
|
+
|
|
5244
|
+
def _layout_read_warnings(unplaced_fields: list[str]) -> list[dict[str, Any]]:
|
|
5245
|
+
if not unplaced_fields:
|
|
5246
|
+
return []
|
|
5247
|
+
return [_warning("LAYOUT_HAS_UNPLACED_FIELDS", "layout still contains unplaced fields", fields=unplaced_fields)]
|
|
5248
|
+
|
|
5249
|
+
|
|
5250
|
+
def _publish_verify_warnings(*, package_attached: bool | None, views_unavailable: bool, verified: bool) -> list[dict[str, Any]]:
|
|
5251
|
+
warnings: list[dict[str, Any]] = []
|
|
5252
|
+
if package_attached is False:
|
|
5253
|
+
warnings.append(_warning("PACKAGE_NOT_ATTACHED", "target package is not attached after publish verification"))
|
|
5254
|
+
if views_unavailable:
|
|
5255
|
+
warnings.append(_warning("VIEWS_READBACK_PENDING", "views readback is unavailable during publish verification"))
|
|
5256
|
+
if not verified and not warnings:
|
|
5257
|
+
warnings.append(_warning("PUBLISH_VERIFY_INCOMPLETE", "publish verification is incomplete"))
|
|
5258
|
+
return warnings
|
|
5259
|
+
|
|
5260
|
+
|
|
5261
|
+
def _chart_apply_warnings(*, failed_items: list[dict[str, Any]], readback_unavailable: bool, verified: bool) -> list[dict[str, Any]]:
|
|
5262
|
+
warnings: list[dict[str, Any]] = []
|
|
5263
|
+
if failed_items:
|
|
5264
|
+
warnings.append(_warning("CHART_OPERATION_FAILED", "one or more chart operations failed", failed_count=len(failed_items)))
|
|
5265
|
+
if readback_unavailable:
|
|
5266
|
+
warnings.append(_warning("CHART_READBACK_PENDING", "chart readback is unavailable after apply"))
|
|
5267
|
+
elif not verified and not failed_items:
|
|
5268
|
+
warnings.append(_warning("CHART_VERIFICATION_INCOMPLETE", "chart apply completed but verification is incomplete"))
|
|
5269
|
+
return warnings
|
|
5270
|
+
|
|
5271
|
+
|
|
5272
|
+
def _portal_apply_warnings(
|
|
5273
|
+
*,
|
|
5274
|
+
publish_failed: bool,
|
|
5275
|
+
draft_verified: bool,
|
|
5276
|
+
draft_meta_verified: bool,
|
|
5277
|
+
live_verified: bool | None,
|
|
5278
|
+
live_meta_verified: bool | None,
|
|
5279
|
+
publish_requested: bool,
|
|
5280
|
+
) -> list[dict[str, Any]]:
|
|
5281
|
+
warnings: list[dict[str, Any]] = []
|
|
5282
|
+
if publish_failed:
|
|
5283
|
+
warnings.append(_warning("PORTAL_PUBLISH_FAILED", "portal publish failed after draft update"))
|
|
5284
|
+
if not draft_verified or not draft_meta_verified:
|
|
5285
|
+
warnings.append(_warning("PORTAL_DRAFT_VERIFICATION_INCOMPLETE", "portal draft verification is incomplete"))
|
|
5286
|
+
if publish_requested and (live_verified is False or live_meta_verified is False):
|
|
5287
|
+
warnings.append(_warning("PORTAL_LIVE_VERIFICATION_INCOMPLETE", "portal live verification is incomplete"))
|
|
5288
|
+
return warnings
|
|
5289
|
+
|
|
5290
|
+
|
|
4898
5291
|
def _build_layout_preset_sections(*, preset: LayoutPreset, field_names: list[str]) -> list[dict[str, Any]]:
|
|
4899
5292
|
ordered = [name for name in field_names if name]
|
|
4900
5293
|
if preset == LayoutPreset.single_section:
|
|
@@ -5281,6 +5674,108 @@ def _summarize_views(result: Any) -> list[dict[str, Any]]:
|
|
|
5281
5674
|
return items
|
|
5282
5675
|
|
|
5283
5676
|
|
|
5677
|
+
def _summarize_charts(result: Any) -> list[dict[str, Any]]:
|
|
5678
|
+
if not isinstance(result, list):
|
|
5679
|
+
return []
|
|
5680
|
+
items: list[dict[str, Any]] = []
|
|
5681
|
+
for index, chart in enumerate(result):
|
|
5682
|
+
if not isinstance(chart, dict):
|
|
5683
|
+
continue
|
|
5684
|
+
chart_id = _extract_chart_identifier(chart)
|
|
5685
|
+
name = str(chart.get("chartName") or "").strip()
|
|
5686
|
+
chart_type = _normalize_backend_chart_type(chart.get("chartType"))
|
|
5687
|
+
if not any((chart_id, name, chart_type)):
|
|
5688
|
+
continue
|
|
5689
|
+
items.append(
|
|
5690
|
+
{
|
|
5691
|
+
"chart_id": chart_id or None,
|
|
5692
|
+
"name": name or None,
|
|
5693
|
+
"chart_type": chart_type or None,
|
|
5694
|
+
"order": index,
|
|
5695
|
+
}
|
|
5696
|
+
)
|
|
5697
|
+
return items
|
|
5698
|
+
|
|
5699
|
+
|
|
5700
|
+
def _summarize_portal_sections(components: Any) -> list[dict[str, Any]]:
|
|
5701
|
+
if not isinstance(components, list):
|
|
5702
|
+
return []
|
|
5703
|
+
items: list[dict[str, Any]] = []
|
|
5704
|
+
for index, component in enumerate(components):
|
|
5705
|
+
if not isinstance(component, dict):
|
|
5706
|
+
continue
|
|
5707
|
+
source_type = _normalize_portal_component_source_type(component.get("type"))
|
|
5708
|
+
title = _extract_portal_component_title(component, source_type=source_type)
|
|
5709
|
+
summary: dict[str, Any] = {
|
|
5710
|
+
"order": index,
|
|
5711
|
+
"source_type": source_type,
|
|
5712
|
+
"title": title,
|
|
5713
|
+
}
|
|
5714
|
+
position = component.get("position")
|
|
5715
|
+
if isinstance(position, dict):
|
|
5716
|
+
summary["position"] = deepcopy(position)
|
|
5717
|
+
if source_type == "chart":
|
|
5718
|
+
chart_config = component.get("chartConfig") if isinstance(component.get("chartConfig"), dict) else {}
|
|
5719
|
+
summary["chart_ref"] = {
|
|
5720
|
+
"chart_id": str(chart_config.get("biChartId") or "").strip() or None,
|
|
5721
|
+
"chart_name": str(chart_config.get("chartComponentTitle") or title or "").strip() or None,
|
|
5722
|
+
}
|
|
5723
|
+
elif source_type == "view":
|
|
5724
|
+
view_config = component.get("viewgraphConfig") if isinstance(component.get("viewgraphConfig"), dict) else {}
|
|
5725
|
+
summary["view_ref"] = {
|
|
5726
|
+
"app_key": str(view_config.get("appKey") or "").strip() or None,
|
|
5727
|
+
"view_key": str(view_config.get("viewgraphKey") or "").strip() or None,
|
|
5728
|
+
"view_name": str(view_config.get("viewgraphName") or title or "").strip() or None,
|
|
5729
|
+
}
|
|
5730
|
+
items.append(summary)
|
|
5731
|
+
return items
|
|
5732
|
+
|
|
5733
|
+
|
|
5734
|
+
def _normalize_portal_component_source_type(value: Any) -> str:
|
|
5735
|
+
raw = str(value or "").strip()
|
|
5736
|
+
mapping = {
|
|
5737
|
+
"2": "grid",
|
|
5738
|
+
"4": "link",
|
|
5739
|
+
"5": "text",
|
|
5740
|
+
"6": "filter",
|
|
5741
|
+
"9": "chart",
|
|
5742
|
+
"10": "view",
|
|
5743
|
+
"grid": "grid",
|
|
5744
|
+
"link": "link",
|
|
5745
|
+
"text": "text",
|
|
5746
|
+
"filter": "filter",
|
|
5747
|
+
"chart": "chart",
|
|
5748
|
+
"view": "view",
|
|
5749
|
+
}
|
|
5750
|
+
return mapping.get(raw, raw.lower() or "unknown")
|
|
5751
|
+
|
|
5752
|
+
|
|
5753
|
+
def _extract_portal_component_title(component: dict[str, Any], *, source_type: str) -> str | None:
|
|
5754
|
+
config_key_map = {
|
|
5755
|
+
"grid": "gridConfig",
|
|
5756
|
+
"link": "linkConfig",
|
|
5757
|
+
"text": "textConfig",
|
|
5758
|
+
"filter": "filterConfig",
|
|
5759
|
+
"chart": "chartConfig",
|
|
5760
|
+
"view": "viewgraphConfig",
|
|
5761
|
+
}
|
|
5762
|
+
config_key = config_key_map.get(source_type, "")
|
|
5763
|
+
config = component.get(config_key) if isinstance(component.get(config_key), dict) else {}
|
|
5764
|
+
title_candidates = {
|
|
5765
|
+
"grid": ["gridTitle"],
|
|
5766
|
+
"link": ["title", "linkTitle"],
|
|
5767
|
+
"text": ["title", "textTitle"],
|
|
5768
|
+
"filter": ["title", "filterTitle"],
|
|
5769
|
+
"chart": ["chartComponentTitle", "componentTitle", "title"],
|
|
5770
|
+
"view": ["componentTitle", "viewgraphName", "title"],
|
|
5771
|
+
}
|
|
5772
|
+
for key in title_candidates.get(source_type, []):
|
|
5773
|
+
title = str(config.get(key) or "").strip()
|
|
5774
|
+
if title:
|
|
5775
|
+
return title
|
|
5776
|
+
return None
|
|
5777
|
+
|
|
5778
|
+
|
|
5284
5779
|
def _entity_spec_from_app(*, base_info: dict[str, Any], schema: dict[str, Any], views: Any) -> dict[str, Any]:
|
|
5285
5780
|
parsed = _parse_schema(schema)
|
|
5286
5781
|
fields = []
|