@josephyan/qingflow-cli 0.2.0-beta.1015 → 0.2.0-beta.1017
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/__init__.py +1 -1
- package/src/qingflow_mcp/cli/formatters.py +7 -0
- package/src/qingflow_mcp/response_trim.py +0 -1
- package/src/qingflow_mcp/tools/export_tools.py +88 -7
- package/src/qingflow_mcp/tools/import_tools.py +42 -2
- package/src/qingflow_mcp/tools/record_tools.py +122 -7
- package/src/qingflow_mcp/tools/resource_read_tools.py +20 -10
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.1017
|
|
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.1017 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.1017",
|
|
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
|
@@ -407,6 +407,13 @@ def _format_import_status(result: dict[str, Any]) -> str:
|
|
|
407
407
|
f"Failed Rows: {result.get('failed') or 0}",
|
|
408
408
|
f"Progress: {result.get('progress') or '-'}",
|
|
409
409
|
]
|
|
410
|
+
if result.get("process_status") not in (None, ""):
|
|
411
|
+
lines.append(f"Process Status: {result.get('process_status')}")
|
|
412
|
+
error_file_urls = result.get("error_file_urls") if isinstance(result.get("error_file_urls"), list) else []
|
|
413
|
+
if error_file_urls:
|
|
414
|
+
lines.append("Error Files:")
|
|
415
|
+
for url in error_file_urls:
|
|
416
|
+
lines.append(f"- {url}")
|
|
410
417
|
_append_warnings(lines, result.get("warnings"))
|
|
411
418
|
_append_verification(lines, result.get("verification"))
|
|
412
419
|
return "\n".join(lines) + "\n"
|
|
@@ -11,6 +11,7 @@ from uuid import uuid4
|
|
|
11
11
|
|
|
12
12
|
from mcp.server.fastmcp import FastMCP
|
|
13
13
|
|
|
14
|
+
from ..backend_client import BackendRequestContext
|
|
14
15
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
|
|
15
16
|
from ..errors import QingflowApiError
|
|
16
17
|
from ..export_store import ExportJobStore
|
|
@@ -192,6 +193,8 @@ class ExportTools(ToolBase):
|
|
|
192
193
|
filter_bean = self._build_export_filter_bean(
|
|
193
194
|
resolved_view,
|
|
194
195
|
selected_record_ids=effective_record_ids,
|
|
196
|
+
order_by=normalized_order_by,
|
|
197
|
+
row_scope=row_scope,
|
|
195
198
|
include_workflow_log=include_workflow_log,
|
|
196
199
|
)
|
|
197
200
|
started_at = _utc_now().replace(microsecond=0).isoformat()
|
|
@@ -211,6 +214,10 @@ class ExportTools(ToolBase):
|
|
|
211
214
|
{
|
|
212
215
|
"created_at": started_at,
|
|
213
216
|
"profile": profile,
|
|
217
|
+
"base_url": context.base_url,
|
|
218
|
+
"ws_id": context.ws_id,
|
|
219
|
+
"qf_version": context.qf_version,
|
|
220
|
+
"qf_version_source": context.qf_version_source,
|
|
214
221
|
"app_key": normalized_app_key,
|
|
215
222
|
"view_id": resolved_view.view_id,
|
|
216
223
|
"backend_export_id": str(socket_result.get("backend_export_id") or ""),
|
|
@@ -278,7 +285,7 @@ class ExportTools(ToolBase):
|
|
|
278
285
|
extra={"status": "failed"},
|
|
279
286
|
)
|
|
280
287
|
|
|
281
|
-
def runner(
|
|
288
|
+
def runner(session_profile, context):
|
|
282
289
|
local_job = self._job_store.get(normalized_handle)
|
|
283
290
|
if local_job is None:
|
|
284
291
|
return self._failed_export_result(
|
|
@@ -286,7 +293,13 @@ class ExportTools(ToolBase):
|
|
|
286
293
|
message="export_handle is missing or expired",
|
|
287
294
|
extra={"export_handle": normalized_handle, "status": "failed"},
|
|
288
295
|
)
|
|
289
|
-
|
|
296
|
+
lookup_context = self._build_export_lookup_context(
|
|
297
|
+
profile=profile,
|
|
298
|
+
session_profile=session_profile,
|
|
299
|
+
current_context=context,
|
|
300
|
+
local_job=local_job,
|
|
301
|
+
)
|
|
302
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
290
303
|
return self._status_payload_from_snapshot(local_job, normalized_handle, snapshot)
|
|
291
304
|
|
|
292
305
|
try:
|
|
@@ -314,7 +327,7 @@ class ExportTools(ToolBase):
|
|
|
314
327
|
extra={"status": "failed"},
|
|
315
328
|
)
|
|
316
329
|
|
|
317
|
-
def runner(
|
|
330
|
+
def runner(session_profile, context):
|
|
318
331
|
local_job = self._job_store.get(normalized_handle)
|
|
319
332
|
if local_job is None:
|
|
320
333
|
return self._failed_export_result(
|
|
@@ -322,7 +335,13 @@ class ExportTools(ToolBase):
|
|
|
322
335
|
message="export_handle is missing or expired",
|
|
323
336
|
extra={"export_handle": normalized_handle, "status": "failed"},
|
|
324
337
|
)
|
|
325
|
-
|
|
338
|
+
lookup_context = self._build_export_lookup_context(
|
|
339
|
+
profile=profile,
|
|
340
|
+
session_profile=session_profile,
|
|
341
|
+
current_context=context,
|
|
342
|
+
local_job=local_job,
|
|
343
|
+
)
|
|
344
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
326
345
|
normalized_status = str(snapshot.get("status") or "unknown")
|
|
327
346
|
if normalized_status not in {"succeeded", "failed"}:
|
|
328
347
|
return self._failed_export_result(
|
|
@@ -412,7 +431,7 @@ class ExportTools(ToolBase):
|
|
|
412
431
|
"downloaded_files": downloaded_files,
|
|
413
432
|
"warnings": snapshot.get("warnings") or [],
|
|
414
433
|
"verification": snapshot.get("verification") or {},
|
|
415
|
-
"request_route": self.backend.describe_route(
|
|
434
|
+
"request_route": self.backend.describe_route(lookup_context),
|
|
416
435
|
}
|
|
417
436
|
|
|
418
437
|
try:
|
|
@@ -449,7 +468,7 @@ class ExportTools(ToolBase):
|
|
|
449
468
|
)
|
|
450
469
|
timeout_seconds = self._normalize_timeout_seconds(wait_timeout_seconds)
|
|
451
470
|
|
|
452
|
-
def runner(
|
|
471
|
+
def runner(session_profile, context):
|
|
453
472
|
start_result = self.record_export_start(
|
|
454
473
|
profile=profile,
|
|
455
474
|
app_key=normalized_app_key,
|
|
@@ -473,7 +492,13 @@ class ExportTools(ToolBase):
|
|
|
473
492
|
message="export_handle is missing or expired",
|
|
474
493
|
extra={"status": "failed", "export_handle": export_handle},
|
|
475
494
|
)
|
|
476
|
-
|
|
495
|
+
lookup_context = self._build_export_lookup_context(
|
|
496
|
+
profile=profile,
|
|
497
|
+
session_profile=session_profile,
|
|
498
|
+
current_context=context,
|
|
499
|
+
local_job=local_job,
|
|
500
|
+
)
|
|
501
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
477
502
|
last_snapshot = snapshot
|
|
478
503
|
normalized_status = str(snapshot.get("status") or "unknown")
|
|
479
504
|
if normalized_status == "succeeded":
|
|
@@ -581,6 +606,49 @@ class ExportTools(ToolBase):
|
|
|
581
606
|
extra={"app_key": normalized_app_key, "view_id": normalized_view_id},
|
|
582
607
|
)
|
|
583
608
|
|
|
609
|
+
def _build_export_lookup_context(
|
|
610
|
+
self,
|
|
611
|
+
*,
|
|
612
|
+
profile: str,
|
|
613
|
+
session_profile,
|
|
614
|
+
current_context: BackendRequestContext,
|
|
615
|
+
local_job: dict[str, Any],
|
|
616
|
+
) -> BackendRequestContext:
|
|
617
|
+
stored_profile = str(local_job.get("profile") or "").strip()
|
|
618
|
+
if stored_profile and stored_profile != profile:
|
|
619
|
+
raise QingflowApiError.config_error(
|
|
620
|
+
"export_handle was created under a different profile",
|
|
621
|
+
details={
|
|
622
|
+
"error_code": "EXPORT_HANDLE_PROFILE_MISMATCH",
|
|
623
|
+
"expected_profile": stored_profile,
|
|
624
|
+
"received_profile": profile,
|
|
625
|
+
},
|
|
626
|
+
)
|
|
627
|
+
stored_uid = _coerce_positive_int(local_job.get("uid"))
|
|
628
|
+
if stored_uid is not None and stored_uid != session_profile.uid:
|
|
629
|
+
raise QingflowApiError.config_error(
|
|
630
|
+
"export_handle belongs to a different authenticated user",
|
|
631
|
+
details={
|
|
632
|
+
"error_code": "EXPORT_HANDLE_OWNER_MISMATCH",
|
|
633
|
+
"expected_uid": stored_uid,
|
|
634
|
+
"current_uid": session_profile.uid,
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
stored_base_url = str(local_job.get("base_url") or "").strip() or current_context.base_url
|
|
638
|
+
stored_ws_id = _coerce_positive_int(local_job.get("ws_id"))
|
|
639
|
+
stored_qf_version = str(local_job.get("qf_version") or "").strip() or current_context.qf_version
|
|
640
|
+
stored_qf_version_source = (
|
|
641
|
+
str(local_job.get("qf_version_source") or "").strip() or current_context.qf_version_source
|
|
642
|
+
)
|
|
643
|
+
return BackendRequestContext(
|
|
644
|
+
base_url=stored_base_url,
|
|
645
|
+
token=current_context.token,
|
|
646
|
+
ws_id=stored_ws_id if stored_ws_id is not None else current_context.ws_id,
|
|
647
|
+
qf_request_id=current_context.qf_request_id,
|
|
648
|
+
qf_version=stored_qf_version,
|
|
649
|
+
qf_version_source=stored_qf_version_source,
|
|
650
|
+
)
|
|
651
|
+
|
|
584
652
|
def _resolve_export_record_scope(
|
|
585
653
|
self,
|
|
586
654
|
*,
|
|
@@ -732,6 +800,8 @@ class ExportTools(ToolBase):
|
|
|
732
800
|
resolved_view: AccessibleViewRoute,
|
|
733
801
|
*,
|
|
734
802
|
selected_record_ids: list[int],
|
|
803
|
+
order_by: list[JSONObject],
|
|
804
|
+
row_scope: str,
|
|
735
805
|
include_workflow_log: bool,
|
|
736
806
|
) -> JSONObject:
|
|
737
807
|
filter_payload: JSONObject = {}
|
|
@@ -744,6 +814,17 @@ class ExportTools(ToolBase):
|
|
|
744
814
|
filter_payload["type"] = DEFAULT_RECORD_LIST_TYPE
|
|
745
815
|
if selected_record_ids:
|
|
746
816
|
filter_payload["applyIds"] = selected_record_ids
|
|
817
|
+
if row_scope == "queried" and order_by:
|
|
818
|
+
normalized_sorts = [
|
|
819
|
+
{
|
|
820
|
+
"queId": field_id,
|
|
821
|
+
"isAscend": str(item.get("direction") or "asc").strip().lower() != "desc",
|
|
822
|
+
}
|
|
823
|
+
for item in order_by
|
|
824
|
+
if isinstance(item, dict) and (field_id := _coerce_int(item.get("field_id"))) is not None
|
|
825
|
+
]
|
|
826
|
+
if normalized_sorts:
|
|
827
|
+
filter_payload["sorts"] = normalized_sorts
|
|
747
828
|
return {
|
|
748
829
|
"filter": filter_payload,
|
|
749
830
|
# Backend export code later auto-unboxes this field to primitive boolean.
|
|
@@ -37,6 +37,13 @@ SAFE_REPAIRS = {
|
|
|
37
37
|
"normalize_url_cells",
|
|
38
38
|
}
|
|
39
39
|
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
40
|
+
IMPORT_STATUS_BY_PROCESS_STATUS = {
|
|
41
|
+
1: "queued",
|
|
42
|
+
2: "running",
|
|
43
|
+
3: "succeeded",
|
|
44
|
+
4: "failed",
|
|
45
|
+
5: "partially_failed",
|
|
46
|
+
}
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class ImportTools(ToolBase):
|
|
@@ -866,13 +873,26 @@ class ImportTools(ToolBase):
|
|
|
866
873
|
"process_id_str": normalized_process,
|
|
867
874
|
},
|
|
868
875
|
)
|
|
876
|
+
raw_process_status = matched_record.get("processStatus")
|
|
869
877
|
total_rows = _coerce_int(matched_record.get("totalNumber") or matched_record.get("total_rows"))
|
|
870
878
|
success_rows = _coerce_int(matched_record.get("successNum") or matched_record.get("success_rows"))
|
|
871
879
|
failed_rows = _coerce_int(matched_record.get("errorNum") or matched_record.get("failed_rows"))
|
|
872
880
|
progress = _coerce_int(matched_record.get("importPercentage") or matched_record.get("progress"))
|
|
881
|
+
normalized_status = _normalize_import_status(raw_process_status)
|
|
882
|
+
warnings: list[dict[str, str]] = []
|
|
883
|
+
if normalized_status in {"succeeded", "failed", "partially_failed"} and all(
|
|
884
|
+
value is None for value in (total_rows, success_rows, failed_rows)
|
|
885
|
+
):
|
|
886
|
+
warnings.append(
|
|
887
|
+
{
|
|
888
|
+
"code": "IMPORT_STATUS_COUNTERS_MISSING",
|
|
889
|
+
"message": "backend import history returned a terminal process status without row counters",
|
|
890
|
+
}
|
|
891
|
+
)
|
|
873
892
|
return {
|
|
874
893
|
"ok": True,
|
|
875
|
-
"status":
|
|
894
|
+
"status": normalized_status,
|
|
895
|
+
"process_status": _coerce_int(raw_process_status),
|
|
876
896
|
"app_key": resolved_app_key,
|
|
877
897
|
"import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
|
|
878
898
|
"process_id_str": normalized_process,
|
|
@@ -885,7 +905,7 @@ class ImportTools(ToolBase):
|
|
|
885
905
|
"error_file_urls": _normalize_error_file_urls(matched_record.get("errorFileUrls")),
|
|
886
906
|
"operate_time": matched_record.get("operateTime"),
|
|
887
907
|
"operate_user": matched_record.get("operateUser"),
|
|
888
|
-
"warnings":
|
|
908
|
+
"warnings": warnings,
|
|
889
909
|
"verification": {
|
|
890
910
|
"status_lookup_completed": True,
|
|
891
911
|
"matched_by": matched_by,
|
|
@@ -2217,6 +2237,26 @@ def _coerce_int(value: Any) -> int | None:
|
|
|
2217
2237
|
return None
|
|
2218
2238
|
|
|
2219
2239
|
|
|
2240
|
+
def _normalize_import_status(value: Any) -> str:
|
|
2241
|
+
status_code = _coerce_int(value)
|
|
2242
|
+
if status_code is not None:
|
|
2243
|
+
return IMPORT_STATUS_BY_PROCESS_STATUS.get(status_code, "unknown")
|
|
2244
|
+
text = str(value or "").strip().lower()
|
|
2245
|
+
if text in {"queued", "running", "succeeded", "failed", "partially_failed", "unknown"}:
|
|
2246
|
+
return text
|
|
2247
|
+
if text in {"line_up", "lineup"}:
|
|
2248
|
+
return "queued"
|
|
2249
|
+
if text in {"execute", "executing", "processing"}:
|
|
2250
|
+
return "running"
|
|
2251
|
+
if text in {"success", "completed"}:
|
|
2252
|
+
return "succeeded"
|
|
2253
|
+
if text in {"partly_fail", "partial_fail", "partially_fail", "partial_failed"}:
|
|
2254
|
+
return "partially_failed"
|
|
2255
|
+
if text in {"fail", "error"}:
|
|
2256
|
+
return "failed"
|
|
2257
|
+
return "unknown"
|
|
2258
|
+
|
|
2259
|
+
|
|
2220
2260
|
def _normalize_error_file_urls(value: Any) -> list[str]:
|
|
2221
2261
|
if isinstance(value, list):
|
|
2222
2262
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
@@ -2920,12 +2920,14 @@ class RecordTools(ToolBase):
|
|
|
2920
2920
|
payload["category"] = error.category
|
|
2921
2921
|
if error.backend_code is not None:
|
|
2922
2922
|
payload["backend_code"] = error.backend_code
|
|
2923
|
+
if error.request_id is not None:
|
|
2924
|
+
payload["request_id"] = error.request_id
|
|
2923
2925
|
if error.http_status is not None:
|
|
2924
2926
|
payload["http_status"] = error.http_status
|
|
2925
2927
|
return payload
|
|
2926
2928
|
|
|
2927
2929
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
2928
|
-
if error.backend_code in {40002, 40027, 40038, 404}:
|
|
2930
|
+
if error.backend_code in {40002, 40023, 40027, 40038, 404}:
|
|
2929
2931
|
return True
|
|
2930
2932
|
if error.http_status == 404:
|
|
2931
2933
|
return True
|
|
@@ -3059,6 +3061,107 @@ class RecordTools(ToolBase):
|
|
|
3059
3061
|
f"({get_record_list_type_label(used_list_type)})."
|
|
3060
3062
|
]
|
|
3061
3063
|
|
|
3064
|
+
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
3065
|
+
if exc.backend_code == 500:
|
|
3066
|
+
return True
|
|
3067
|
+
if exc.http_status is not None and exc.http_status >= 500:
|
|
3068
|
+
return True
|
|
3069
|
+
normalized_message = exc.message.strip().lower()
|
|
3070
|
+
return normalized_message in {"unknown error", "internal server error"}
|
|
3071
|
+
|
|
3072
|
+
def _remap_record_update_target_context_error(
|
|
3073
|
+
self,
|
|
3074
|
+
profile: str,
|
|
3075
|
+
context, # type: ignore[no-untyped-def]
|
|
3076
|
+
*,
|
|
3077
|
+
app_key: str,
|
|
3078
|
+
apply_id: int,
|
|
3079
|
+
exc: QingflowApiError,
|
|
3080
|
+
) -> None:
|
|
3081
|
+
if not self._looks_like_generic_record_update_backend_failure(exc):
|
|
3082
|
+
return
|
|
3083
|
+
try:
|
|
3084
|
+
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
3085
|
+
probes = self._probe_candidate_record_contexts(
|
|
3086
|
+
context,
|
|
3087
|
+
app_key=app_key,
|
|
3088
|
+
apply_id=apply_id,
|
|
3089
|
+
candidate_routes=candidate_routes,
|
|
3090
|
+
)
|
|
3091
|
+
except (QingflowApiError, RuntimeError):
|
|
3092
|
+
return
|
|
3093
|
+
if not probes or any(probe.readable for probe in probes):
|
|
3094
|
+
return
|
|
3095
|
+
|
|
3096
|
+
blocker = (
|
|
3097
|
+
"CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3098
|
+
if all(probe.transport_error for probe in probes)
|
|
3099
|
+
else "NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"
|
|
3100
|
+
)
|
|
3101
|
+
recommended_next_actions = (
|
|
3102
|
+
[
|
|
3103
|
+
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
3104
|
+
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
3105
|
+
]
|
|
3106
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3107
|
+
else [
|
|
3108
|
+
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
3109
|
+
"Call record_update_schema_get to inspect whether any accessible view still matches this record.",
|
|
3110
|
+
]
|
|
3111
|
+
)
|
|
3112
|
+
first_error_payload = next(
|
|
3113
|
+
(
|
|
3114
|
+
cast(JSONObject, probe.error_payload)
|
|
3115
|
+
for probe in probes
|
|
3116
|
+
if isinstance(probe.error_payload, dict)
|
|
3117
|
+
),
|
|
3118
|
+
None,
|
|
3119
|
+
)
|
|
3120
|
+
backend_code = (
|
|
3121
|
+
cast(int, first_error_payload.get("backend_code"))
|
|
3122
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("backend_code"), int)
|
|
3123
|
+
else exc.backend_code
|
|
3124
|
+
)
|
|
3125
|
+
request_id = (
|
|
3126
|
+
_normalize_optional_text(first_error_payload.get("request_id"))
|
|
3127
|
+
if isinstance(first_error_payload, dict)
|
|
3128
|
+
else None
|
|
3129
|
+
) or exc.request_id
|
|
3130
|
+
http_status = (
|
|
3131
|
+
cast(int, first_error_payload.get("http_status"))
|
|
3132
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("http_status"), int)
|
|
3133
|
+
else exc.http_status
|
|
3134
|
+
)
|
|
3135
|
+
raise_tool_error(
|
|
3136
|
+
QingflowApiError(
|
|
3137
|
+
category="backend",
|
|
3138
|
+
message=(
|
|
3139
|
+
"Direct record edit was blocked because the current record context could not be loaded from any candidate route."
|
|
3140
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3141
|
+
else "Direct record edit was blocked because the target record is no longer accessible in any readable view for the current profile."
|
|
3142
|
+
),
|
|
3143
|
+
backend_code=backend_code,
|
|
3144
|
+
request_id=request_id,
|
|
3145
|
+
http_status=http_status,
|
|
3146
|
+
details={
|
|
3147
|
+
"error_code": blocker,
|
|
3148
|
+
"operation": "update",
|
|
3149
|
+
"app_key": app_key,
|
|
3150
|
+
"record_id": apply_id,
|
|
3151
|
+
"blockers": [blocker],
|
|
3152
|
+
"request_route": self._request_route_payload(context),
|
|
3153
|
+
"view_probe_summary": [
|
|
3154
|
+
self._record_context_probe_summary_payload(probe)
|
|
3155
|
+
for probe in probes
|
|
3156
|
+
],
|
|
3157
|
+
"recommended_next_actions": recommended_next_actions,
|
|
3158
|
+
"fix_hint": (
|
|
3159
|
+
"Confirm the target record still exists and remains visible in at least one accessible view before retrying the update."
|
|
3160
|
+
),
|
|
3161
|
+
},
|
|
3162
|
+
)
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3062
3165
|
def _record_matches_accessible_view(
|
|
3063
3166
|
self,
|
|
3064
3167
|
context, # type: ignore[no-untyped-def]
|
|
@@ -6355,12 +6458,22 @@ class RecordTools(ToolBase):
|
|
|
6355
6458
|
force_refresh_form=force_refresh_form,
|
|
6356
6459
|
)
|
|
6357
6460
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6461
|
+
try:
|
|
6462
|
+
result = self.backend.request(
|
|
6463
|
+
"POST",
|
|
6464
|
+
context,
|
|
6465
|
+
f"/app/{app_key}/apply/{normalized_apply_id}",
|
|
6466
|
+
json_body={"role": role, "answers": normalized_answers},
|
|
6467
|
+
)
|
|
6468
|
+
except QingflowApiError as exc:
|
|
6469
|
+
self._remap_record_update_target_context_error(
|
|
6470
|
+
profile,
|
|
6471
|
+
context,
|
|
6472
|
+
app_key=app_key,
|
|
6473
|
+
apply_id=normalized_apply_id,
|
|
6474
|
+
exc=exc,
|
|
6475
|
+
)
|
|
6476
|
+
raise
|
|
6364
6477
|
verification = self._verify_record_write_result(
|
|
6365
6478
|
context,
|
|
6366
6479
|
app_key=app_key,
|
|
@@ -8810,6 +8923,8 @@ class RecordTools(ToolBase):
|
|
|
8810
8923
|
error_payload["request_id"] = parsed.get("request_id")
|
|
8811
8924
|
if isinstance(parsed.get("request_route"), dict):
|
|
8812
8925
|
request_route = cast(JSONObject, parsed.get("request_route"))
|
|
8926
|
+
elif details_payload is not None and isinstance(details_payload.get("request_route"), dict):
|
|
8927
|
+
request_route = cast(JSONObject, details_payload.get("request_route"))
|
|
8813
8928
|
response: JSONObject = {
|
|
8814
8929
|
"profile": profile,
|
|
8815
8930
|
"ws_id": None,
|
|
@@ -96,7 +96,9 @@ class ResourceReadTools(ToolBase):
|
|
|
96
96
|
)
|
|
97
97
|
)
|
|
98
98
|
if system_view is not None:
|
|
99
|
-
export_capability = _view_export_capability_payload(
|
|
99
|
+
export_capability = _view_export_capability_payload(
|
|
100
|
+
supported=_export_supported_for_view_type(system_view["view_type"])
|
|
101
|
+
)
|
|
100
102
|
return self._run(
|
|
101
103
|
profile,
|
|
102
104
|
lambda session_profile, _context: {
|
|
@@ -133,13 +135,11 @@ class ResourceReadTools(ToolBase):
|
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
def runner(session_profile, context):
|
|
136
|
-
|
|
138
|
+
raw_view_type = None
|
|
137
139
|
warnings: list[JSONObject] = []
|
|
138
140
|
verification = {
|
|
139
141
|
"view_exists": True,
|
|
140
142
|
"questions_verified": True,
|
|
141
|
-
"export_route_supported": export_capability["supported"],
|
|
142
|
-
"export_permission_verified": export_capability["permission_verified"],
|
|
143
143
|
}
|
|
144
144
|
config = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
145
145
|
base_info = self.backend.request("GET", context, f"/view/{view_key}/viewConfig/baseInfo")
|
|
@@ -161,6 +161,11 @@ class ResourceReadTools(ToolBase):
|
|
|
161
161
|
str(base_info.get("viewgraphType") or "").strip()
|
|
162
162
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
163
163
|
)
|
|
164
|
+
export_capability = _view_export_capability_payload(
|
|
165
|
+
supported=_export_supported_for_view_type(raw_view_type or None)
|
|
166
|
+
)
|
|
167
|
+
verification["export_route_supported"] = export_capability["supported"]
|
|
168
|
+
verification["export_permission_verified"] = export_capability["permission_verified"]
|
|
164
169
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
165
170
|
if not resolved_app_key:
|
|
166
171
|
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
@@ -176,12 +181,13 @@ class ResourceReadTools(ToolBase):
|
|
|
176
181
|
"message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
|
|
177
182
|
}
|
|
178
183
|
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
if export_capability["supported"]:
|
|
185
|
+
warnings.append(
|
|
186
|
+
{
|
|
187
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
188
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
185
191
|
return {
|
|
186
192
|
"profile": profile,
|
|
187
193
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -493,6 +499,10 @@ def _view_export_capability_payload(*, supported: bool) -> JSONObject:
|
|
|
493
499
|
}
|
|
494
500
|
|
|
495
501
|
|
|
502
|
+
def _export_supported_for_view_type(view_type: str | None) -> bool:
|
|
503
|
+
return _analysis_supported_for_view_type(view_type)
|
|
504
|
+
|
|
505
|
+
|
|
496
506
|
def _normalize_view_type(view_type: Any) -> str | None:
|
|
497
507
|
value = str(view_type or "").strip()
|
|
498
508
|
if not value:
|