@josephyan/qingflow-cli 1.1.4 → 1.1.5

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 (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -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,10 +61,11 @@ class ExportTools(ToolBase):
59
61
  def record_export_start(
60
62
  profile: str = DEFAULT_PROFILE,
61
63
  app_key: str = "",
62
- view_id: str = "system:all",
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,
68
+ record_id: str | int | None = None,
66
69
  record_ids: list[str | int] | None = None,
67
70
  include_workflow_log: bool = False,
68
71
  ) -> dict[str, Any]:
@@ -73,6 +76,7 @@ class ExportTools(ToolBase):
73
76
  columns=columns or [],
74
77
  where=where or [],
75
78
  order_by=order_by or [],
79
+ record_id=record_id,
76
80
  record_ids=record_ids or [],
77
81
  include_workflow_log=include_workflow_log,
78
82
  )
@@ -103,10 +107,11 @@ class ExportTools(ToolBase):
103
107
  def record_export_direct(
104
108
  profile: str = DEFAULT_PROFILE,
105
109
  app_key: str = "",
106
- view_id: str = "system:all",
110
+ view_id: str = "",
107
111
  columns: list[JSONObject | int] | None = None,
108
112
  where: list[JSONObject] | None = None,
109
113
  order_by: list[JSONObject] | None = None,
114
+ record_id: str | int | None = None,
110
115
  record_ids: list[str | int] | None = None,
111
116
  include_workflow_log: bool = False,
112
117
  download_to_path: str | None = None,
@@ -119,6 +124,7 @@ class ExportTools(ToolBase):
119
124
  columns=columns or [],
120
125
  where=where or [],
121
126
  order_by=order_by or [],
127
+ record_id=record_id,
122
128
  record_ids=record_ids or [],
123
129
  include_workflow_log=include_workflow_log,
124
130
  download_to_path=download_to_path,
@@ -131,25 +137,32 @@ class ExportTools(ToolBase):
131
137
  *,
132
138
  profile: str = DEFAULT_PROFILE,
133
139
  app_key: str,
134
- view_id: str = "system:all",
140
+ view_id: str = "",
135
141
  columns: list[JSONObject | int] | None = None,
136
142
  where: list[JSONObject] | None = None,
137
143
  order_by: list[JSONObject] | None = None,
144
+ record_id: str | int | None = None,
138
145
  record_ids: list[str | int] | None = None,
139
146
  include_workflow_log: bool = False,
140
147
  ) -> dict[str, Any]:
141
148
  normalized_app_key = str(app_key or "").strip()
142
- normalized_view_id = str(view_id or "").strip() or "system:all"
149
+ normalized_view_id = str(view_id or "").strip()
143
150
  normalized_columns = _normalize_export_columns(columns or [])
144
151
  normalized_where = self._record_tools._normalize_record_list_where(where or [])
145
152
  normalized_order_by = self._record_tools._normalize_record_list_order_by(order_by or [])
146
- normalized_record_ids = _normalize_export_record_ids(record_ids or [])
153
+ normalized_record_ids = _normalize_export_record_ids(_merge_single_export_record_id(record_id, record_ids or []))
147
154
  if not normalized_app_key:
148
155
  return self._failed_export_result(
149
156
  error_code="EXPORT_START_FAILED",
150
157
  message="app_key is required",
151
158
  extra={"view_id": normalized_view_id, "status": "failed"},
152
159
  )
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
+ )
153
166
 
154
167
  def runner(session_profile, context):
155
168
  resolved_view, compatibility_warnings = self._record_tools._resolve_accessible_view_route(
@@ -160,7 +173,7 @@ class ExportTools(ToolBase):
160
173
  list_type=None,
161
174
  view_key=None,
162
175
  view_name=None,
163
- allow_default=True,
176
+ allow_default=False,
164
177
  )
165
178
  export_config, export_config_warnings = self._build_export_config(
166
179
  profile=profile,
@@ -238,6 +251,8 @@ class ExportTools(ToolBase):
238
251
  return {
239
252
  "ok": True,
240
253
  "status": "accepted",
254
+ "export_executed": True,
255
+ "safe_to_retry_export": False,
241
256
  "app_key": normalized_app_key,
242
257
  "view_id": resolved_view.view_id,
243
258
  "export_handle": export_handle,
@@ -404,13 +419,17 @@ class ExportTools(ToolBase):
404
419
  "verification": snapshot.get("verification") or {},
405
420
  },
406
421
  )
407
- downloaded_files = self._download_export_files(
422
+ downloaded_files, download_warnings = self._download_export_files(
408
423
  file_infos=file_infos,
409
424
  download_to_path=download_to_path,
410
425
  default_directory=None,
411
426
  app_key=str(local_job.get("app_key") or ""),
412
427
  view_id=str(local_job.get("view_id") or ""),
413
428
  )
429
+ warnings = [
430
+ *cast(list[JSONObject], snapshot.get("warnings") or []),
431
+ *download_warnings,
432
+ ]
414
433
  return {
415
434
  "ok": True,
416
435
  "status": "succeeded",
@@ -429,7 +448,7 @@ class ExportTools(ToolBase):
429
448
  "file_urls": snapshot.get("file_urls") or [],
430
449
  "file_names": snapshot.get("file_names") or [],
431
450
  "downloaded_files": downloaded_files,
432
- "warnings": snapshot.get("warnings") or [],
451
+ "warnings": warnings,
433
452
  "verification": snapshot.get("verification") or {},
434
453
  "request_route": self.backend.describe_route(lookup_context),
435
454
  }
@@ -449,23 +468,30 @@ class ExportTools(ToolBase):
449
468
  *,
450
469
  profile: str = DEFAULT_PROFILE,
451
470
  app_key: str,
452
- view_id: str = "system:all",
471
+ view_id: str = "",
453
472
  columns: list[JSONObject | int] | None = None,
454
473
  where: list[JSONObject] | None = None,
455
474
  order_by: list[JSONObject] | None = None,
475
+ record_id: str | int | None = None,
456
476
  record_ids: list[str | int] | None = None,
457
477
  include_workflow_log: bool = False,
458
478
  download_to_path: str | None = None,
459
479
  wait_timeout_seconds: float | None = None,
460
480
  ) -> dict[str, Any]:
461
481
  normalized_app_key = str(app_key or "").strip()
462
- normalized_view_id = str(view_id or "").strip() or "system:all"
482
+ normalized_view_id = str(view_id or "").strip()
463
483
  if not normalized_app_key:
464
484
  return self._failed_export_result(
465
485
  error_code="EXPORT_START_FAILED",
466
486
  message="app_key is required",
467
487
  extra={"status": "failed", "view_id": normalized_view_id},
468
488
  )
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
+ )
469
495
  timeout_seconds = self._normalize_timeout_seconds(wait_timeout_seconds)
470
496
 
471
497
  def runner(session_profile, context):
@@ -476,6 +502,7 @@ class ExportTools(ToolBase):
476
502
  columns=columns or [],
477
503
  where=where or [],
478
504
  order_by=order_by or [],
505
+ record_id=record_id,
479
506
  record_ids=record_ids or [],
480
507
  include_workflow_log=include_workflow_log,
481
508
  )
@@ -509,9 +536,15 @@ class ExportTools(ToolBase):
509
536
  download_to_path=effective_download_path,
510
537
  )
511
538
  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)
512
543
  return get_result
513
544
  return {
514
545
  **get_result,
546
+ "export_executed": True,
547
+ "safe_to_retry_export": False,
515
548
  "export_handle": export_handle,
516
549
  "file_urls": snapshot.get("file_urls") or [],
517
550
  "file_names": snapshot.get("file_names") or [],
@@ -547,8 +580,11 @@ class ExportTools(ToolBase):
547
580
  }
548
581
  if "EXPORT_HISTORY_AMBIGUOUS" in warning_codes:
549
582
  return {
550
- "ok": True,
583
+ "ok": False,
551
584
  "status": "unknown",
585
+ "error_code": "EXPORT_STATUS_UNKNOWN",
586
+ "export_executed": True,
587
+ "safe_to_retry_export": False,
552
588
  "export_handle": export_handle,
553
589
  "app_key": str(local_job.get("app_key") or ""),
554
590
  "view_id": str(local_job.get("view_id") or ""),
@@ -579,8 +615,11 @@ class ExportTools(ToolBase):
579
615
  }
580
616
  )
581
617
  return {
582
- "ok": True,
618
+ "ok": False,
583
619
  "status": timeout_status,
620
+ "error_code": "EXPORT_WAIT_TIMEOUT",
621
+ "export_executed": True,
622
+ "safe_to_retry_export": False,
584
623
  "export_handle": str(start_result.get("export_handle") or ""),
585
624
  "app_key": normalized_app_key,
586
625
  "view_id": str(start_result.get("view_id") or normalized_view_id),
@@ -704,7 +743,7 @@ class ExportTools(ToolBase):
704
743
  order_by: list[JSONObject],
705
744
  select_columns: list[JSONObject],
706
745
  ) -> list[int]:
707
- browse_scope = self._record_tools._build_browse_write_scope(
746
+ browse_scope = self._build_export_read_scope(
708
747
  profile,
709
748
  context,
710
749
  app_key,
@@ -841,7 +880,7 @@ class ExportTools(ToolBase):
841
880
  resolved_view: AccessibleViewRoute,
842
881
  column_selectors: list[int],
843
882
  ) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
844
- browse_scope = self._record_tools._build_browse_write_scope(
883
+ browse_scope = self._build_export_read_scope(
845
884
  profile,
846
885
  context,
847
886
  app_key,
@@ -912,6 +951,71 @@ class ExportTools(ToolBase):
912
951
  )
913
952
  return {"questionExportConfigList": question_export_config_list}, warnings
914
953
 
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
+
915
1019
  def _resolve_exportable_fields(
916
1020
  self,
917
1021
  *,
@@ -1047,16 +1151,35 @@ class ExportTools(ToolBase):
1047
1151
  ) -> dict[str, Any]: # type: ignore[no-untyped-def]
1048
1152
  app_key = str(local_job.get("app_key") or "").strip()
1049
1153
  process_payload = self._lookup_process_details(context, app_key=app_key)
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
- )
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
1056
1178
  history_records = _extract_export_records(history_page)
1057
1179
  matched_record, matched_by = _match_export_history_record(history_records, local_job=local_job)
1058
1180
  if process_payload is not None:
1059
1181
  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"))
1060
1183
  if normalized_status in {"queued", "running"}:
1061
1184
  return {
1062
1185
  "status": normalized_status,
@@ -1064,22 +1187,47 @@ class ExportTools(ToolBase):
1064
1187
  "num": _coerce_int(process_payload.get("num")),
1065
1188
  "error_code": process_payload.get("errorCode"),
1066
1189
  "audit_record_status": process_payload.get("auditRecordStatus"),
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": [],
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 [],
1071
1194
  "verification": {
1072
1195
  "current_process_visible": True,
1073
1196
  "history_match_resolved": matched_record is not None,
1197
+ "history_readable": history_unavailable_warning is None,
1074
1198
  },
1075
1199
  "message": None,
1076
1200
  }
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
+ }
1077
1221
  if matched_record is None:
1078
1222
  warning_code = "EXPORT_HISTORY_PENDING"
1079
1223
  warning_message = "export has not appeared in export history yet"
1080
1224
  if matched_by == "ambiguous":
1081
1225
  warning_code = "EXPORT_HISTORY_AMBIGUOUS"
1082
1226
  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"])
1083
1231
  return {
1084
1232
  "status": "unknown",
1085
1233
  "process_status": None,
@@ -1089,10 +1237,11 @@ class ExportTools(ToolBase):
1089
1237
  "file_infos": [],
1090
1238
  "file_urls": [],
1091
1239
  "file_names": [],
1092
- "warnings": [{"code": warning_code, "message": warning_message}],
1240
+ "warnings": warnings,
1093
1241
  "verification": {
1094
1242
  "current_process_visible": process_payload is not None,
1095
1243
  "history_match_resolved": False,
1244
+ "history_readable": history_unavailable_warning is None,
1096
1245
  },
1097
1246
  "message": warning_message,
1098
1247
  }
@@ -1129,7 +1278,7 @@ class ExportTools(ToolBase):
1129
1278
  params={"taskType": 2},
1130
1279
  )
1131
1280
  except QingflowApiError as exc:
1132
- if exc.backend_code in {40002, 40027}:
1281
+ if _is_optional_export_lookup_error(exc):
1133
1282
  return None
1134
1283
  raise
1135
1284
  if isinstance(payload, dict):
@@ -1183,9 +1332,9 @@ class ExportTools(ToolBase):
1183
1332
  default_directory: str | None,
1184
1333
  app_key: str,
1185
1334
  view_id: str,
1186
- ) -> list[JSONObject]:
1335
+ ) -> tuple[list[JSONObject], list[JSONObject]]:
1187
1336
  if download_to_path is None and default_directory is None:
1188
- return []
1337
+ return [], []
1189
1338
  effective_hint = download_to_path or default_directory
1190
1339
  assert effective_hint is not None
1191
1340
  targets = _resolve_download_targets(
@@ -1195,13 +1344,45 @@ class ExportTools(ToolBase):
1195
1344
  view_id=view_id,
1196
1345
  )
1197
1346
  downloaded_files: list[JSONObject] = []
1347
+ warnings: list[JSONObject] = []
1198
1348
  for file_info, target in zip(file_infos, targets, strict=False):
1199
1349
  url = str(file_info.get("url") or "").strip()
1200
1350
  if not url:
1201
1351
  continue
1202
- content = self.backend.download_binary(url)
1203
- target.parent.mkdir(parents=True, exist_ok=True)
1204
- target.write_bytes(content)
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
1205
1386
  downloaded_files.append(
1206
1387
  {
1207
1388
  "file_name": str(file_info.get("name") or target.name),
@@ -1209,7 +1390,7 @@ class ExportTools(ToolBase):
1209
1390
  "url": url,
1210
1391
  }
1211
1392
  )
1212
- return downloaded_files
1393
+ return downloaded_files, warnings
1213
1394
 
1214
1395
  def _normalize_timeout_seconds(self, wait_timeout_seconds: float | None) -> float:
1215
1396
  if wait_timeout_seconds is None:
@@ -1259,10 +1440,16 @@ class ExportTools(ToolBase):
1259
1440
  payload = json.loads(str(error))
1260
1441
  except json.JSONDecodeError:
1261
1442
  payload = {"message": str(error)}
1443
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
1262
1444
  response = self._failed_export_result(
1263
- error_code=((payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}).get("error_code") or error_code,
1445
+ error_code=details.get("error_code") or _runtime_error_code(payload, default=error_code),
1264
1446
  message=str(payload.get("message") or str(error)),
1265
1447
  )
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
1266
1453
  if extra:
1267
1454
  response.update(extra)
1268
1455
  return response
@@ -1279,6 +1466,23 @@ def _extract_export_records(payload: Any) -> list[JSONObject]:
1279
1466
  return []
1280
1467
 
1281
1468
 
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
+
1282
1486
  def _match_export_history_record(
1283
1487
  records: list[JSONObject],
1284
1488
  *,
@@ -1468,6 +1672,12 @@ def _normalize_export_record_ids(record_ids: list[str | int]) -> list[int]:
1468
1672
  return normalized
1469
1673
 
1470
1674
 
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
+
1471
1681
  def _effective_total(page: JSONObject, *, page_size: int) -> int:
1472
1682
  rows = page.get("list")
1473
1683
  returned_rows = len(rows) if isinstance(rows, list) else 0
@@ -146,6 +146,15 @@ 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
+ },
149
158
  "normalized_payload": normalized_payload,
150
159
  "feedback_request_id": feedback_request_id,
151
160
  }
@@ -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 = {"40118", 40118}
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,
@@ -343,6 +345,8 @@ class FileTools(ToolBase):
343
345
  }
344
346
  if path_id is not None:
345
347
  encrypted_payload["pathId"] = path_id
348
+ if file_related_url:
349
+ encrypted_payload["fileRelatedUrl"] = file_related_url
346
350
  if bucket_type:
347
351
  encrypted_payload["bucketType"] = bucket_type
348
352
  result = self.backend.request("POST", context, endpoint, json_body=encrypted_payload)
@@ -357,10 +361,12 @@ class FileTools(ToolBase):
357
361
  error: QingflowApiError,
358
362
  ) -> bool:
359
363
  """执行内部辅助逻辑。"""
364
+ if is_auth_like_error(error):
365
+ return False
360
366
  return (
361
367
  requested_kind == "attachment"
362
368
  and attempted_kind == "attachment"
363
- and error.backend_code in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
369
+ and backend_code_int(error) in ATTACHMENT_UPLOAD_INFO_FALLBACK_CODES
364
370
  )
365
371
 
366
372
  def _is_legacy_oss_form_upload(self, payload: JSONObject) -> bool: