@josephyan/qingflow-cli 1.1.4 → 1.1.6

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 +4192 -935
  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
@@ -3,13 +3,13 @@ from __future__ import annotations
3
3
  from copy import deepcopy
4
4
  from typing import Any
5
5
 
6
- from ..errors import QingflowApiError, raise_tool_error
6
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
9
9
  from ..solution.compiler.icon_utils import workspace_icon_config
10
10
  from .app_tools import _analysis_supported_for_view_type
11
11
  from .base import ToolBase, tool_cn_name
12
- from .qingbi_report_tools import QingbiReportTools
12
+ from .qingbi_report_tools import QingbiReportTools, _coerce_tool_error
13
13
 
14
14
 
15
15
  class ResourceReadTools(ToolBase):
@@ -141,21 +141,60 @@ class ResourceReadTools(ToolBase):
141
141
  warnings: list[JSONObject] = []
142
142
  verification = {
143
143
  "view_exists": True,
144
+ "config_verified": True,
145
+ "base_info_verified": True,
144
146
  "questions_verified": True,
145
147
  }
146
- config = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
147
- base_info = self.backend.request("GET", context, f"/view/{view_key}/viewConfig/baseInfo")
148
+ try:
149
+ config = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
150
+ if not isinstance(config, dict):
151
+ config = {}
152
+ except QingflowApiError as error:
153
+ if not _is_optional_view_config_error(error):
154
+ raise
155
+ config = {}
156
+ verification["config_verified"] = False
157
+ warnings.append(
158
+ {
159
+ "code": "VIEW_CONFIG_UNAVAILABLE",
160
+ "message": "view_get used baseInfo because viewConfig is unavailable for this user.",
161
+ "backend_code": error.backend_code,
162
+ "http_status": error.http_status,
163
+ "request_id": error.request_id,
164
+ }
165
+ )
166
+ try:
167
+ base_info = self.backend.request("GET", context, f"/view/{view_key}/viewConfig/baseInfo")
168
+ except QingflowApiError as error:
169
+ if not _is_optional_view_base_error(error):
170
+ raise
171
+ base_info = {}
172
+ verification["base_info_verified"] = False
173
+ warnings.append(
174
+ {
175
+ "code": "VIEW_BASE_INFO_UNAVAILABLE",
176
+ "message": "view_get used viewConfig because view baseInfo is unavailable for this user.",
177
+ "backend_code": error.backend_code,
178
+ "http_status": error.http_status,
179
+ "request_id": error.request_id,
180
+ }
181
+ )
148
182
  questions: list[dict[str, Any]] = []
149
183
  try:
150
184
  questions_payload = self.backend.request("GET", context, f"/view/{view_key}/question")
151
185
  if isinstance(questions_payload, list):
152
186
  questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
153
- except QingflowApiError:
187
+ except QingflowApiError as error:
188
+ if not _is_optional_view_question_error(error):
189
+ raise
154
190
  verification["questions_verified"] = False
155
191
  warnings.append(
156
192
  {
157
193
  "code": "VIEW_QUESTIONS_UNAVAILABLE",
158
194
  "message": "view_get could not load visible columns because question readback is unavailable.",
195
+ "backend_code": error.backend_code,
196
+ "http_status": error.http_status,
197
+ "request_id": error.request_id,
159
198
  }
160
199
  )
161
200
 
@@ -223,13 +262,32 @@ class ResourceReadTools(ToolBase):
223
262
  self._require_chart_id(chart_id)
224
263
 
225
264
  def runner(session_profile, _context):
226
- base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
227
265
  warnings: list[JSONObject] = []
228
266
  verification = {
229
267
  "chart_exists": True,
268
+ "chart_base_loaded": False,
230
269
  "chart_data_loaded": False,
231
270
  "chart_config_loaded": False,
232
271
  }
272
+ transport_errors: list[JSONObject] = []
273
+ base: dict[str, Any] = {}
274
+ try:
275
+ base_result = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
276
+ if isinstance(base_result, dict):
277
+ base = deepcopy(base_result)
278
+ verification["chart_base_loaded"] = True
279
+ except (QingflowApiError, RuntimeError) as error:
280
+ api_error = _coerce_tool_error(error)
281
+ if api_error is None or not _is_optional_chart_base_error(api_error):
282
+ raise
283
+ transport_errors.append(_transport_error_payload(stage="base_info", error=api_error))
284
+ warnings.append(
285
+ {
286
+ "code": "CHART_BASE_INFO_UNAVAILABLE",
287
+ "message": "chart_get could not load chart base info; continuing with chart data when available.",
288
+ **_transport_warning_fields(api_error),
289
+ }
290
+ )
233
291
  data: Any = None
234
292
  data_config: dict[str, Any] = {}
235
293
  try:
@@ -239,23 +297,54 @@ class ResourceReadTools(ToolBase):
239
297
  data_config = deepcopy(data.get("config"))
240
298
  verification["chart_config_loaded"] = True
241
299
  except (QingflowApiError, RuntimeError) as error:
242
- api_error = error if isinstance(error, QingflowApiError) else None
300
+ api_error = _coerce_tool_error(error)
301
+ if api_error is None or not _is_optional_chart_data_error(api_error):
302
+ raise
303
+ transport_errors.append(_transport_error_payload(stage="data", error=api_error))
243
304
  warnings.append(
244
305
  {
245
306
  "code": "CHART_DATA_UNAVAILABLE",
246
307
  "message": "chart_get could not load chart data; returning chart metadata and config when available.",
247
- "backend_code": api_error.backend_code if api_error is not None else None,
248
- "http_status": api_error.http_status if api_error is not None else None,
308
+ **_transport_warning_fields(api_error),
249
309
  }
250
310
  )
251
311
  if not data_config:
252
312
  try:
253
313
  config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
254
- except (QingflowApiError, RuntimeError):
314
+ except (QingflowApiError, RuntimeError) as error:
315
+ api_error = _coerce_tool_error(error)
316
+ if api_error is None or not _is_optional_chart_config_error(api_error):
317
+ raise
318
+ transport_errors.append(_transport_error_payload(stage="config", error=api_error))
319
+ warnings.append(
320
+ {
321
+ "code": "CHART_CONFIG_UNAVAILABLE",
322
+ "message": "chart_get could not load chart config; returning chart base info or data when available.",
323
+ **_transport_warning_fields(api_error),
324
+ }
325
+ )
255
326
  config_result = {}
256
327
  if isinstance(config_result, dict) and config_result:
257
328
  data_config = deepcopy(config_result)
258
329
  verification["chart_config_loaded"] = True
330
+ if not any(
331
+ bool(verification[key])
332
+ for key in ("chart_base_loaded", "chart_data_loaded", "chart_config_loaded")
333
+ ):
334
+ return {
335
+ "profile": profile,
336
+ "ws_id": session_profile.selected_ws_id,
337
+ "ok": False,
338
+ "status": "failed",
339
+ "error_code": "CHART_READ_UNAVAILABLE",
340
+ "message": "chart_get could not load chart base info, data, or config in this permission context.",
341
+ "warnings": warnings,
342
+ "verification": verification,
343
+ "details": {
344
+ "chart_id": chart_id,
345
+ "transport_errors": transport_errors,
346
+ },
347
+ }
259
348
  data_payload: dict[str, Any] = {
260
349
  "chart_id": chart_id,
261
350
  "chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
@@ -296,7 +385,9 @@ class ResourceReadTools(ToolBase):
296
385
  def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
297
386
  try:
298
387
  form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
299
- except QingflowApiError:
388
+ except QingflowApiError as exc:
389
+ if not _is_optional_view_metadata_resolution_error(exc):
390
+ raise
300
391
  return None
301
392
  if not isinstance(form_payload, dict):
302
393
  return None
@@ -309,38 +400,13 @@ class ResourceReadTools(ToolBase):
309
400
  return None
310
401
  try:
311
402
  payload = self.backend.request("GET", context, "/tag/apps")
312
- except QingflowApiError:
403
+ except QingflowApiError as exc:
404
+ if not _is_optional_view_metadata_resolution_error(exc):
405
+ raise
313
406
  payload = None
314
407
  app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
315
408
  if app_key:
316
409
  return app_key
317
- page_num = 1
318
- page_size = 200
319
- while page_num <= 20:
320
- try:
321
- page = self.backend.request(
322
- "GET",
323
- context,
324
- "/app/item",
325
- params={"pageNum": page_num, "pageSize": page_size},
326
- )
327
- except QingflowApiError:
328
- return None
329
- items = page.get("list") if isinstance(page, dict) else []
330
- if not isinstance(items, list) or not items:
331
- return None
332
- for item in items:
333
- if not isinstance(item, dict):
334
- continue
335
- if _coerce_positive_int(item.get("formId") or item.get("form_id")) != form_id:
336
- continue
337
- app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
338
- if app_key:
339
- return app_key
340
- total = _coerce_positive_int(page.get("total")) if isinstance(page, dict) else None
341
- if total is not None and page_num * page_size >= total:
342
- break
343
- page_num += 1
344
410
  return None
345
411
 
346
412
 
@@ -423,6 +489,89 @@ def _normalize_user_portal_components(components: Any) -> list[dict[str, Any]]:
423
489
  return items
424
490
 
425
491
 
492
+ def _is_optional_chart_base_error(error: QingflowApiError) -> bool:
493
+ if is_auth_like_error(error):
494
+ return False
495
+ backend_code = _coerce_backend_code(error)
496
+ return backend_code in {40002, 40027, 404, 81007} or error.http_status == 404
497
+
498
+
499
+ def _is_optional_chart_data_error(error: QingflowApiError) -> bool:
500
+ if is_auth_like_error(error):
501
+ return False
502
+ backend_code = _coerce_backend_code(error)
503
+ return backend_code in {40002, 40027, 404, 44011, 81007, 81011} or error.http_status == 404
504
+
505
+
506
+ def _is_optional_chart_config_error(error: QingflowApiError) -> bool:
507
+ if is_auth_like_error(error):
508
+ return False
509
+ backend_code = _coerce_backend_code(error)
510
+ return backend_code in {40002, 40027, 404, 44011, 81007, 81011} or error.http_status == 404
511
+
512
+
513
+ def _is_optional_view_base_error(error: QingflowApiError) -> bool:
514
+ if is_auth_like_error(error):
515
+ return False
516
+ backend_code = _coerce_backend_code(error)
517
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
518
+
519
+
520
+ def _is_optional_view_config_error(error: QingflowApiError) -> bool:
521
+ if is_auth_like_error(error):
522
+ return False
523
+ backend_code = _coerce_backend_code(error)
524
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
525
+
526
+
527
+ def _is_optional_view_question_error(error: QingflowApiError) -> bool:
528
+ if is_auth_like_error(error):
529
+ return False
530
+ backend_code = _coerce_backend_code(error)
531
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
532
+
533
+
534
+ def _is_optional_view_metadata_resolution_error(error: QingflowApiError) -> bool:
535
+ if is_auth_like_error(error):
536
+ return False
537
+ backend_code = _coerce_backend_code(error)
538
+ return backend_code in {40002, 40027, 404} or error.http_status == 404
539
+
540
+
541
+ def _coerce_backend_code(error: QingflowApiError) -> int | None:
542
+ return backend_code_int(error)
543
+
544
+
545
+ def _transport_error_payload(*, stage: str, error: QingflowApiError) -> JSONObject:
546
+ payload: JSONObject = {
547
+ "stage": stage,
548
+ "category": error.category,
549
+ "message": error.message,
550
+ }
551
+ if error.backend_code is not None:
552
+ payload["backend_code"] = error.backend_code
553
+ if error.http_status is not None:
554
+ payload["http_status"] = error.http_status
555
+ if error.request_id:
556
+ payload["request_id"] = error.request_id
557
+ if error.details:
558
+ payload["details"] = deepcopy(error.details)
559
+ return payload
560
+
561
+
562
+ def _transport_warning_fields(error: QingflowApiError) -> JSONObject:
563
+ payload: JSONObject = {}
564
+ if error.backend_code is not None:
565
+ payload["backend_code"] = error.backend_code
566
+ if error.http_status is not None:
567
+ payload["http_status"] = error.http_status
568
+ if error.request_id:
569
+ payload["request_id"] = error.request_id
570
+ if error.details:
571
+ payload["details"] = deepcopy(error.details)
572
+ return payload
573
+
574
+
426
575
  def _normalize_portal_component_source_type(value: Any) -> str:
427
576
  raw = str(value or "").strip()
428
577
  mapping = {
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import Any
4
+
3
5
  from mcp.server.fastmcp import FastMCP
4
6
 
5
7
  from ..config import DEFAULT_PROFILE
6
- from ..errors import QingflowApiError, raise_tool_error
8
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
9
  from ..json_types import JSONObject
8
10
  from .base import ToolBase, tool_cn_name
9
11
 
@@ -20,7 +22,7 @@ class RoleTools(ToolBase):
20
22
 
21
23
  def register(self, mcp: FastMCP) -> None:
22
24
  """注册当前工具到 MCP 服务。"""
23
- @mcp.tool()
25
+ @mcp.tool(description="Search workspace roles for builder permission and workflow configuration. Requires contact/role management permission and a non-empty keyword; do not use for record member/department field candidates.")
24
26
  def role_search(
25
27
  profile: str = DEFAULT_PROFILE,
26
28
  keyword: str = "",
@@ -50,18 +52,36 @@ class RoleTools(ToolBase):
50
52
  """执行角色相关逻辑。"""
51
53
  if page_num <= 0 or page_size <= 0:
52
54
  raise_tool_error(QingflowApiError.config_error("page_num and page_size must be positive"))
55
+ normalized_keyword = keyword.strip()
56
+ if not normalized_keyword:
57
+ raise_tool_error(
58
+ QingflowApiError.config_error(
59
+ "keyword is required for role_search; role lookup is a contact-management path, not a record candidate fallback"
60
+ )
61
+ )
53
62
 
54
63
  def runner(session_profile, context):
55
- result = self.backend.request(
56
- "GET",
57
- context,
58
- "/contact/roleByPage",
59
- params={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
60
- )
64
+ try:
65
+ result = self.backend.request(
66
+ "GET",
67
+ context,
68
+ "/contact/roleByPage",
69
+ params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
70
+ )
71
+ except QingflowApiError as exc:
72
+ if _is_role_permission_denied(exc):
73
+ return _contact_role_permission_denied_payload(
74
+ profile=profile,
75
+ ws_id=session_profile.selected_ws_id,
76
+ error=exc,
77
+ operation="role_search",
78
+ selection={"keyword": normalized_keyword, "page_num": page_num, "page_size": page_size},
79
+ )
80
+ raise
61
81
  return {
62
82
  "profile": profile,
63
83
  "ws_id": session_profile.selected_ws_id,
64
- "keyword": keyword,
84
+ "keyword": normalized_keyword,
65
85
  "page": result,
66
86
  }
67
87
 
@@ -110,3 +130,54 @@ class RoleTools(ToolBase):
110
130
  )
111
131
 
112
132
  return self._run(profile, runner)
133
+
134
+
135
+ def _is_role_permission_denied(error: QingflowApiError) -> bool:
136
+ if is_auth_like_error(error):
137
+ return False
138
+ return backend_code_int(error) in {40002, 40027}
139
+
140
+
141
+ def _contact_role_permission_denied_payload(
142
+ *,
143
+ profile: str,
144
+ ws_id: Any,
145
+ error: QingflowApiError,
146
+ operation: str,
147
+ selection: JSONObject,
148
+ ) -> JSONObject:
149
+ return {
150
+ "profile": profile,
151
+ "ws_id": ws_id,
152
+ "ok": False,
153
+ "status": "failed",
154
+ "error_code": "CONTACT_ROLE_PERMISSION_DENIED",
155
+ "message": (
156
+ "Contact role-management data is not readable for the current user. "
157
+ "This is a role/contact-management permission boundary, not proof that record "
158
+ "member or department field candidates are unavailable."
159
+ ),
160
+ "backend_code": error.backend_code,
161
+ "request_id": error.request_id,
162
+ "http_status": error.http_status,
163
+ "keyword": selection.get("keyword"),
164
+ "page": {"list": [], "total": 0, "pageAmount": 0},
165
+ "data": {
166
+ "items": [],
167
+ "pagination": {
168
+ "page": selection.get("page_num"),
169
+ "page_size": selection.get("page_size"),
170
+ "returned_items": 0,
171
+ "reported_total": 0,
172
+ "page_amount": 0,
173
+ },
174
+ "selection": selection,
175
+ },
176
+ "warnings": [
177
+ {
178
+ "code": "CONTACT_ROLE_PERMISSION_DENIED",
179
+ "message": "This is a role/contact-management permission boundary, not a record field candidate failure.",
180
+ "operation": operation,
181
+ }
182
+ ],
183
+ }
@@ -9,7 +9,7 @@ from mcp.server.fastmcp import FastMCP
9
9
  from pydantic import BaseModel, ValidationError
10
10
 
11
11
  from ..config import DEFAULT_PROFILE
12
- from ..errors import QingflowApiError
12
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
13
13
  from ..list_type_labels import get_record_list_type_label
14
14
  from ..solution.build_assembly_store import BuildAssemblyStore, default_manifest
15
15
  from ..solution.compiler import CompiledSolution, ExecutionPlan, ExecutionStep, build_execution_plan, compile_solution
@@ -38,6 +38,7 @@ from .qingbi_report_tools import QingbiReportTools
38
38
  from .record_tools import RecordTools
39
39
  from .role_tools import RoleTools
40
40
  from .view_tools import ViewTools
41
+ from .workflow_tools import WorkflowTools
41
42
  from .workspace_tools import WorkspaceTools
42
43
 
43
44
  STAGED_BUILD_MODES = {"preflight", "plan", "apply", "repair"}
@@ -736,36 +737,6 @@ class SolutionTools(ToolBase):
736
737
  ) -> dict[str, Any]:
737
738
  """执行方案相关逻辑。"""
738
739
  mode = _normalize_staged_build_mode(mode)
739
- app_key = str(flow_spec.get("app_key") or "").strip()
740
- spec_payload = flow_spec.get("spec")
741
- if app_key and isinstance(spec_payload, dict) and spec_payload:
742
- from .ai_builder_tools import AiBuilderTools
743
-
744
- builder = AiBuilderTools(self.sessions, self.backend)
745
- if mode in {"preflight", "plan"}:
746
- return {
747
- "build_id": build_id or _generate_build_id(run_label=run_label, stage_name="flow"),
748
- "mode": mode,
749
- "stage": "flow",
750
- "status": "planned" if mode == "plan" else "preflighted",
751
- "normalized_args": {"app_key": app_key, "spec": spec_payload},
752
- "tool_name": "solution_build_flow",
753
- }
754
- result = builder.app_flow_apply(
755
- profile=profile,
756
- app_key=app_key,
757
- spec=spec_payload,
758
- publish=publish,
759
- schema_version=flow_spec.get("schema_version") or flow_spec.get("schemaVersion"),
760
- )
761
- return {
762
- "build_id": build_id,
763
- "mode": mode,
764
- "stage": "flow",
765
- "status": result.get("status"),
766
- "result": result,
767
- "tool_name": "solution_build_flow",
768
- }
769
740
  return self._stage_build(
770
741
  profile=profile,
771
742
  mode=mode,
@@ -1438,6 +1409,7 @@ class SolutionTools(ToolBase):
1438
1409
  role_tools=RoleTools(self.sessions, self.backend),
1439
1410
  app_tools=AppTools(self.sessions, self.backend),
1440
1411
  record_tools=RecordTools(self.sessions, self.backend),
1412
+ workflow_tools=WorkflowTools(self.sessions, self.backend),
1441
1413
  view_tools=ViewTools(self.sessions, self.backend),
1442
1414
  chart_tools=QingbiReportTools(self.sessions, self.backend),
1443
1415
  portal_tools=PortalTools(self.sessions, self.backend),
@@ -1643,14 +1615,15 @@ class SolutionTools(ToolBase):
1643
1615
  )
1644
1616
  except Exception as exc: # noqa: BLE001
1645
1617
  verification["status"] = "partial"
1646
- verification["errors"].append(
1618
+ error_payload = _verification_error_payload(exc)
1619
+ error_payload.update(
1647
1620
  {
1648
1621
  "category": "verification",
1649
- "detail": str(exc),
1650
1622
  "entity_id": entity_id,
1651
1623
  "app_key": app_key,
1652
1624
  }
1653
1625
  )
1626
+ verification["errors"].append(error_payload)
1654
1627
  if verification["views_created"]:
1655
1628
  verification["views_strategy"] = "created"
1656
1629
  else:
@@ -1936,7 +1909,9 @@ class SolutionTools(ToolBase):
1936
1909
  result = packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=False)
1937
1910
  except (QingflowApiError, RuntimeError) as exc:
1938
1911
  error = _coerce_solution_api_error(exc)
1939
- if error.backend_code in {40002, 40027}:
1912
+ if is_auth_like_error(error):
1913
+ raise_tool_error(error)
1914
+ if backend_code_int(error) in {40002, 40027}:
1940
1915
  return {
1941
1916
  "status": "resolved",
1942
1917
  "matched_via": "tag_id",
@@ -1962,13 +1937,33 @@ class SolutionTools(ToolBase):
1962
1937
  retried=False,
1963
1938
  )
1964
1939
  summary = result.get("result") if isinstance(result.get("result"), dict) else {}
1965
- return {
1940
+ resolution: dict[str, Any] = {
1966
1941
  "status": "resolved",
1967
1942
  "matched_via": "tag_id",
1968
1943
  "tag_id": package_tag_id,
1969
1944
  "tag_name": summary.get("tagName"),
1970
1945
  "candidates": [],
1971
1946
  }
1947
+ warnings = result.get("warnings")
1948
+ if isinstance(warnings, list):
1949
+ for warning in warnings:
1950
+ if not isinstance(warning, dict):
1951
+ continue
1952
+ if warning.get("code") not in {"PACKAGE_BASE_INFO_UNAVAILABLE", "PACKAGE_DETAIL_READ_DEGRADED"}:
1953
+ continue
1954
+ resolution["metadata_unverified"] = True
1955
+ resolution["lookup_permission_blocked"] = {
1956
+ "scope": "package",
1957
+ "target": {"tag_id": package_tag_id},
1958
+ "transport_error": {
1959
+ "http_status": warning.get("http_status"),
1960
+ "backend_code": warning.get("backend_code"),
1961
+ "category": "backend",
1962
+ "request_id": warning.get("request_id"),
1963
+ },
1964
+ }
1965
+ break
1966
+ return resolution
1972
1967
  if not normalized_name:
1973
1968
  return {
1974
1969
  "status": "new_package",
@@ -3118,24 +3113,30 @@ def _builder_package_resolution_failed(
3118
3113
  error: QingflowApiError,
3119
3114
  retried: bool,
3120
3115
  ) -> dict[str, Any]:
3116
+ permission_restricted_listing = (
3117
+ package_tag_id <= 0
3118
+ and not is_auth_like_error(error)
3119
+ and backend_code_int(error) in {40002, 40027}
3120
+ )
3121
3121
  if package_tag_id > 0:
3122
3122
  detail = f"failed to resolve package_tag_id '{package_tag_id}': {error.message}"
3123
- elif error.backend_code in {40002, 40027}:
3123
+ elif permission_restricted_listing:
3124
3124
  detail = (
3125
3125
  f"failed to resolve package '{package_name}' because package listing is permission-restricted; "
3126
3126
  "provide package_tag_id explicitly or use an account that can list packages"
3127
3127
  )
3128
3128
  else:
3129
3129
  detail = f"failed to resolve package '{package_name}': {error.message}"
3130
- error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
3131
- error_fields["error_code"] = "PACKAGE_RESOLVE_FAILED"
3130
+ category = "config" if permission_restricted_listing else str(error.category or "backend")
3131
+ error_fields = _solution_error_fields(category=category, detail=detail, suggested_next_call=None, stage="app")
3132
+ error_fields["error_code"] = "AUTH_REQUIRED" if is_auth_like_error(error) else "PACKAGE_RESOLVE_FAILED"
3132
3133
  return {
3133
3134
  "status": "failed",
3134
3135
  "response": {
3135
3136
  "status": "failed",
3136
3137
  "mode": "plan",
3137
3138
  "stage": "app",
3138
- "errors": [{"category": "config", "detail": detail}],
3139
+ "errors": [{"category": category, "detail": detail}],
3139
3140
  "package_resolution": {
3140
3141
  "status": "failed",
3141
3142
  "requested_name": package_name or None,
@@ -3437,15 +3438,28 @@ def _stage_skip_reason(stage_name: str) -> str:
3437
3438
  return reasons.get(stage_name, "Stage skipped because no applicable payload was provided.")
3438
3439
 
3439
3440
 
3441
+ def _verification_error_payload(exc: Exception) -> dict[str, Any]:
3442
+ payload: dict[str, Any] = {"detail": str(exc)}
3443
+ if isinstance(exc, QingflowApiError):
3444
+ payload["transport_error"] = {
3445
+ "http_status": exc.http_status,
3446
+ "backend_code": exc.backend_code,
3447
+ "category": exc.category,
3448
+ "request_id": exc.request_id,
3449
+ }
3450
+ payload["request_id"] = exc.request_id
3451
+ payload["backend_code"] = exc.backend_code
3452
+ payload["http_status"] = exc.http_status
3453
+ return payload
3454
+
3455
+
3440
3456
  def _append_verification_error(verification: dict[str, Any], scope: str, exc: Exception) -> None:
3441
3457
  errors = verification.setdefault("errors", [])
3442
3458
  if isinstance(errors, list):
3443
- errors.append(
3444
- {
3445
- "scope": scope,
3446
- "message": str(exc),
3447
- }
3448
- )
3459
+ error_payload = _verification_error_payload(exc)
3460
+ error_payload["scope"] = scope
3461
+ error_payload["message"] = str(exc)
3462
+ errors.append(error_payload)
3449
3463
 
3450
3464
 
3451
3465
  PREFERRED_RECORD_TITLE_LABELS = (