@josephyan/qingflow-cli 0.2.0-beta.69 → 0.2.0-beta.71
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/backend_client.py +0 -1
- package/src/qingflow_mcp/builder_facade/models.py +34 -0
- package/src/qingflow_mcp/builder_facade/service.py +372 -17
- package/src/qingflow_mcp/cli/commands/builder.py +248 -1
- package/src/qingflow_mcp/cli/commands/common.py +15 -0
- package/src/qingflow_mcp/cli/commands/imports.py +12 -2
- package/src/qingflow_mcp/cli/commands/record.py +132 -32
- package/src/qingflow_mcp/cli/commands/workspace.py +1 -1
- package/src/qingflow_mcp/cli/formatters.py +52 -2
- package/src/qingflow_mcp/cli/main.py +7 -5
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server_app_builder.py +136 -8
- package/src/qingflow_mcp/server_app_user.py +55 -11
- package/src/qingflow_mcp/tools/ai_builder_tools.py +270 -5
- package/src/qingflow_mcp/tools/app_tools.py +29 -0
- package/src/qingflow_mcp/tools/auth_tools.py +259 -1
- package/src/qingflow_mcp/tools/import_tools.py +59 -7
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +75 -7
- package/src/qingflow_mcp/tools/record_tools.py +6 -12
- package/src/qingflow_mcp/tools/workspace_tools.py +124 -7
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-cli@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-cli@0.2.0-beta.71
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.71 qingflow
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josephyan/qingflow-cli",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.71",
|
|
4
4
|
"description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/pyproject.toml
CHANGED
|
@@ -1382,6 +1382,11 @@ class AppChartsReadResponse(StrictModel):
|
|
|
1382
1382
|
chart_count: int = 0
|
|
1383
1383
|
|
|
1384
1384
|
|
|
1385
|
+
class PortalListResponse(StrictModel):
|
|
1386
|
+
items: list[dict[str, Any]] = Field(default_factory=list)
|
|
1387
|
+
total: int = 0
|
|
1388
|
+
|
|
1389
|
+
|
|
1385
1390
|
class PortalReadSummaryResponse(StrictModel):
|
|
1386
1391
|
dash_key: str
|
|
1387
1392
|
being_draft: bool = True
|
|
@@ -1395,6 +1400,35 @@ class PortalReadSummaryResponse(StrictModel):
|
|
|
1395
1400
|
sections: list[dict[str, Any]] = Field(default_factory=list)
|
|
1396
1401
|
|
|
1397
1402
|
|
|
1403
|
+
class PortalGetResponse(StrictModel):
|
|
1404
|
+
dash_key: str
|
|
1405
|
+
being_draft: bool = True
|
|
1406
|
+
dash_name: str | None = None
|
|
1407
|
+
package_tag_ids: list[int] = Field(default_factory=list)
|
|
1408
|
+
dash_icon: str | None = None
|
|
1409
|
+
hide_copyright: bool | None = None
|
|
1410
|
+
auth: dict[str, Any] = Field(default_factory=dict)
|
|
1411
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1412
|
+
dash_global_config: dict[str, Any] = Field(default_factory=dict)
|
|
1413
|
+
component_count: int = 0
|
|
1414
|
+
components: list[dict[str, Any]] = Field(default_factory=list)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
class ViewGetResponse(StrictModel):
|
|
1418
|
+
viewgraph_key: str
|
|
1419
|
+
base_info: dict[str, Any] = Field(default_factory=dict)
|
|
1420
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1421
|
+
questions: list[dict[str, Any]] = Field(default_factory=list)
|
|
1422
|
+
associations: list[dict[str, Any]] = Field(default_factory=list)
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
class ChartGetResponse(StrictModel):
|
|
1426
|
+
chart_id: str
|
|
1427
|
+
base: dict[str, Any] = Field(default_factory=dict)
|
|
1428
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
1429
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
1430
|
+
|
|
1431
|
+
|
|
1398
1432
|
class SchemaPlanRequest(StrictModel):
|
|
1399
1433
|
app_key: str = ""
|
|
1400
1434
|
package_tag_id: int | None = None
|
|
@@ -34,8 +34,8 @@ from .models import (
|
|
|
34
34
|
AppChartsReadResponse,
|
|
35
35
|
AppFieldsReadResponse,
|
|
36
36
|
AppFlowReadResponse,
|
|
37
|
+
ChartGetResponse,
|
|
37
38
|
AppLayoutReadResponse,
|
|
38
|
-
PortalReadSummaryResponse,
|
|
39
39
|
AppReadSummaryResponse,
|
|
40
40
|
AppViewsReadResponse,
|
|
41
41
|
ChartApplyRequest,
|
|
@@ -53,6 +53,9 @@ from .models import (
|
|
|
53
53
|
LayoutSectionPatch,
|
|
54
54
|
LayoutPreset,
|
|
55
55
|
PortalApplyRequest,
|
|
56
|
+
PortalGetResponse,
|
|
57
|
+
PortalListResponse,
|
|
58
|
+
PortalReadSummaryResponse,
|
|
56
59
|
PortalSectionPatch,
|
|
57
60
|
PublicFieldType,
|
|
58
61
|
PublicRelationMode,
|
|
@@ -65,6 +68,7 @@ from .models import (
|
|
|
65
68
|
ViewButtonBindingPatch,
|
|
66
69
|
ViewUpsertPatch,
|
|
67
70
|
ViewFilterOperator,
|
|
71
|
+
ViewGetResponse,
|
|
68
72
|
ViewsPlanRequest,
|
|
69
73
|
ViewsPreset,
|
|
70
74
|
FlowPreset,
|
|
@@ -114,6 +118,18 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
114
118
|
FieldType.relation.value: 25,
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
122
|
+
FieldType.text.value,
|
|
123
|
+
FieldType.long_text.value,
|
|
124
|
+
FieldType.number.value,
|
|
125
|
+
FieldType.amount.value,
|
|
126
|
+
FieldType.date.value,
|
|
127
|
+
FieldType.datetime.value,
|
|
128
|
+
FieldType.single_select.value,
|
|
129
|
+
FieldType.multi_select.value,
|
|
130
|
+
FieldType.boolean.value,
|
|
131
|
+
)
|
|
132
|
+
|
|
117
133
|
MATCH_TYPE_ACCURACY = 1
|
|
118
134
|
JUDGE_EQUAL = 0
|
|
119
135
|
JUDGE_UNEQUAL = 1
|
|
@@ -2261,6 +2277,38 @@ class AiBuilderFacade:
|
|
|
2261
2277
|
**response.model_dump(mode="json"),
|
|
2262
2278
|
}
|
|
2263
2279
|
|
|
2280
|
+
def portal_list(self, *, profile: str) -> JSONObject:
|
|
2281
|
+
try:
|
|
2282
|
+
raw_items = self.portals.portal_list(profile=profile).get("items") or []
|
|
2283
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2284
|
+
api_error = _coerce_api_error(error)
|
|
2285
|
+
return _failed_from_api_error(
|
|
2286
|
+
"PORTAL_LIST_FAILED",
|
|
2287
|
+
api_error,
|
|
2288
|
+
normalized_args={},
|
|
2289
|
+
details={},
|
|
2290
|
+
suggested_next_call=None,
|
|
2291
|
+
)
|
|
2292
|
+
items = _normalize_portal_list_items(raw_items)
|
|
2293
|
+
response = PortalListResponse(items=items, total=len(items))
|
|
2294
|
+
return {
|
|
2295
|
+
"status": "success",
|
|
2296
|
+
"error_code": None,
|
|
2297
|
+
"recoverable": False,
|
|
2298
|
+
"message": "list accessible portals",
|
|
2299
|
+
"normalized_args": {},
|
|
2300
|
+
"missing_fields": [],
|
|
2301
|
+
"allowed_values": {},
|
|
2302
|
+
"details": {},
|
|
2303
|
+
"request_id": None,
|
|
2304
|
+
"suggested_next_call": None,
|
|
2305
|
+
"noop": False,
|
|
2306
|
+
"warnings": [],
|
|
2307
|
+
"verification": {"portal_list_loaded": True},
|
|
2308
|
+
"verified": True,
|
|
2309
|
+
**response.model_dump(mode="json"),
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2264
2312
|
def _load_chart_list_for_builder(self, *, profile: str, app_key: str) -> tuple[list[dict[str, Any]], str]:
|
|
2265
2313
|
try:
|
|
2266
2314
|
sorted_items = self.charts.qingbi_report_list_sorted(profile=profile, app_key=app_key, page_num=1, page_size=500).get("items") or []
|
|
@@ -2271,6 +2319,57 @@ class AiBuilderFacade:
|
|
|
2271
2319
|
fallback_items = self.charts.qingbi_report_list(profile=profile, app_key=app_key).get("items") or []
|
|
2272
2320
|
return list(fallback_items) if isinstance(fallback_items, list) else [], "fallback"
|
|
2273
2321
|
|
|
2322
|
+
def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
2323
|
+
try:
|
|
2324
|
+
result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft).get("result") or {}
|
|
2325
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2326
|
+
api_error = _coerce_api_error(error)
|
|
2327
|
+
return _failed_from_api_error(
|
|
2328
|
+
"PORTAL_GET_FAILED",
|
|
2329
|
+
api_error,
|
|
2330
|
+
normalized_args={"dash_key": dash_key, "being_draft": being_draft},
|
|
2331
|
+
details={"dash_key": dash_key, "being_draft": being_draft},
|
|
2332
|
+
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key, "being_draft": being_draft}},
|
|
2333
|
+
)
|
|
2334
|
+
response = PortalGetResponse(
|
|
2335
|
+
dash_key=dash_key,
|
|
2336
|
+
being_draft=being_draft,
|
|
2337
|
+
dash_name=str(result.get("dashName") or "").strip() or None,
|
|
2338
|
+
package_tag_ids=[
|
|
2339
|
+
tag_id
|
|
2340
|
+
for tag_id in (
|
|
2341
|
+
_coerce_positive_int((item or {}).get("tagId"))
|
|
2342
|
+
for item in (result.get("tags") or [])
|
|
2343
|
+
if isinstance(item, dict)
|
|
2344
|
+
)
|
|
2345
|
+
if tag_id is not None
|
|
2346
|
+
],
|
|
2347
|
+
dash_icon=str(result.get("dashIcon") or "").strip() or None,
|
|
2348
|
+
hide_copyright=bool(result.get("hideCopyright")) if "hideCopyright" in result else None,
|
|
2349
|
+
auth=deepcopy(result.get("auth")) if isinstance(result.get("auth"), dict) else {},
|
|
2350
|
+
config=deepcopy(result.get("config")) if isinstance(result.get("config"), dict) else {},
|
|
2351
|
+
dash_global_config=deepcopy(result.get("dashGlobalConfig")) if isinstance(result.get("dashGlobalConfig"), dict) else {},
|
|
2352
|
+
component_count=len(result.get("components") or []) if isinstance(result.get("components"), list) else 0,
|
|
2353
|
+
components=_normalize_portal_components(result.get("components")),
|
|
2354
|
+
)
|
|
2355
|
+
return {
|
|
2356
|
+
"status": "success",
|
|
2357
|
+
"error_code": None,
|
|
2358
|
+
"recoverable": False,
|
|
2359
|
+
"message": "read portal detail",
|
|
2360
|
+
"normalized_args": {"dash_key": dash_key, "being_draft": being_draft},
|
|
2361
|
+
"missing_fields": [],
|
|
2362
|
+
"allowed_values": {},
|
|
2363
|
+
"details": {},
|
|
2364
|
+
"request_id": None,
|
|
2365
|
+
"suggested_next_call": None,
|
|
2366
|
+
"noop": False,
|
|
2367
|
+
"warnings": [],
|
|
2368
|
+
"verification": {"portal_exists": True, "being_draft": being_draft},
|
|
2369
|
+
"verified": True,
|
|
2370
|
+
**response.model_dump(mode="json"),
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2274
2373
|
def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
2275
2374
|
try:
|
|
2276
2375
|
result = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft).get("result") or {}
|
|
@@ -2321,6 +2420,186 @@ class AiBuilderFacade:
|
|
|
2321
2420
|
**response.model_dump(mode="json"),
|
|
2322
2421
|
}
|
|
2323
2422
|
|
|
2423
|
+
def view_get(self, *, profile: str, viewgraph_key: str) -> JSONObject:
|
|
2424
|
+
try:
|
|
2425
|
+
config = self.views.view_get_config(profile=profile, viewgraph_key=viewgraph_key).get("result") or {}
|
|
2426
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2427
|
+
api_error = _coerce_api_error(error)
|
|
2428
|
+
return _failed_from_api_error(
|
|
2429
|
+
"VIEW_GET_FAILED",
|
|
2430
|
+
api_error,
|
|
2431
|
+
normalized_args={"viewgraph_key": viewgraph_key},
|
|
2432
|
+
details={"viewgraph_key": viewgraph_key},
|
|
2433
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, "viewgraph_key": viewgraph_key}},
|
|
2434
|
+
)
|
|
2435
|
+
|
|
2436
|
+
warnings: list[dict[str, Any]] = []
|
|
2437
|
+
verification = {
|
|
2438
|
+
"view_exists": True,
|
|
2439
|
+
"base_info_verified": True,
|
|
2440
|
+
"questions_verified": True,
|
|
2441
|
+
"associations_verified": True,
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
base_info: dict[str, Any] = {}
|
|
2445
|
+
try:
|
|
2446
|
+
base_info_payload = self.views.view_get_base_info(profile=profile, viewgraph_key=viewgraph_key, passcode=None).get("result") or {}
|
|
2447
|
+
if isinstance(base_info_payload, dict):
|
|
2448
|
+
base_info = deepcopy(base_info_payload)
|
|
2449
|
+
except (QingflowApiError, RuntimeError):
|
|
2450
|
+
verification["base_info_verified"] = False
|
|
2451
|
+
warnings.append(_warning("VIEW_BASE_INFO_UNAVAILABLE", "view base info readback is unavailable"))
|
|
2452
|
+
|
|
2453
|
+
questions: list[dict[str, Any]] = []
|
|
2454
|
+
try:
|
|
2455
|
+
questions_payload = self.views.view_list_questions(profile=profile, viewgraph_key=viewgraph_key).get("result") or []
|
|
2456
|
+
if isinstance(questions_payload, list):
|
|
2457
|
+
questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
|
|
2458
|
+
except (QingflowApiError, RuntimeError):
|
|
2459
|
+
verification["questions_verified"] = False
|
|
2460
|
+
warnings.append(_warning("VIEW_QUESTIONS_UNAVAILABLE", "view question list readback is unavailable"))
|
|
2461
|
+
|
|
2462
|
+
associations: list[dict[str, Any]] = []
|
|
2463
|
+
try:
|
|
2464
|
+
associations_payload = self.views.view_list_associations(profile=profile, viewgraph_key=viewgraph_key).get("result") or []
|
|
2465
|
+
if isinstance(associations_payload, list):
|
|
2466
|
+
associations = [deepcopy(item) for item in associations_payload if isinstance(item, dict)]
|
|
2467
|
+
except (QingflowApiError, RuntimeError):
|
|
2468
|
+
verification["associations_verified"] = False
|
|
2469
|
+
warnings.append(_warning("VIEW_ASSOCIATIONS_UNAVAILABLE", "view association list readback is unavailable"))
|
|
2470
|
+
|
|
2471
|
+
response = ViewGetResponse(
|
|
2472
|
+
viewgraph_key=viewgraph_key,
|
|
2473
|
+
base_info=base_info,
|
|
2474
|
+
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
2475
|
+
questions=questions,
|
|
2476
|
+
associations=associations,
|
|
2477
|
+
)
|
|
2478
|
+
return {
|
|
2479
|
+
"status": "success",
|
|
2480
|
+
"error_code": None,
|
|
2481
|
+
"recoverable": False,
|
|
2482
|
+
"message": "read view detail",
|
|
2483
|
+
"normalized_args": {"viewgraph_key": viewgraph_key},
|
|
2484
|
+
"missing_fields": [],
|
|
2485
|
+
"allowed_values": {},
|
|
2486
|
+
"details": {},
|
|
2487
|
+
"request_id": None,
|
|
2488
|
+
"suggested_next_call": None,
|
|
2489
|
+
"noop": False,
|
|
2490
|
+
"warnings": warnings,
|
|
2491
|
+
"verification": verification,
|
|
2492
|
+
"verified": all(bool(value) for value in verification.values()),
|
|
2493
|
+
**response.model_dump(mode="json"),
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
def chart_get(
|
|
2497
|
+
self,
|
|
2498
|
+
*,
|
|
2499
|
+
profile: str,
|
|
2500
|
+
chart_id: str,
|
|
2501
|
+
data_payload: dict[str, Any] | None = None,
|
|
2502
|
+
page_num: int | None = None,
|
|
2503
|
+
page_size: int | None = None,
|
|
2504
|
+
page_num_y: int | None = None,
|
|
2505
|
+
page_size_y: int | None = None,
|
|
2506
|
+
) -> JSONObject:
|
|
2507
|
+
normalized_payload = deepcopy(data_payload) if isinstance(data_payload, dict) else {}
|
|
2508
|
+
warnings: list[dict[str, Any]] = []
|
|
2509
|
+
verification = {
|
|
2510
|
+
"chart_exists": True,
|
|
2511
|
+
"chart_data_loaded": True,
|
|
2512
|
+
"chart_config_loaded": True,
|
|
2513
|
+
}
|
|
2514
|
+
try:
|
|
2515
|
+
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
2516
|
+
data = self.charts.qingbi_report_get_data(
|
|
2517
|
+
profile=profile,
|
|
2518
|
+
chart_id=chart_id,
|
|
2519
|
+
payload=normalized_payload,
|
|
2520
|
+
page_num=page_num,
|
|
2521
|
+
page_size=page_size,
|
|
2522
|
+
page_num_y=page_num_y,
|
|
2523
|
+
page_size_y=page_size_y,
|
|
2524
|
+
).get("result") or {}
|
|
2525
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2526
|
+
api_error = _coerce_api_error(error)
|
|
2527
|
+
return _failed_from_api_error(
|
|
2528
|
+
"CHART_GET_FAILED",
|
|
2529
|
+
api_error,
|
|
2530
|
+
normalized_args={
|
|
2531
|
+
"chart_id": chart_id,
|
|
2532
|
+
"data_payload": normalized_payload,
|
|
2533
|
+
"page_num": page_num,
|
|
2534
|
+
"page_size": page_size,
|
|
2535
|
+
"page_num_y": page_num_y,
|
|
2536
|
+
"page_size_y": page_size_y,
|
|
2537
|
+
},
|
|
2538
|
+
details={"chart_id": chart_id},
|
|
2539
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
2540
|
+
)
|
|
2541
|
+
|
|
2542
|
+
try:
|
|
2543
|
+
config = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
2544
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
2545
|
+
config_from_data = data.get("config") if isinstance(data, dict) else None
|
|
2546
|
+
if isinstance(config_from_data, dict):
|
|
2547
|
+
config = deepcopy(config_from_data)
|
|
2548
|
+
verification["chart_config_loaded"] = True
|
|
2549
|
+
warnings.append(
|
|
2550
|
+
_warning(
|
|
2551
|
+
"CHART_CONFIG_FALLBACK_FROM_DATA",
|
|
2552
|
+
"chart config endpoint is unavailable for this chart id; using config embedded in chart data instead",
|
|
2553
|
+
)
|
|
2554
|
+
)
|
|
2555
|
+
else:
|
|
2556
|
+
api_error = _coerce_api_error(error)
|
|
2557
|
+
return _failed_from_api_error(
|
|
2558
|
+
"CHART_GET_FAILED",
|
|
2559
|
+
api_error,
|
|
2560
|
+
normalized_args={
|
|
2561
|
+
"chart_id": chart_id,
|
|
2562
|
+
"data_payload": normalized_payload,
|
|
2563
|
+
"page_num": page_num,
|
|
2564
|
+
"page_size": page_size,
|
|
2565
|
+
"page_num_y": page_num_y,
|
|
2566
|
+
"page_size_y": page_size_y,
|
|
2567
|
+
},
|
|
2568
|
+
details={"chart_id": chart_id},
|
|
2569
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
2570
|
+
)
|
|
2571
|
+
|
|
2572
|
+
response = ChartGetResponse(
|
|
2573
|
+
chart_id=chart_id,
|
|
2574
|
+
base=deepcopy(base) if isinstance(base, dict) else {},
|
|
2575
|
+
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
2576
|
+
data=deepcopy(data) if isinstance(data, dict) else {"value": data},
|
|
2577
|
+
)
|
|
2578
|
+
return {
|
|
2579
|
+
"status": "success",
|
|
2580
|
+
"error_code": None,
|
|
2581
|
+
"recoverable": False,
|
|
2582
|
+
"message": "read chart detail",
|
|
2583
|
+
"normalized_args": {
|
|
2584
|
+
"chart_id": chart_id,
|
|
2585
|
+
"data_payload": normalized_payload,
|
|
2586
|
+
"page_num": page_num,
|
|
2587
|
+
"page_size": page_size,
|
|
2588
|
+
"page_num_y": page_num_y,
|
|
2589
|
+
"page_size_y": page_size_y,
|
|
2590
|
+
},
|
|
2591
|
+
"missing_fields": [],
|
|
2592
|
+
"allowed_values": {},
|
|
2593
|
+
"details": {},
|
|
2594
|
+
"request_id": None,
|
|
2595
|
+
"suggested_next_call": None,
|
|
2596
|
+
"noop": False,
|
|
2597
|
+
"warnings": warnings,
|
|
2598
|
+
"verification": verification,
|
|
2599
|
+
"verified": True,
|
|
2600
|
+
**response.model_dump(mode="json"),
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2324
2603
|
def app_schema_plan(self, *, profile: str, request: SchemaPlanRequest) -> JSONObject:
|
|
2325
2604
|
normalized_args = request.model_dump(mode="json")
|
|
2326
2605
|
target = self._preview_target_app(
|
|
@@ -8842,8 +9121,9 @@ def _compile_code_block_binding_fields(
|
|
|
8842
9121
|
selector_payload=target_payload,
|
|
8843
9122
|
location=f"code_block_binding.outputs[{output_index}].target_field",
|
|
8844
9123
|
)
|
|
8845
|
-
|
|
8846
|
-
|
|
9124
|
+
target_type = str(target_field.get("type") or "")
|
|
9125
|
+
if not _integration_output_target_type_supported(target_type):
|
|
9126
|
+
raise ValueError(_integration_output_target_type_error("code_block", str(target_field.get("name") or ""), target_type))
|
|
8847
9127
|
target_que_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
8848
9128
|
if target_que_ref is None:
|
|
8849
9129
|
target_que_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
@@ -9040,18 +9320,17 @@ def _overlay_code_block_binding_fields(*, target_fields: list[dict[str, Any]], s
|
|
|
9040
9320
|
field["code_block_binding"] = binding
|
|
9041
9321
|
|
|
9042
9322
|
|
|
9043
|
-
def
|
|
9044
|
-
return field_type in
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
}
|
|
9323
|
+
def _integration_output_target_type_supported(field_type: str) -> bool:
|
|
9324
|
+
return field_type in INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
|
|
9325
|
+
|
|
9326
|
+
|
|
9327
|
+
def _integration_output_target_type_error(binding_kind: str, field_name: str, field_type: str) -> str:
|
|
9328
|
+
allowed = ", ".join(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES)
|
|
9329
|
+
rendered_type = field_type or "unknown"
|
|
9330
|
+
return (
|
|
9331
|
+
f"{binding_kind} output target field '{field_name}' uses unsupported field type '{rendered_type}'. "
|
|
9332
|
+
f"Allowed target field types: {allowed}"
|
|
9333
|
+
)
|
|
9055
9334
|
|
|
9056
9335
|
|
|
9057
9336
|
def _compile_q_linker_binding_fields(
|
|
@@ -9215,8 +9494,8 @@ def _compile_q_linker_binding_fields(
|
|
|
9215
9494
|
location=f"q_linker_binding.outputs[{output_index}].target_field",
|
|
9216
9495
|
)
|
|
9217
9496
|
target_type = str(target_field.get("type") or "")
|
|
9218
|
-
if not
|
|
9219
|
-
raise ValueError(
|
|
9497
|
+
if not _integration_output_target_type_supported(target_type):
|
|
9498
|
+
raise ValueError(_integration_output_target_type_error("q_linker", str(target_field.get("name") or ""), target_type))
|
|
9220
9499
|
target_ref = _coerce_positive_int(target_field.get("que_id"))
|
|
9221
9500
|
if target_ref is None:
|
|
9222
9501
|
target_ref = _coerce_any_int(target_field.get("que_temp_id"))
|
|
@@ -10986,6 +11265,38 @@ def _summarize_charts(result: Any) -> list[dict[str, Any]]:
|
|
|
10986
11265
|
return items
|
|
10987
11266
|
|
|
10988
11267
|
|
|
11268
|
+
def _normalize_portal_list_items(raw_items: Any) -> list[dict[str, Any]]:
|
|
11269
|
+
if not isinstance(raw_items, list):
|
|
11270
|
+
return []
|
|
11271
|
+
items: list[dict[str, Any]] = []
|
|
11272
|
+
for item in raw_items:
|
|
11273
|
+
if not isinstance(item, dict):
|
|
11274
|
+
continue
|
|
11275
|
+
dash_key = str(item.get("dashKey") or "").strip()
|
|
11276
|
+
dash_name = str(item.get("dashName") or "").strip()
|
|
11277
|
+
dash_icon = str(item.get("dashIcon") or "").strip() or None
|
|
11278
|
+
package_tag_ids = [
|
|
11279
|
+
tag_id
|
|
11280
|
+
for tag_id in (
|
|
11281
|
+
_coerce_positive_int((tag or {}).get("tagId"))
|
|
11282
|
+
for tag in (item.get("tags") or [])
|
|
11283
|
+
if isinstance(tag, dict)
|
|
11284
|
+
)
|
|
11285
|
+
if tag_id is not None
|
|
11286
|
+
]
|
|
11287
|
+
if not any((dash_key, dash_name, dash_icon, package_tag_ids)):
|
|
11288
|
+
continue
|
|
11289
|
+
items.append(
|
|
11290
|
+
{
|
|
11291
|
+
"dash_key": dash_key or None,
|
|
11292
|
+
"dash_name": dash_name or None,
|
|
11293
|
+
"dash_icon": dash_icon,
|
|
11294
|
+
"package_tag_ids": package_tag_ids,
|
|
11295
|
+
}
|
|
11296
|
+
)
|
|
11297
|
+
return items
|
|
11298
|
+
|
|
11299
|
+
|
|
10989
11300
|
def _summarize_portal_sections(components: Any) -> list[dict[str, Any]]:
|
|
10990
11301
|
if not isinstance(components, list):
|
|
10991
11302
|
return []
|
|
@@ -11020,6 +11331,50 @@ def _summarize_portal_sections(components: Any) -> list[dict[str, Any]]:
|
|
|
11020
11331
|
return items
|
|
11021
11332
|
|
|
11022
11333
|
|
|
11334
|
+
def _normalize_portal_components(components: Any) -> list[dict[str, Any]]:
|
|
11335
|
+
if not isinstance(components, list):
|
|
11336
|
+
return []
|
|
11337
|
+
items: list[dict[str, Any]] = []
|
|
11338
|
+
config_key_map = {
|
|
11339
|
+
"grid": "gridConfig",
|
|
11340
|
+
"link": "linkConfig",
|
|
11341
|
+
"text": "textConfig",
|
|
11342
|
+
"filter": "filterConfig",
|
|
11343
|
+
"chart": "chartConfig",
|
|
11344
|
+
"view": "viewgraphConfig",
|
|
11345
|
+
}
|
|
11346
|
+
for index, component in enumerate(components):
|
|
11347
|
+
if not isinstance(component, dict):
|
|
11348
|
+
continue
|
|
11349
|
+
source_type = _normalize_portal_component_source_type(component.get("type"))
|
|
11350
|
+
title = _extract_portal_component_title(component, source_type=source_type)
|
|
11351
|
+
summary: dict[str, Any] = {
|
|
11352
|
+
"order": index,
|
|
11353
|
+
"source_type": source_type,
|
|
11354
|
+
"title": title,
|
|
11355
|
+
}
|
|
11356
|
+
position = component.get("position")
|
|
11357
|
+
if isinstance(position, dict):
|
|
11358
|
+
summary["position"] = deepcopy(position)
|
|
11359
|
+
config_key = config_key_map.get(source_type, "")
|
|
11360
|
+
config = component.get(config_key) if isinstance(component.get(config_key), dict) else {}
|
|
11361
|
+
if source_type == "chart":
|
|
11362
|
+
summary["chart_ref"] = {
|
|
11363
|
+
"chart_id": str(config.get("biChartId") or "").strip() or None,
|
|
11364
|
+
"chart_name": str(config.get("chartComponentTitle") or title or "").strip() or None,
|
|
11365
|
+
}
|
|
11366
|
+
elif source_type == "view":
|
|
11367
|
+
summary["view_ref"] = {
|
|
11368
|
+
"app_key": str(config.get("appKey") or "").strip() or None,
|
|
11369
|
+
"view_key": str(config.get("viewgraphKey") or "").strip() or None,
|
|
11370
|
+
"view_name": str(config.get("viewgraphName") or title or "").strip() or None,
|
|
11371
|
+
}
|
|
11372
|
+
elif source_type in {"grid", "link", "text", "filter"} and config:
|
|
11373
|
+
summary["config"] = deepcopy(config)
|
|
11374
|
+
items.append(summary)
|
|
11375
|
+
return items
|
|
11376
|
+
|
|
11377
|
+
|
|
11023
11378
|
def _normalize_portal_component_source_type(value: Any) -> str:
|
|
11024
11379
|
raw = str(value or "").strip()
|
|
11025
11380
|
mapping = {
|