@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12
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 +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -13,7 +13,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
13
13
|
|
|
14
14
|
from ..backend_client import BackendRequestContext
|
|
15
15
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
|
|
16
|
-
from ..errors import QingflowApiError
|
|
16
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, message_looks_like_invalid_token
|
|
17
17
|
from ..export_store import ExportJobStore
|
|
18
18
|
from ..json_types import JSONObject
|
|
19
19
|
from .base import ToolBase, tool_cn_name
|
|
@@ -23,6 +23,8 @@ from .record_tools import (
|
|
|
23
23
|
FormField,
|
|
24
24
|
LAYOUT_ONLY_QUE_TYPES,
|
|
25
25
|
RecordTools,
|
|
26
|
+
_build_top_level_field_index,
|
|
27
|
+
_normalize_data_list_base_info_schema,
|
|
26
28
|
_normalize_public_column_selectors,
|
|
27
29
|
)
|
|
28
30
|
|
|
@@ -59,7 +61,7 @@ class ExportTools(ToolBase):
|
|
|
59
61
|
def record_export_start(
|
|
60
62
|
profile: str = DEFAULT_PROFILE,
|
|
61
63
|
app_key: str = "",
|
|
62
|
-
view_id: str = "
|
|
64
|
+
view_id: str = "",
|
|
63
65
|
columns: list[JSONObject | int] | None = None,
|
|
64
66
|
where: list[JSONObject] | None = None,
|
|
65
67
|
order_by: list[JSONObject] | None = None,
|
|
@@ -103,7 +105,7 @@ class ExportTools(ToolBase):
|
|
|
103
105
|
def record_export_direct(
|
|
104
106
|
profile: str = DEFAULT_PROFILE,
|
|
105
107
|
app_key: str = "",
|
|
106
|
-
view_id: str = "
|
|
108
|
+
view_id: str = "",
|
|
107
109
|
columns: list[JSONObject | int] | None = None,
|
|
108
110
|
where: list[JSONObject] | None = None,
|
|
109
111
|
order_by: list[JSONObject] | None = None,
|
|
@@ -131,7 +133,7 @@ class ExportTools(ToolBase):
|
|
|
131
133
|
*,
|
|
132
134
|
profile: str = DEFAULT_PROFILE,
|
|
133
135
|
app_key: str,
|
|
134
|
-
view_id: str = "
|
|
136
|
+
view_id: str = "",
|
|
135
137
|
columns: list[JSONObject | int] | None = None,
|
|
136
138
|
where: list[JSONObject] | None = None,
|
|
137
139
|
order_by: list[JSONObject] | None = None,
|
|
@@ -139,7 +141,7 @@ class ExportTools(ToolBase):
|
|
|
139
141
|
include_workflow_log: bool = False,
|
|
140
142
|
) -> dict[str, Any]:
|
|
141
143
|
normalized_app_key = str(app_key or "").strip()
|
|
142
|
-
normalized_view_id = str(view_id or "").strip()
|
|
144
|
+
normalized_view_id = str(view_id or "").strip()
|
|
143
145
|
normalized_columns = _normalize_export_columns(columns or [])
|
|
144
146
|
normalized_where = self._record_tools._normalize_record_list_where(where or [])
|
|
145
147
|
normalized_order_by = self._record_tools._normalize_record_list_order_by(order_by or [])
|
|
@@ -150,6 +152,12 @@ class ExportTools(ToolBase):
|
|
|
150
152
|
message="app_key is required",
|
|
151
153
|
extra={"view_id": normalized_view_id, "status": "failed"},
|
|
152
154
|
)
|
|
155
|
+
if not normalized_view_id:
|
|
156
|
+
return self._failed_export_result(
|
|
157
|
+
error_code="EXPORT_VIEW_REQUIRED",
|
|
158
|
+
message="view_id is required; call app_get first and pass accessible_views[].view_id or use the view_id from the frontend URL",
|
|
159
|
+
extra={"app_key": normalized_app_key, "view_id": normalized_view_id, "status": "failed"},
|
|
160
|
+
)
|
|
153
161
|
|
|
154
162
|
def runner(session_profile, context):
|
|
155
163
|
resolved_view, compatibility_warnings = self._record_tools._resolve_accessible_view_route(
|
|
@@ -160,7 +168,7 @@ class ExportTools(ToolBase):
|
|
|
160
168
|
list_type=None,
|
|
161
169
|
view_key=None,
|
|
162
170
|
view_name=None,
|
|
163
|
-
allow_default=
|
|
171
|
+
allow_default=False,
|
|
164
172
|
)
|
|
165
173
|
export_config, export_config_warnings = self._build_export_config(
|
|
166
174
|
profile=profile,
|
|
@@ -238,6 +246,8 @@ class ExportTools(ToolBase):
|
|
|
238
246
|
return {
|
|
239
247
|
"ok": True,
|
|
240
248
|
"status": "accepted",
|
|
249
|
+
"export_executed": True,
|
|
250
|
+
"safe_to_retry_export": False,
|
|
241
251
|
"app_key": normalized_app_key,
|
|
242
252
|
"view_id": resolved_view.view_id,
|
|
243
253
|
"export_handle": export_handle,
|
|
@@ -404,13 +414,17 @@ class ExportTools(ToolBase):
|
|
|
404
414
|
"verification": snapshot.get("verification") or {},
|
|
405
415
|
},
|
|
406
416
|
)
|
|
407
|
-
downloaded_files = self._download_export_files(
|
|
417
|
+
downloaded_files, download_warnings = self._download_export_files(
|
|
408
418
|
file_infos=file_infos,
|
|
409
419
|
download_to_path=download_to_path,
|
|
410
420
|
default_directory=None,
|
|
411
421
|
app_key=str(local_job.get("app_key") or ""),
|
|
412
422
|
view_id=str(local_job.get("view_id") or ""),
|
|
413
423
|
)
|
|
424
|
+
warnings = [
|
|
425
|
+
*cast(list[JSONObject], snapshot.get("warnings") or []),
|
|
426
|
+
*download_warnings,
|
|
427
|
+
]
|
|
414
428
|
return {
|
|
415
429
|
"ok": True,
|
|
416
430
|
"status": "succeeded",
|
|
@@ -429,7 +443,7 @@ class ExportTools(ToolBase):
|
|
|
429
443
|
"file_urls": snapshot.get("file_urls") or [],
|
|
430
444
|
"file_names": snapshot.get("file_names") or [],
|
|
431
445
|
"downloaded_files": downloaded_files,
|
|
432
|
-
"warnings":
|
|
446
|
+
"warnings": warnings,
|
|
433
447
|
"verification": snapshot.get("verification") or {},
|
|
434
448
|
"request_route": self.backend.describe_route(lookup_context),
|
|
435
449
|
}
|
|
@@ -449,7 +463,7 @@ class ExportTools(ToolBase):
|
|
|
449
463
|
*,
|
|
450
464
|
profile: str = DEFAULT_PROFILE,
|
|
451
465
|
app_key: str,
|
|
452
|
-
view_id: str = "
|
|
466
|
+
view_id: str = "",
|
|
453
467
|
columns: list[JSONObject | int] | None = None,
|
|
454
468
|
where: list[JSONObject] | None = None,
|
|
455
469
|
order_by: list[JSONObject] | None = None,
|
|
@@ -459,13 +473,19 @@ class ExportTools(ToolBase):
|
|
|
459
473
|
wait_timeout_seconds: float | None = None,
|
|
460
474
|
) -> dict[str, Any]:
|
|
461
475
|
normalized_app_key = str(app_key or "").strip()
|
|
462
|
-
normalized_view_id = str(view_id or "").strip()
|
|
476
|
+
normalized_view_id = str(view_id or "").strip()
|
|
463
477
|
if not normalized_app_key:
|
|
464
478
|
return self._failed_export_result(
|
|
465
479
|
error_code="EXPORT_START_FAILED",
|
|
466
480
|
message="app_key is required",
|
|
467
481
|
extra={"status": "failed", "view_id": normalized_view_id},
|
|
468
482
|
)
|
|
483
|
+
if not normalized_view_id:
|
|
484
|
+
return self._failed_export_result(
|
|
485
|
+
error_code="EXPORT_VIEW_REQUIRED",
|
|
486
|
+
message="view_id is required; call app_get first and pass accessible_views[].view_id or use the view_id from the frontend URL",
|
|
487
|
+
extra={"status": "failed", "app_key": normalized_app_key, "view_id": normalized_view_id},
|
|
488
|
+
)
|
|
469
489
|
timeout_seconds = self._normalize_timeout_seconds(wait_timeout_seconds)
|
|
470
490
|
|
|
471
491
|
def runner(session_profile, context):
|
|
@@ -509,9 +529,15 @@ class ExportTools(ToolBase):
|
|
|
509
529
|
download_to_path=effective_download_path,
|
|
510
530
|
)
|
|
511
531
|
if bool(get_result.get("ok")):
|
|
532
|
+
get_result = dict(get_result)
|
|
533
|
+
get_result.setdefault("export_executed", True)
|
|
534
|
+
get_result.setdefault("safe_to_retry_export", False)
|
|
535
|
+
get_result.setdefault("export_handle", export_handle)
|
|
512
536
|
return get_result
|
|
513
537
|
return {
|
|
514
538
|
**get_result,
|
|
539
|
+
"export_executed": True,
|
|
540
|
+
"safe_to_retry_export": False,
|
|
515
541
|
"export_handle": export_handle,
|
|
516
542
|
"file_urls": snapshot.get("file_urls") or [],
|
|
517
543
|
"file_names": snapshot.get("file_names") or [],
|
|
@@ -547,8 +573,11 @@ class ExportTools(ToolBase):
|
|
|
547
573
|
}
|
|
548
574
|
if "EXPORT_HISTORY_AMBIGUOUS" in warning_codes:
|
|
549
575
|
return {
|
|
550
|
-
"ok":
|
|
576
|
+
"ok": False,
|
|
551
577
|
"status": "unknown",
|
|
578
|
+
"error_code": "EXPORT_STATUS_UNKNOWN",
|
|
579
|
+
"export_executed": True,
|
|
580
|
+
"safe_to_retry_export": False,
|
|
552
581
|
"export_handle": export_handle,
|
|
553
582
|
"app_key": str(local_job.get("app_key") or ""),
|
|
554
583
|
"view_id": str(local_job.get("view_id") or ""),
|
|
@@ -579,8 +608,11 @@ class ExportTools(ToolBase):
|
|
|
579
608
|
}
|
|
580
609
|
)
|
|
581
610
|
return {
|
|
582
|
-
"ok":
|
|
611
|
+
"ok": False,
|
|
583
612
|
"status": timeout_status,
|
|
613
|
+
"error_code": "EXPORT_WAIT_TIMEOUT",
|
|
614
|
+
"export_executed": True,
|
|
615
|
+
"safe_to_retry_export": False,
|
|
584
616
|
"export_handle": str(start_result.get("export_handle") or ""),
|
|
585
617
|
"app_key": normalized_app_key,
|
|
586
618
|
"view_id": str(start_result.get("view_id") or normalized_view_id),
|
|
@@ -704,7 +736,7 @@ class ExportTools(ToolBase):
|
|
|
704
736
|
order_by: list[JSONObject],
|
|
705
737
|
select_columns: list[JSONObject],
|
|
706
738
|
) -> list[int]:
|
|
707
|
-
browse_scope = self.
|
|
739
|
+
browse_scope = self._build_export_read_scope(
|
|
708
740
|
profile,
|
|
709
741
|
context,
|
|
710
742
|
app_key,
|
|
@@ -841,7 +873,7 @@ class ExportTools(ToolBase):
|
|
|
841
873
|
resolved_view: AccessibleViewRoute,
|
|
842
874
|
column_selectors: list[int],
|
|
843
875
|
) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
844
|
-
browse_scope = self.
|
|
876
|
+
browse_scope = self._build_export_read_scope(
|
|
845
877
|
profile,
|
|
846
878
|
context,
|
|
847
879
|
app_key,
|
|
@@ -912,6 +944,71 @@ class ExportTools(ToolBase):
|
|
|
912
944
|
)
|
|
913
945
|
return {"questionExportConfigList": question_export_config_list}, warnings
|
|
914
946
|
|
|
947
|
+
def _build_export_read_scope(
|
|
948
|
+
self,
|
|
949
|
+
profile: str,
|
|
950
|
+
context,
|
|
951
|
+
app_key: str,
|
|
952
|
+
resolved_view: AccessibleViewRoute,
|
|
953
|
+
*,
|
|
954
|
+
force_refresh: bool,
|
|
955
|
+
) -> JSONObject:
|
|
956
|
+
try:
|
|
957
|
+
scope = self._record_tools._build_browse_read_scope(
|
|
958
|
+
profile,
|
|
959
|
+
context,
|
|
960
|
+
app_key,
|
|
961
|
+
resolved_view,
|
|
962
|
+
force_refresh=force_refresh,
|
|
963
|
+
)
|
|
964
|
+
except QingflowApiError as exc:
|
|
965
|
+
if not _is_optional_export_lookup_error(exc):
|
|
966
|
+
raise
|
|
967
|
+
scope = {}
|
|
968
|
+
index = scope.get("index") if isinstance(scope, dict) else None
|
|
969
|
+
if getattr(index, "by_id", None):
|
|
970
|
+
return scope
|
|
971
|
+
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
972
|
+
try:
|
|
973
|
+
list_base_scope = self._build_system_export_list_base_info_scope(context, app_key)
|
|
974
|
+
except QingflowApiError as exc:
|
|
975
|
+
if not _is_optional_export_lookup_error(exc):
|
|
976
|
+
raise
|
|
977
|
+
else:
|
|
978
|
+
list_base_index = list_base_scope.get("index")
|
|
979
|
+
if getattr(list_base_index, "by_id", None):
|
|
980
|
+
return list_base_scope
|
|
981
|
+
try:
|
|
982
|
+
applicant_index = self._record_tools._get_applicant_top_level_field_index(
|
|
983
|
+
profile,
|
|
984
|
+
context,
|
|
985
|
+
app_key,
|
|
986
|
+
force_refresh=force_refresh,
|
|
987
|
+
)
|
|
988
|
+
except QingflowApiError as exc:
|
|
989
|
+
if not _is_optional_export_lookup_error(exc):
|
|
990
|
+
raise
|
|
991
|
+
applicant_index = None
|
|
992
|
+
if applicant_index is not None and applicant_index.by_id:
|
|
993
|
+
visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
|
|
994
|
+
return {
|
|
995
|
+
"index": applicant_index,
|
|
996
|
+
"writable_field_ids": set(),
|
|
997
|
+
"visible_question_ids": visible_question_ids,
|
|
998
|
+
}
|
|
999
|
+
return scope or {"index": _build_top_level_field_index({}), "writable_field_ids": set(), "visible_question_ids": set()}
|
|
1000
|
+
|
|
1001
|
+
def _build_system_export_list_base_info_scope(self, context, app_key: str) -> JSONObject:
|
|
1002
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/data/listBaseInfo")
|
|
1003
|
+
schema = _normalize_data_list_base_info_schema(payload)
|
|
1004
|
+
index = _build_top_level_field_index(schema)
|
|
1005
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
1006
|
+
return {
|
|
1007
|
+
"index": index,
|
|
1008
|
+
"writable_field_ids": set(),
|
|
1009
|
+
"visible_question_ids": visible_question_ids,
|
|
1010
|
+
}
|
|
1011
|
+
|
|
915
1012
|
def _resolve_exportable_fields(
|
|
916
1013
|
self,
|
|
917
1014
|
*,
|
|
@@ -1047,16 +1144,35 @@ class ExportTools(ToolBase):
|
|
|
1047
1144
|
) -> dict[str, Any]: # type: ignore[no-untyped-def]
|
|
1048
1145
|
app_key = str(local_job.get("app_key") or "").strip()
|
|
1049
1146
|
process_payload = self._lookup_process_details(context, app_key=app_key)
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1147
|
+
history_unavailable_warning: JSONObject | None = None
|
|
1148
|
+
try:
|
|
1149
|
+
history_page = self.backend.request(
|
|
1150
|
+
"GET",
|
|
1151
|
+
context,
|
|
1152
|
+
"/app/apply/dataExport/record",
|
|
1153
|
+
params={"appKey": app_key, "pageNum": 1, "pageSize": 100},
|
|
1154
|
+
)
|
|
1155
|
+
except QingflowApiError as exc:
|
|
1156
|
+
if not _is_optional_export_lookup_error(exc):
|
|
1157
|
+
raise
|
|
1158
|
+
history_page = {"list": []}
|
|
1159
|
+
history_unavailable_warning = {
|
|
1160
|
+
"code": "EXPORT_HISTORY_UNAVAILABLE",
|
|
1161
|
+
"message": "export history is not readable for the current user; using current process details when available.",
|
|
1162
|
+
}
|
|
1163
|
+
if exc.category:
|
|
1164
|
+
history_unavailable_warning["category"] = exc.category
|
|
1165
|
+
if exc.backend_code is not None:
|
|
1166
|
+
history_unavailable_warning["backend_code"] = exc.backend_code
|
|
1167
|
+
if exc.http_status is not None:
|
|
1168
|
+
history_unavailable_warning["http_status"] = exc.http_status
|
|
1169
|
+
if exc.request_id:
|
|
1170
|
+
history_unavailable_warning["request_id"] = exc.request_id
|
|
1056
1171
|
history_records = _extract_export_records(history_page)
|
|
1057
1172
|
matched_record, matched_by = _match_export_history_record(history_records, local_job=local_job)
|
|
1058
1173
|
if process_payload is not None:
|
|
1059
1174
|
normalized_status = _normalize_export_status(process_payload.get("processStatus") or process_payload.get("status"))
|
|
1175
|
+
process_file_infos = _normalize_export_file_infos(process_payload.get("fileUrls"))
|
|
1060
1176
|
if normalized_status in {"queued", "running"}:
|
|
1061
1177
|
return {
|
|
1062
1178
|
"status": normalized_status,
|
|
@@ -1064,22 +1180,47 @@ class ExportTools(ToolBase):
|
|
|
1064
1180
|
"num": _coerce_int(process_payload.get("num")),
|
|
1065
1181
|
"error_code": process_payload.get("errorCode"),
|
|
1066
1182
|
"audit_record_status": process_payload.get("auditRecordStatus"),
|
|
1067
|
-
"file_infos":
|
|
1068
|
-
"file_urls":
|
|
1069
|
-
"file_names":
|
|
1070
|
-
"warnings": [],
|
|
1183
|
+
"file_infos": process_file_infos,
|
|
1184
|
+
"file_urls": [item.get("url") for item in process_file_infos if isinstance(item.get("url"), str)],
|
|
1185
|
+
"file_names": [item.get("name") for item in process_file_infos if isinstance(item.get("name"), str)],
|
|
1186
|
+
"warnings": [history_unavailable_warning] if history_unavailable_warning is not None else [],
|
|
1071
1187
|
"verification": {
|
|
1072
1188
|
"current_process_visible": True,
|
|
1073
1189
|
"history_match_resolved": matched_record is not None,
|
|
1190
|
+
"history_readable": history_unavailable_warning is None,
|
|
1074
1191
|
},
|
|
1075
1192
|
"message": None,
|
|
1076
1193
|
}
|
|
1194
|
+
if normalized_status in {"succeeded", "failed"} and (process_file_infos or normalized_status == "failed"):
|
|
1195
|
+
message = "export failed" if normalized_status == "failed" else None
|
|
1196
|
+
return {
|
|
1197
|
+
"status": normalized_status,
|
|
1198
|
+
"process_status": _coerce_int(process_payload.get("processStatus") or process_payload.get("status")),
|
|
1199
|
+
"num": _coerce_int(process_payload.get("num")),
|
|
1200
|
+
"error_code": process_payload.get("errorCode"),
|
|
1201
|
+
"audit_record_status": process_payload.get("auditRecordStatus"),
|
|
1202
|
+
"file_infos": process_file_infos,
|
|
1203
|
+
"file_urls": [item.get("url") for item in process_file_infos if isinstance(item.get("url"), str)],
|
|
1204
|
+
"file_names": [item.get("name") for item in process_file_infos if isinstance(item.get("name"), str)],
|
|
1205
|
+
"warnings": [history_unavailable_warning] if history_unavailable_warning is not None else [],
|
|
1206
|
+
"verification": {
|
|
1207
|
+
"current_process_visible": True,
|
|
1208
|
+
"history_match_resolved": matched_record is not None,
|
|
1209
|
+
"history_readable": history_unavailable_warning is None,
|
|
1210
|
+
"matched_by": "current_process",
|
|
1211
|
+
},
|
|
1212
|
+
"message": message,
|
|
1213
|
+
}
|
|
1077
1214
|
if matched_record is None:
|
|
1078
1215
|
warning_code = "EXPORT_HISTORY_PENDING"
|
|
1079
1216
|
warning_message = "export has not appeared in export history yet"
|
|
1080
1217
|
if matched_by == "ambiguous":
|
|
1081
1218
|
warning_code = "EXPORT_HISTORY_AMBIGUOUS"
|
|
1082
1219
|
warning_message = "export result could not be matched uniquely in export history"
|
|
1220
|
+
warnings = [{"code": warning_code, "message": warning_message}]
|
|
1221
|
+
if history_unavailable_warning is not None:
|
|
1222
|
+
warnings = [history_unavailable_warning]
|
|
1223
|
+
warning_message = str(history_unavailable_warning["message"])
|
|
1083
1224
|
return {
|
|
1084
1225
|
"status": "unknown",
|
|
1085
1226
|
"process_status": None,
|
|
@@ -1089,10 +1230,11 @@ class ExportTools(ToolBase):
|
|
|
1089
1230
|
"file_infos": [],
|
|
1090
1231
|
"file_urls": [],
|
|
1091
1232
|
"file_names": [],
|
|
1092
|
-
"warnings":
|
|
1233
|
+
"warnings": warnings,
|
|
1093
1234
|
"verification": {
|
|
1094
1235
|
"current_process_visible": process_payload is not None,
|
|
1095
1236
|
"history_match_resolved": False,
|
|
1237
|
+
"history_readable": history_unavailable_warning is None,
|
|
1096
1238
|
},
|
|
1097
1239
|
"message": warning_message,
|
|
1098
1240
|
}
|
|
@@ -1129,7 +1271,7 @@ class ExportTools(ToolBase):
|
|
|
1129
1271
|
params={"taskType": 2},
|
|
1130
1272
|
)
|
|
1131
1273
|
except QingflowApiError as exc:
|
|
1132
|
-
if exc
|
|
1274
|
+
if _is_optional_export_lookup_error(exc):
|
|
1133
1275
|
return None
|
|
1134
1276
|
raise
|
|
1135
1277
|
if isinstance(payload, dict):
|
|
@@ -1183,9 +1325,9 @@ class ExportTools(ToolBase):
|
|
|
1183
1325
|
default_directory: str | None,
|
|
1184
1326
|
app_key: str,
|
|
1185
1327
|
view_id: str,
|
|
1186
|
-
) -> list[JSONObject]:
|
|
1328
|
+
) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
1187
1329
|
if download_to_path is None and default_directory is None:
|
|
1188
|
-
return []
|
|
1330
|
+
return [], []
|
|
1189
1331
|
effective_hint = download_to_path or default_directory
|
|
1190
1332
|
assert effective_hint is not None
|
|
1191
1333
|
targets = _resolve_download_targets(
|
|
@@ -1195,13 +1337,45 @@ class ExportTools(ToolBase):
|
|
|
1195
1337
|
view_id=view_id,
|
|
1196
1338
|
)
|
|
1197
1339
|
downloaded_files: list[JSONObject] = []
|
|
1340
|
+
warnings: list[JSONObject] = []
|
|
1198
1341
|
for file_info, target in zip(file_infos, targets, strict=False):
|
|
1199
1342
|
url = str(file_info.get("url") or "").strip()
|
|
1200
1343
|
if not url:
|
|
1201
1344
|
continue
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1345
|
+
try:
|
|
1346
|
+
content = self.backend.download_binary(url)
|
|
1347
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1348
|
+
target.write_bytes(content)
|
|
1349
|
+
except QingflowApiError as exc:
|
|
1350
|
+
warning: JSONObject = {
|
|
1351
|
+
"code": "EXPORT_FILE_DOWNLOAD_UNAVAILABLE",
|
|
1352
|
+
"message": "export file link is available, but local download failed; use file_urls or retry download later.",
|
|
1353
|
+
"file_name": str(file_info.get("name") or target.name),
|
|
1354
|
+
"url": url,
|
|
1355
|
+
"category": exc.category,
|
|
1356
|
+
"backend_code": exc.backend_code,
|
|
1357
|
+
"request_id": exc.request_id,
|
|
1358
|
+
"http_status": exc.http_status,
|
|
1359
|
+
}
|
|
1360
|
+
if is_auth_like_error(exc):
|
|
1361
|
+
warning["auth_like"] = True
|
|
1362
|
+
warning["error_code"] = "AUTH_REQUIRED"
|
|
1363
|
+
if exc.details:
|
|
1364
|
+
warning["details"] = exc.details
|
|
1365
|
+
warnings.append(warning)
|
|
1366
|
+
continue
|
|
1367
|
+
except OSError as exc:
|
|
1368
|
+
warnings.append(
|
|
1369
|
+
{
|
|
1370
|
+
"code": "EXPORT_FILE_WRITE_UNAVAILABLE",
|
|
1371
|
+
"message": "export file link is available, but writing the local file failed; use file_urls or retry with another download_to_path.",
|
|
1372
|
+
"file_name": str(file_info.get("name") or target.name),
|
|
1373
|
+
"url": url,
|
|
1374
|
+
"path": str(target),
|
|
1375
|
+
"error": str(exc),
|
|
1376
|
+
}
|
|
1377
|
+
)
|
|
1378
|
+
continue
|
|
1205
1379
|
downloaded_files.append(
|
|
1206
1380
|
{
|
|
1207
1381
|
"file_name": str(file_info.get("name") or target.name),
|
|
@@ -1209,7 +1383,7 @@ class ExportTools(ToolBase):
|
|
|
1209
1383
|
"url": url,
|
|
1210
1384
|
}
|
|
1211
1385
|
)
|
|
1212
|
-
return downloaded_files
|
|
1386
|
+
return downloaded_files, warnings
|
|
1213
1387
|
|
|
1214
1388
|
def _normalize_timeout_seconds(self, wait_timeout_seconds: float | None) -> float:
|
|
1215
1389
|
if wait_timeout_seconds is None:
|
|
@@ -1259,10 +1433,16 @@ class ExportTools(ToolBase):
|
|
|
1259
1433
|
payload = json.loads(str(error))
|
|
1260
1434
|
except json.JSONDecodeError:
|
|
1261
1435
|
payload = {"message": str(error)}
|
|
1436
|
+
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
1262
1437
|
response = self._failed_export_result(
|
|
1263
|
-
error_code=
|
|
1438
|
+
error_code=details.get("error_code") or _runtime_error_code(payload, default=error_code),
|
|
1264
1439
|
message=str(payload.get("message") or str(error)),
|
|
1265
1440
|
)
|
|
1441
|
+
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
1442
|
+
if key in payload:
|
|
1443
|
+
response[key] = payload.get(key)
|
|
1444
|
+
if details:
|
|
1445
|
+
response["details"] = details
|
|
1266
1446
|
if extra:
|
|
1267
1447
|
response.update(extra)
|
|
1268
1448
|
return response
|
|
@@ -1279,6 +1459,23 @@ def _extract_export_records(payload: Any) -> list[JSONObject]:
|
|
|
1279
1459
|
return []
|
|
1280
1460
|
|
|
1281
1461
|
|
|
1462
|
+
def _is_optional_export_lookup_error(error: QingflowApiError) -> bool:
|
|
1463
|
+
if is_auth_like_error(error):
|
|
1464
|
+
return False
|
|
1465
|
+
backend_code = backend_code_int(error)
|
|
1466
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _runtime_error_code(payload: JSONObject, *, default: str) -> str:
|
|
1470
|
+
category = str(payload.get("category") or "").strip().lower()
|
|
1471
|
+
http_status = _coerce_int(payload.get("http_status"))
|
|
1472
|
+
if category == "auth" or http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
|
|
1473
|
+
return "AUTH_REQUIRED"
|
|
1474
|
+
if category == "workspace":
|
|
1475
|
+
return "WORKSPACE_NOT_SELECTED"
|
|
1476
|
+
return default
|
|
1477
|
+
|
|
1478
|
+
|
|
1282
1479
|
def _match_export_history_record(
|
|
1283
1480
|
records: list[JSONObject],
|
|
1284
1481
|
*,
|
|
@@ -12,12 +12,12 @@ from urllib.parse import quote
|
|
|
12
12
|
from mcp.server.fastmcp import FastMCP
|
|
13
13
|
|
|
14
14
|
from ..config import DEFAULT_PROFILE
|
|
15
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
15
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
16
16
|
from ..json_types import JSONObject
|
|
17
17
|
from .base import ToolBase, tool_cn_name
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES = {
|
|
20
|
+
ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES = {40118}
|
|
21
21
|
LEGACY_OSS_FORM_REQUIRED_KEYS = ("key", "policy", "signature", "ossAccessKeyId")
|
|
22
22
|
|
|
23
23
|
|
|
@@ -190,6 +190,8 @@ class FileTools(ToolBase):
|
|
|
190
190
|
"result": result,
|
|
191
191
|
"upload_result": upload_result,
|
|
192
192
|
"download_url": download_url,
|
|
193
|
+
"write_executed": True,
|
|
194
|
+
"safe_to_retry": False,
|
|
193
195
|
"attachment_value": {
|
|
194
196
|
"value": download_url,
|
|
195
197
|
"otherInfo": file_name,
|
|
@@ -357,10 +359,12 @@ class FileTools(ToolBase):
|
|
|
357
359
|
error: QingflowApiError,
|
|
358
360
|
) -> bool:
|
|
359
361
|
"""执行内部辅助逻辑。"""
|
|
362
|
+
if is_auth_like_error(error):
|
|
363
|
+
return False
|
|
360
364
|
return (
|
|
361
365
|
requested_kind == "attachment"
|
|
362
366
|
and attempted_kind == "attachment"
|
|
363
|
-
and error
|
|
367
|
+
and backend_code_int(error) in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
|
|
364
368
|
)
|
|
365
369
|
|
|
366
370
|
def _is_legacy_oss_form_upload(self, payload: JSONObject) -> bool:
|