@josephyan/qingflow-app-builder-mcp 1.1.25 → 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.
Files changed (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. 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, backend_code_int, is_auth_like_error, message_looks_like_invalid_token
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(_merge_single_export_record_id(record_id, record_ids or []))
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=False,
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, download_warnings = self._download_export_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": False,
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": False,
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._build_export_read_scope(
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._build_export_read_scope(
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
- history_unavailable_warning: JSONObject | None = None
1155
- try:
1156
- history_page = self.backend.request(
1157
- "GET",
1158
- context,
1159
- "/app/apply/dataExport/record",
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": process_file_infos,
1191
- "file_urls": [item.get("url") for item in process_file_infos if isinstance(item.get("url"), str)],
1192
- "file_names": [item.get("name") for item in process_file_infos if isinstance(item.get("name"), str)],
1193
- "warnings": [history_unavailable_warning] if history_unavailable_warning is not None else [],
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": 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 _is_optional_export_lookup_error(exc):
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
- ) -> tuple[list[JSONObject], list[JSONObject]]:
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
- try:
1353
- content = self.backend.download_binary(url)
1354
- target.parent.mkdir(parents=True, exist_ok=True)
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, warnings
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=details.get("error_code") or _runtime_error_code(payload, default=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, backend_code_int, is_auth_like_error, raise_tool_error
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 backend_code_int(error) in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
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: