@josephyan/qingflow-app-builder-mcp 1.1.24 → 1.1.26
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 +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -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 +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3100
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -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
|
|
17
17
|
from ..export_store import ExportJobStore
|
|
18
18
|
from ..json_types import JSONObject
|
|
19
19
|
from .base import ToolBase, tool_cn_name
|
|
@@ -23,8 +23,6 @@ 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,
|
|
28
26
|
_normalize_public_column_selectors,
|
|
29
27
|
)
|
|
30
28
|
|
|
@@ -61,11 +59,10 @@ class ExportTools(ToolBase):
|
|
|
61
59
|
def record_export_start(
|
|
62
60
|
profile: str = DEFAULT_PROFILE,
|
|
63
61
|
app_key: str = "",
|
|
64
|
-
view_id: str = "",
|
|
62
|
+
view_id: str = "system:all",
|
|
65
63
|
columns: list[JSONObject | int] | None = None,
|
|
66
64
|
where: list[JSONObject] | None = None,
|
|
67
65
|
order_by: list[JSONObject] | None = None,
|
|
68
|
-
record_id: str | int | None = None,
|
|
69
66
|
record_ids: list[str | int] | None = None,
|
|
70
67
|
include_workflow_log: bool = False,
|
|
71
68
|
) -> dict[str, Any]:
|
|
@@ -76,7 +73,6 @@ class ExportTools(ToolBase):
|
|
|
76
73
|
columns=columns or [],
|
|
77
74
|
where=where or [],
|
|
78
75
|
order_by=order_by or [],
|
|
79
|
-
record_id=record_id,
|
|
80
76
|
record_ids=record_ids or [],
|
|
81
77
|
include_workflow_log=include_workflow_log,
|
|
82
78
|
)
|
|
@@ -107,11 +103,10 @@ class ExportTools(ToolBase):
|
|
|
107
103
|
def record_export_direct(
|
|
108
104
|
profile: str = DEFAULT_PROFILE,
|
|
109
105
|
app_key: str = "",
|
|
110
|
-
view_id: str = "",
|
|
106
|
+
view_id: str = "system:all",
|
|
111
107
|
columns: list[JSONObject | int] | None = None,
|
|
112
108
|
where: list[JSONObject] | None = None,
|
|
113
109
|
order_by: list[JSONObject] | None = None,
|
|
114
|
-
record_id: str | int | None = None,
|
|
115
110
|
record_ids: list[str | int] | None = None,
|
|
116
111
|
include_workflow_log: bool = False,
|
|
117
112
|
download_to_path: str | None = None,
|
|
@@ -124,7 +119,6 @@ class ExportTools(ToolBase):
|
|
|
124
119
|
columns=columns or [],
|
|
125
120
|
where=where or [],
|
|
126
121
|
order_by=order_by or [],
|
|
127
|
-
record_id=record_id,
|
|
128
122
|
record_ids=record_ids or [],
|
|
129
123
|
include_workflow_log=include_workflow_log,
|
|
130
124
|
download_to_path=download_to_path,
|
|
@@ -137,32 +131,25 @@ class ExportTools(ToolBase):
|
|
|
137
131
|
*,
|
|
138
132
|
profile: str = DEFAULT_PROFILE,
|
|
139
133
|
app_key: str,
|
|
140
|
-
view_id: str = "",
|
|
134
|
+
view_id: str = "system:all",
|
|
141
135
|
columns: list[JSONObject | int] | None = None,
|
|
142
136
|
where: list[JSONObject] | None = None,
|
|
143
137
|
order_by: list[JSONObject] | None = None,
|
|
144
|
-
record_id: str | int | None = None,
|
|
145
138
|
record_ids: list[str | int] | None = None,
|
|
146
139
|
include_workflow_log: bool = False,
|
|
147
140
|
) -> dict[str, Any]:
|
|
148
141
|
normalized_app_key = str(app_key or "").strip()
|
|
149
|
-
normalized_view_id = str(view_id or "").strip()
|
|
142
|
+
normalized_view_id = str(view_id or "").strip() or "system:all"
|
|
150
143
|
normalized_columns = _normalize_export_columns(columns or [])
|
|
151
144
|
normalized_where = self._record_tools._normalize_record_list_where(where or [])
|
|
152
145
|
normalized_order_by = self._record_tools._normalize_record_list_order_by(order_by or [])
|
|
153
|
-
normalized_record_ids = _normalize_export_record_ids(
|
|
146
|
+
normalized_record_ids = _normalize_export_record_ids(record_ids or [])
|
|
154
147
|
if not normalized_app_key:
|
|
155
148
|
return self._failed_export_result(
|
|
156
149
|
error_code="EXPORT_START_FAILED",
|
|
157
150
|
message="app_key is required",
|
|
158
151
|
extra={"view_id": normalized_view_id, "status": "failed"},
|
|
159
152
|
)
|
|
160
|
-
if not normalized_view_id:
|
|
161
|
-
return self._failed_export_result(
|
|
162
|
-
error_code="EXPORT_VIEW_REQUIRED",
|
|
163
|
-
message="view_id is required; call app_get first and pass accessible_views[].view_id or use the view_id from the frontend URL",
|
|
164
|
-
extra={"app_key": normalized_app_key, "view_id": normalized_view_id, "status": "failed"},
|
|
165
|
-
)
|
|
166
153
|
|
|
167
154
|
def runner(session_profile, context):
|
|
168
155
|
resolved_view, compatibility_warnings = self._record_tools._resolve_accessible_view_route(
|
|
@@ -173,7 +160,7 @@ class ExportTools(ToolBase):
|
|
|
173
160
|
list_type=None,
|
|
174
161
|
view_key=None,
|
|
175
162
|
view_name=None,
|
|
176
|
-
allow_default=
|
|
163
|
+
allow_default=True,
|
|
177
164
|
)
|
|
178
165
|
export_config, export_config_warnings = self._build_export_config(
|
|
179
166
|
profile=profile,
|
|
@@ -251,8 +238,6 @@ class ExportTools(ToolBase):
|
|
|
251
238
|
return {
|
|
252
239
|
"ok": True,
|
|
253
240
|
"status": "accepted",
|
|
254
|
-
"export_executed": True,
|
|
255
|
-
"safe_to_retry_export": False,
|
|
256
241
|
"app_key": normalized_app_key,
|
|
257
242
|
"view_id": resolved_view.view_id,
|
|
258
243
|
"export_handle": export_handle,
|
|
@@ -419,17 +404,13 @@ class ExportTools(ToolBase):
|
|
|
419
404
|
"verification": snapshot.get("verification") or {},
|
|
420
405
|
},
|
|
421
406
|
)
|
|
422
|
-
downloaded_files
|
|
407
|
+
downloaded_files = self._download_export_files(
|
|
423
408
|
file_infos=file_infos,
|
|
424
409
|
download_to_path=download_to_path,
|
|
425
410
|
default_directory=None,
|
|
426
411
|
app_key=str(local_job.get("app_key") or ""),
|
|
427
412
|
view_id=str(local_job.get("view_id") or ""),
|
|
428
413
|
)
|
|
429
|
-
warnings = [
|
|
430
|
-
*cast(list[JSONObject], snapshot.get("warnings") or []),
|
|
431
|
-
*download_warnings,
|
|
432
|
-
]
|
|
433
414
|
return {
|
|
434
415
|
"ok": True,
|
|
435
416
|
"status": "succeeded",
|
|
@@ -448,7 +429,7 @@ class ExportTools(ToolBase):
|
|
|
448
429
|
"file_urls": snapshot.get("file_urls") or [],
|
|
449
430
|
"file_names": snapshot.get("file_names") or [],
|
|
450
431
|
"downloaded_files": downloaded_files,
|
|
451
|
-
"warnings": warnings,
|
|
432
|
+
"warnings": snapshot.get("warnings") or [],
|
|
452
433
|
"verification": snapshot.get("verification") or {},
|
|
453
434
|
"request_route": self.backend.describe_route(lookup_context),
|
|
454
435
|
}
|
|
@@ -468,30 +449,23 @@ class ExportTools(ToolBase):
|
|
|
468
449
|
*,
|
|
469
450
|
profile: str = DEFAULT_PROFILE,
|
|
470
451
|
app_key: str,
|
|
471
|
-
view_id: str = "",
|
|
452
|
+
view_id: str = "system:all",
|
|
472
453
|
columns: list[JSONObject | int] | None = None,
|
|
473
454
|
where: list[JSONObject] | None = None,
|
|
474
455
|
order_by: list[JSONObject] | None = None,
|
|
475
|
-
record_id: str | int | None = None,
|
|
476
456
|
record_ids: list[str | int] | None = None,
|
|
477
457
|
include_workflow_log: bool = False,
|
|
478
458
|
download_to_path: str | None = None,
|
|
479
459
|
wait_timeout_seconds: float | None = None,
|
|
480
460
|
) -> dict[str, Any]:
|
|
481
461
|
normalized_app_key = str(app_key or "").strip()
|
|
482
|
-
normalized_view_id = str(view_id or "").strip()
|
|
462
|
+
normalized_view_id = str(view_id or "").strip() or "system:all"
|
|
483
463
|
if not normalized_app_key:
|
|
484
464
|
return self._failed_export_result(
|
|
485
465
|
error_code="EXPORT_START_FAILED",
|
|
486
466
|
message="app_key is required",
|
|
487
467
|
extra={"status": "failed", "view_id": normalized_view_id},
|
|
488
468
|
)
|
|
489
|
-
if not normalized_view_id:
|
|
490
|
-
return self._failed_export_result(
|
|
491
|
-
error_code="EXPORT_VIEW_REQUIRED",
|
|
492
|
-
message="view_id is required; call app_get first and pass accessible_views[].view_id or use the view_id from the frontend URL",
|
|
493
|
-
extra={"status": "failed", "app_key": normalized_app_key, "view_id": normalized_view_id},
|
|
494
|
-
)
|
|
495
469
|
timeout_seconds = self._normalize_timeout_seconds(wait_timeout_seconds)
|
|
496
470
|
|
|
497
471
|
def runner(session_profile, context):
|
|
@@ -502,7 +476,6 @@ class ExportTools(ToolBase):
|
|
|
502
476
|
columns=columns or [],
|
|
503
477
|
where=where or [],
|
|
504
478
|
order_by=order_by or [],
|
|
505
|
-
record_id=record_id,
|
|
506
479
|
record_ids=record_ids or [],
|
|
507
480
|
include_workflow_log=include_workflow_log,
|
|
508
481
|
)
|
|
@@ -536,15 +509,9 @@ class ExportTools(ToolBase):
|
|
|
536
509
|
download_to_path=effective_download_path,
|
|
537
510
|
)
|
|
538
511
|
if bool(get_result.get("ok")):
|
|
539
|
-
get_result = dict(get_result)
|
|
540
|
-
get_result.setdefault("export_executed", True)
|
|
541
|
-
get_result.setdefault("safe_to_retry_export", False)
|
|
542
|
-
get_result.setdefault("export_handle", export_handle)
|
|
543
512
|
return get_result
|
|
544
513
|
return {
|
|
545
514
|
**get_result,
|
|
546
|
-
"export_executed": True,
|
|
547
|
-
"safe_to_retry_export": False,
|
|
548
515
|
"export_handle": export_handle,
|
|
549
516
|
"file_urls": snapshot.get("file_urls") or [],
|
|
550
517
|
"file_names": snapshot.get("file_names") or [],
|
|
@@ -580,11 +547,8 @@ class ExportTools(ToolBase):
|
|
|
580
547
|
}
|
|
581
548
|
if "EXPORT_HISTORY_AMBIGUOUS" in warning_codes:
|
|
582
549
|
return {
|
|
583
|
-
"ok":
|
|
550
|
+
"ok": True,
|
|
584
551
|
"status": "unknown",
|
|
585
|
-
"error_code": "EXPORT_STATUS_UNKNOWN",
|
|
586
|
-
"export_executed": True,
|
|
587
|
-
"safe_to_retry_export": False,
|
|
588
552
|
"export_handle": export_handle,
|
|
589
553
|
"app_key": str(local_job.get("app_key") or ""),
|
|
590
554
|
"view_id": str(local_job.get("view_id") or ""),
|
|
@@ -615,11 +579,8 @@ class ExportTools(ToolBase):
|
|
|
615
579
|
}
|
|
616
580
|
)
|
|
617
581
|
return {
|
|
618
|
-
"ok":
|
|
582
|
+
"ok": True,
|
|
619
583
|
"status": timeout_status,
|
|
620
|
-
"error_code": "EXPORT_WAIT_TIMEOUT",
|
|
621
|
-
"export_executed": True,
|
|
622
|
-
"safe_to_retry_export": False,
|
|
623
584
|
"export_handle": str(start_result.get("export_handle") or ""),
|
|
624
585
|
"app_key": normalized_app_key,
|
|
625
586
|
"view_id": str(start_result.get("view_id") or normalized_view_id),
|
|
@@ -743,7 +704,7 @@ class ExportTools(ToolBase):
|
|
|
743
704
|
order_by: list[JSONObject],
|
|
744
705
|
select_columns: list[JSONObject],
|
|
745
706
|
) -> list[int]:
|
|
746
|
-
browse_scope = self.
|
|
707
|
+
browse_scope = self._record_tools._build_browse_write_scope(
|
|
747
708
|
profile,
|
|
748
709
|
context,
|
|
749
710
|
app_key,
|
|
@@ -880,7 +841,7 @@ class ExportTools(ToolBase):
|
|
|
880
841
|
resolved_view: AccessibleViewRoute,
|
|
881
842
|
column_selectors: list[int],
|
|
882
843
|
) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
883
|
-
browse_scope = self.
|
|
844
|
+
browse_scope = self._record_tools._build_browse_write_scope(
|
|
884
845
|
profile,
|
|
885
846
|
context,
|
|
886
847
|
app_key,
|
|
@@ -951,71 +912,6 @@ class ExportTools(ToolBase):
|
|
|
951
912
|
)
|
|
952
913
|
return {"questionExportConfigList": question_export_config_list}, warnings
|
|
953
914
|
|
|
954
|
-
def _build_export_read_scope(
|
|
955
|
-
self,
|
|
956
|
-
profile: str,
|
|
957
|
-
context,
|
|
958
|
-
app_key: str,
|
|
959
|
-
resolved_view: AccessibleViewRoute,
|
|
960
|
-
*,
|
|
961
|
-
force_refresh: bool,
|
|
962
|
-
) -> JSONObject:
|
|
963
|
-
try:
|
|
964
|
-
scope = self._record_tools._build_browse_read_scope(
|
|
965
|
-
profile,
|
|
966
|
-
context,
|
|
967
|
-
app_key,
|
|
968
|
-
resolved_view,
|
|
969
|
-
force_refresh=force_refresh,
|
|
970
|
-
)
|
|
971
|
-
except QingflowApiError as exc:
|
|
972
|
-
if not _is_optional_export_lookup_error(exc):
|
|
973
|
-
raise
|
|
974
|
-
scope = {}
|
|
975
|
-
index = scope.get("index") if isinstance(scope, dict) else None
|
|
976
|
-
if getattr(index, "by_id", None):
|
|
977
|
-
return scope
|
|
978
|
-
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
979
|
-
try:
|
|
980
|
-
list_base_scope = self._build_system_export_list_base_info_scope(context, app_key)
|
|
981
|
-
except QingflowApiError as exc:
|
|
982
|
-
if not _is_optional_export_lookup_error(exc):
|
|
983
|
-
raise
|
|
984
|
-
else:
|
|
985
|
-
list_base_index = list_base_scope.get("index")
|
|
986
|
-
if getattr(list_base_index, "by_id", None):
|
|
987
|
-
return list_base_scope
|
|
988
|
-
try:
|
|
989
|
-
applicant_index = self._record_tools._get_applicant_top_level_field_index(
|
|
990
|
-
profile,
|
|
991
|
-
context,
|
|
992
|
-
app_key,
|
|
993
|
-
force_refresh=force_refresh,
|
|
994
|
-
)
|
|
995
|
-
except QingflowApiError as exc:
|
|
996
|
-
if not _is_optional_export_lookup_error(exc):
|
|
997
|
-
raise
|
|
998
|
-
applicant_index = None
|
|
999
|
-
if applicant_index is not None and applicant_index.by_id:
|
|
1000
|
-
visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
|
|
1001
|
-
return {
|
|
1002
|
-
"index": applicant_index,
|
|
1003
|
-
"writable_field_ids": set(),
|
|
1004
|
-
"visible_question_ids": visible_question_ids,
|
|
1005
|
-
}
|
|
1006
|
-
return scope or {"index": _build_top_level_field_index({}), "writable_field_ids": set(), "visible_question_ids": set()}
|
|
1007
|
-
|
|
1008
|
-
def _build_system_export_list_base_info_scope(self, context, app_key: str) -> JSONObject:
|
|
1009
|
-
payload = self.backend.request("GET", context, f"/app/{app_key}/data/listBaseInfo")
|
|
1010
|
-
schema = _normalize_data_list_base_info_schema(payload)
|
|
1011
|
-
index = _build_top_level_field_index(schema)
|
|
1012
|
-
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
1013
|
-
return {
|
|
1014
|
-
"index": index,
|
|
1015
|
-
"writable_field_ids": set(),
|
|
1016
|
-
"visible_question_ids": visible_question_ids,
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
915
|
def _resolve_exportable_fields(
|
|
1020
916
|
self,
|
|
1021
917
|
*,
|
|
@@ -1151,35 +1047,16 @@ class ExportTools(ToolBase):
|
|
|
1151
1047
|
) -> dict[str, Any]: # type: ignore[no-untyped-def]
|
|
1152
1048
|
app_key = str(local_job.get("app_key") or "").strip()
|
|
1153
1049
|
process_payload = self._lookup_process_details(context, app_key=app_key)
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
params={"appKey": app_key, "pageNum": 1, "pageSize": 100},
|
|
1161
|
-
)
|
|
1162
|
-
except QingflowApiError as exc:
|
|
1163
|
-
if not _is_optional_export_lookup_error(exc):
|
|
1164
|
-
raise
|
|
1165
|
-
history_page = {"list": []}
|
|
1166
|
-
history_unavailable_warning = {
|
|
1167
|
-
"code": "EXPORT_HISTORY_UNAVAILABLE",
|
|
1168
|
-
"message": "export history is not readable for the current user; using current process details when available.",
|
|
1169
|
-
}
|
|
1170
|
-
if exc.category:
|
|
1171
|
-
history_unavailable_warning["category"] = exc.category
|
|
1172
|
-
if exc.backend_code is not None:
|
|
1173
|
-
history_unavailable_warning["backend_code"] = exc.backend_code
|
|
1174
|
-
if exc.http_status is not None:
|
|
1175
|
-
history_unavailable_warning["http_status"] = exc.http_status
|
|
1176
|
-
if exc.request_id:
|
|
1177
|
-
history_unavailable_warning["request_id"] = exc.request_id
|
|
1050
|
+
history_page = self.backend.request(
|
|
1051
|
+
"GET",
|
|
1052
|
+
context,
|
|
1053
|
+
"/app/apply/dataExport/record",
|
|
1054
|
+
params={"appKey": app_key, "pageNum": 1, "pageSize": 100},
|
|
1055
|
+
)
|
|
1178
1056
|
history_records = _extract_export_records(history_page)
|
|
1179
1057
|
matched_record, matched_by = _match_export_history_record(history_records, local_job=local_job)
|
|
1180
1058
|
if process_payload is not None:
|
|
1181
1059
|
normalized_status = _normalize_export_status(process_payload.get("processStatus") or process_payload.get("status"))
|
|
1182
|
-
process_file_infos = _normalize_export_file_infos(process_payload.get("fileUrls"))
|
|
1183
1060
|
if normalized_status in {"queued", "running"}:
|
|
1184
1061
|
return {
|
|
1185
1062
|
"status": normalized_status,
|
|
@@ -1187,47 +1064,22 @@ class ExportTools(ToolBase):
|
|
|
1187
1064
|
"num": _coerce_int(process_payload.get("num")),
|
|
1188
1065
|
"error_code": process_payload.get("errorCode"),
|
|
1189
1066
|
"audit_record_status": process_payload.get("auditRecordStatus"),
|
|
1190
|
-
"file_infos":
|
|
1191
|
-
"file_urls":
|
|
1192
|
-
"file_names":
|
|
1193
|
-
"warnings": [
|
|
1067
|
+
"file_infos": _normalize_export_file_infos(process_payload.get("fileUrls")),
|
|
1068
|
+
"file_urls": _extract_export_file_urls(process_payload.get("fileUrls")),
|
|
1069
|
+
"file_names": _extract_export_file_names(process_payload.get("fileUrls")),
|
|
1070
|
+
"warnings": [],
|
|
1194
1071
|
"verification": {
|
|
1195
1072
|
"current_process_visible": True,
|
|
1196
1073
|
"history_match_resolved": matched_record is not None,
|
|
1197
|
-
"history_readable": history_unavailable_warning is None,
|
|
1198
1074
|
},
|
|
1199
1075
|
"message": None,
|
|
1200
1076
|
}
|
|
1201
|
-
if normalized_status in {"succeeded", "failed"} and (process_file_infos or normalized_status == "failed"):
|
|
1202
|
-
message = "export failed" if normalized_status == "failed" else None
|
|
1203
|
-
return {
|
|
1204
|
-
"status": normalized_status,
|
|
1205
|
-
"process_status": _coerce_int(process_payload.get("processStatus") or process_payload.get("status")),
|
|
1206
|
-
"num": _coerce_int(process_payload.get("num")),
|
|
1207
|
-
"error_code": process_payload.get("errorCode"),
|
|
1208
|
-
"audit_record_status": process_payload.get("auditRecordStatus"),
|
|
1209
|
-
"file_infos": process_file_infos,
|
|
1210
|
-
"file_urls": [item.get("url") for item in process_file_infos if isinstance(item.get("url"), str)],
|
|
1211
|
-
"file_names": [item.get("name") for item in process_file_infos if isinstance(item.get("name"), str)],
|
|
1212
|
-
"warnings": [history_unavailable_warning] if history_unavailable_warning is not None else [],
|
|
1213
|
-
"verification": {
|
|
1214
|
-
"current_process_visible": True,
|
|
1215
|
-
"history_match_resolved": matched_record is not None,
|
|
1216
|
-
"history_readable": history_unavailable_warning is None,
|
|
1217
|
-
"matched_by": "current_process",
|
|
1218
|
-
},
|
|
1219
|
-
"message": message,
|
|
1220
|
-
}
|
|
1221
1077
|
if matched_record is None:
|
|
1222
1078
|
warning_code = "EXPORT_HISTORY_PENDING"
|
|
1223
1079
|
warning_message = "export has not appeared in export history yet"
|
|
1224
1080
|
if matched_by == "ambiguous":
|
|
1225
1081
|
warning_code = "EXPORT_HISTORY_AMBIGUOUS"
|
|
1226
1082
|
warning_message = "export result could not be matched uniquely in export history"
|
|
1227
|
-
warnings = [{"code": warning_code, "message": warning_message}]
|
|
1228
|
-
if history_unavailable_warning is not None:
|
|
1229
|
-
warnings = [history_unavailable_warning]
|
|
1230
|
-
warning_message = str(history_unavailable_warning["message"])
|
|
1231
1083
|
return {
|
|
1232
1084
|
"status": "unknown",
|
|
1233
1085
|
"process_status": None,
|
|
@@ -1237,11 +1089,10 @@ class ExportTools(ToolBase):
|
|
|
1237
1089
|
"file_infos": [],
|
|
1238
1090
|
"file_urls": [],
|
|
1239
1091
|
"file_names": [],
|
|
1240
|
-
"warnings":
|
|
1092
|
+
"warnings": [{"code": warning_code, "message": warning_message}],
|
|
1241
1093
|
"verification": {
|
|
1242
1094
|
"current_process_visible": process_payload is not None,
|
|
1243
1095
|
"history_match_resolved": False,
|
|
1244
|
-
"history_readable": history_unavailable_warning is None,
|
|
1245
1096
|
},
|
|
1246
1097
|
"message": warning_message,
|
|
1247
1098
|
}
|
|
@@ -1278,7 +1129,7 @@ class ExportTools(ToolBase):
|
|
|
1278
1129
|
params={"taskType": 2},
|
|
1279
1130
|
)
|
|
1280
1131
|
except QingflowApiError as exc:
|
|
1281
|
-
if
|
|
1132
|
+
if exc.backend_code in {40002, 40027}:
|
|
1282
1133
|
return None
|
|
1283
1134
|
raise
|
|
1284
1135
|
if isinstance(payload, dict):
|
|
@@ -1332,9 +1183,9 @@ class ExportTools(ToolBase):
|
|
|
1332
1183
|
default_directory: str | None,
|
|
1333
1184
|
app_key: str,
|
|
1334
1185
|
view_id: str,
|
|
1335
|
-
) ->
|
|
1186
|
+
) -> list[JSONObject]:
|
|
1336
1187
|
if download_to_path is None and default_directory is None:
|
|
1337
|
-
return []
|
|
1188
|
+
return []
|
|
1338
1189
|
effective_hint = download_to_path or default_directory
|
|
1339
1190
|
assert effective_hint is not None
|
|
1340
1191
|
targets = _resolve_download_targets(
|
|
@@ -1344,45 +1195,13 @@ class ExportTools(ToolBase):
|
|
|
1344
1195
|
view_id=view_id,
|
|
1345
1196
|
)
|
|
1346
1197
|
downloaded_files: list[JSONObject] = []
|
|
1347
|
-
warnings: list[JSONObject] = []
|
|
1348
1198
|
for file_info, target in zip(file_infos, targets, strict=False):
|
|
1349
1199
|
url = str(file_info.get("url") or "").strip()
|
|
1350
1200
|
if not url:
|
|
1351
1201
|
continue
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
target.write_bytes(content)
|
|
1356
|
-
except QingflowApiError as exc:
|
|
1357
|
-
warning: JSONObject = {
|
|
1358
|
-
"code": "EXPORT_FILE_DOWNLOAD_UNAVAILABLE",
|
|
1359
|
-
"message": "export file link is available, but local download failed; use file_urls or retry download later.",
|
|
1360
|
-
"file_name": str(file_info.get("name") or target.name),
|
|
1361
|
-
"url": url,
|
|
1362
|
-
"category": exc.category,
|
|
1363
|
-
"backend_code": exc.backend_code,
|
|
1364
|
-
"request_id": exc.request_id,
|
|
1365
|
-
"http_status": exc.http_status,
|
|
1366
|
-
}
|
|
1367
|
-
if is_auth_like_error(exc):
|
|
1368
|
-
warning["auth_like"] = True
|
|
1369
|
-
warning["error_code"] = "AUTH_REQUIRED"
|
|
1370
|
-
if exc.details:
|
|
1371
|
-
warning["details"] = exc.details
|
|
1372
|
-
warnings.append(warning)
|
|
1373
|
-
continue
|
|
1374
|
-
except OSError as exc:
|
|
1375
|
-
warnings.append(
|
|
1376
|
-
{
|
|
1377
|
-
"code": "EXPORT_FILE_WRITE_UNAVAILABLE",
|
|
1378
|
-
"message": "export file link is available, but writing the local file failed; use file_urls or retry with another download_to_path.",
|
|
1379
|
-
"file_name": str(file_info.get("name") or target.name),
|
|
1380
|
-
"url": url,
|
|
1381
|
-
"path": str(target),
|
|
1382
|
-
"error": str(exc),
|
|
1383
|
-
}
|
|
1384
|
-
)
|
|
1385
|
-
continue
|
|
1202
|
+
content = self.backend.download_binary(url)
|
|
1203
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1204
|
+
target.write_bytes(content)
|
|
1386
1205
|
downloaded_files.append(
|
|
1387
1206
|
{
|
|
1388
1207
|
"file_name": str(file_info.get("name") or target.name),
|
|
@@ -1390,7 +1209,7 @@ class ExportTools(ToolBase):
|
|
|
1390
1209
|
"url": url,
|
|
1391
1210
|
}
|
|
1392
1211
|
)
|
|
1393
|
-
return downloaded_files
|
|
1212
|
+
return downloaded_files
|
|
1394
1213
|
|
|
1395
1214
|
def _normalize_timeout_seconds(self, wait_timeout_seconds: float | None) -> float:
|
|
1396
1215
|
if wait_timeout_seconds is None:
|
|
@@ -1440,16 +1259,10 @@ class ExportTools(ToolBase):
|
|
|
1440
1259
|
payload = json.loads(str(error))
|
|
1441
1260
|
except json.JSONDecodeError:
|
|
1442
1261
|
payload = {"message": str(error)}
|
|
1443
|
-
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
1444
1262
|
response = self._failed_export_result(
|
|
1445
|
-
error_code=
|
|
1263
|
+
error_code=((payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}).get("error_code") or error_code,
|
|
1446
1264
|
message=str(payload.get("message") or str(error)),
|
|
1447
1265
|
)
|
|
1448
|
-
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
1449
|
-
if key in payload:
|
|
1450
|
-
response[key] = payload.get(key)
|
|
1451
|
-
if details:
|
|
1452
|
-
response["details"] = details
|
|
1453
1266
|
if extra:
|
|
1454
1267
|
response.update(extra)
|
|
1455
1268
|
return response
|
|
@@ -1466,23 +1279,6 @@ def _extract_export_records(payload: Any) -> list[JSONObject]:
|
|
|
1466
1279
|
return []
|
|
1467
1280
|
|
|
1468
1281
|
|
|
1469
|
-
def _is_optional_export_lookup_error(error: QingflowApiError) -> bool:
|
|
1470
|
-
if is_auth_like_error(error):
|
|
1471
|
-
return False
|
|
1472
|
-
backend_code = backend_code_int(error)
|
|
1473
|
-
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
def _runtime_error_code(payload: JSONObject, *, default: str) -> str:
|
|
1477
|
-
category = str(payload.get("category") or "").strip().lower()
|
|
1478
|
-
http_status = _coerce_int(payload.get("http_status"))
|
|
1479
|
-
if category == "auth" or http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
|
|
1480
|
-
return "AUTH_REQUIRED"
|
|
1481
|
-
if category == "workspace":
|
|
1482
|
-
return "WORKSPACE_NOT_SELECTED"
|
|
1483
|
-
return default
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
1282
|
def _match_export_history_record(
|
|
1487
1283
|
records: list[JSONObject],
|
|
1488
1284
|
*,
|
|
@@ -1672,12 +1468,6 @@ def _normalize_export_record_ids(record_ids: list[str | int]) -> list[int]:
|
|
|
1672
1468
|
return normalized
|
|
1673
1469
|
|
|
1674
1470
|
|
|
1675
|
-
def _merge_single_export_record_id(record_id: str | int | None, record_ids: list[str | int]) -> list[str | int]:
|
|
1676
|
-
if record_id is None or str(record_id).strip() == "":
|
|
1677
|
-
return list(record_ids)
|
|
1678
|
-
return [record_id, *record_ids]
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
1471
|
def _effective_total(page: JSONObject, *, page_size: int) -> int:
|
|
1682
1472
|
rows = page.get("list")
|
|
1683
1473
|
returned_rows = len(rows) if isinstance(rows, list) else 0
|
|
@@ -146,15 +146,6 @@ class FeedbackTools:
|
|
|
146
146
|
"app_key": get_feedback_app_key(),
|
|
147
147
|
"mcp_side": self.mcp_side,
|
|
148
148
|
},
|
|
149
|
-
"submission_summary": {
|
|
150
|
-
"category": normalized_payload.get("反馈类型"),
|
|
151
|
-
"title": normalized_payload.get("title"),
|
|
152
|
-
"tool_name": normalized_payload.get("关联工具"),
|
|
153
|
-
"app_key": normalized_payload.get("关联应用"),
|
|
154
|
-
"record_id": normalized_payload.get("关联记录"),
|
|
155
|
-
"workflow_node_id": normalized_payload.get("关联节点"),
|
|
156
|
-
"impact_scope": normalized_payload.get("影响范围"),
|
|
157
|
-
},
|
|
158
149
|
"normalized_payload": normalized_payload,
|
|
159
150
|
"feedback_request_id": feedback_request_id,
|
|
160
151
|
}
|
|
@@ -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,
|
|
15
|
+
from ..errors import QingflowApiError, 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 = {40118}
|
|
20
|
+
ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES = {"40118", 40118}
|
|
21
21
|
LEGACY_OSS_FORM_REQUIRED_KEYS = ("key", "policy", "signature", "ossAccessKeyId")
|
|
22
22
|
|
|
23
23
|
|
|
@@ -190,8 +190,6 @@ 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,
|
|
195
193
|
"attachment_value": {
|
|
196
194
|
"value": download_url,
|
|
197
195
|
"otherInfo": file_name,
|
|
@@ -345,8 +343,6 @@ class FileTools(ToolBase):
|
|
|
345
343
|
}
|
|
346
344
|
if path_id is not None:
|
|
347
345
|
encrypted_payload["pathId"] = path_id
|
|
348
|
-
if file_related_url:
|
|
349
|
-
encrypted_payload["fileRelatedUrl"] = file_related_url
|
|
350
346
|
if bucket_type:
|
|
351
347
|
encrypted_payload["bucketType"] = bucket_type
|
|
352
348
|
result = self.backend.request("POST", context, endpoint, json_body=encrypted_payload)
|
|
@@ -361,12 +357,10 @@ class FileTools(ToolBase):
|
|
|
361
357
|
error: QingflowApiError,
|
|
362
358
|
) -> bool:
|
|
363
359
|
"""执行内部辅助逻辑。"""
|
|
364
|
-
if is_auth_like_error(error):
|
|
365
|
-
return False
|
|
366
360
|
return (
|
|
367
361
|
requested_kind == "attachment"
|
|
368
362
|
and attempted_kind == "attachment"
|
|
369
|
-
and
|
|
363
|
+
and error.backend_code in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
|
|
370
364
|
)
|
|
371
365
|
|
|
372
366
|
def _is_legacy_oss_form_upload(self, payload: JSONObject) -> bool:
|