@josephyan/qingflow-app-user-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 (113) hide show
  1. package/README.md +7 -7
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-user-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 +277 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  12. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  13. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  14. package/skills/qingflow-app-builder/references/environments.md +63 -0
  15. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  16. package/skills/qingflow-app-builder/references/gotchas.md +108 -0
  17. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  18. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  19. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  20. package/skills/qingflow-app-builder/references/tool-selection.md +107 -0
  21. package/skills/qingflow-app-builder/references/update-flow.md +186 -0
  22. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  23. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  24. package/skills/qingflow-app-builder/references/update-views.md +291 -0
  25. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  26. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  27. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  28. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  29. package/skills/qingflow-app-user/SKILL.md +14 -17
  30. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  31. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  32. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  33. package/skills/qingflow-app-user/references/workflow-usage.md +5 -4
  34. package/skills/qingflow-mcp-setup/SKILL.md +9 -13
  35. package/skills/qingflow-mcp-setup/references/claude-desktop.md +1 -1
  36. package/skills/qingflow-mcp-setup/references/environments.md +1 -2
  37. package/skills/qingflow-mcp-setup/references/generic-stdio.md +5 -6
  38. package/skills/qingflow-record-analysis/SKILL.md +8 -9
  39. package/skills/qingflow-record-delete/SKILL.md +3 -7
  40. package/skills/qingflow-record-import/SKILL.md +2 -35
  41. package/skills/qingflow-record-insert/SKILL.md +6 -78
  42. package/skills/qingflow-record-update/SKILL.md +24 -39
  43. package/skills/qingflow-task-ops/SKILL.md +27 -30
  44. package/skills/qingflow-task-ops/references/environments.md +1 -0
  45. package/skills/qingflow-task-ops/references/workflow-usage.md +6 -4
  46. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  47. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  48. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  49. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  50. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  51. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  52. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  53. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  54. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  55. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  56. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  57. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  58. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  59. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  60. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  61. package/src/qingflow_mcp/__init__.py +1 -1
  62. package/src/qingflow_mcp/__main__.py +2 -6
  63. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  64. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  65. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  66. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  67. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  68. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  69. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  70. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  71. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  72. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  73. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  74. package/src/qingflow_mcp/cli/context.py +3 -0
  75. package/src/qingflow_mcp/cli/formatters.py +16 -784
  76. package/src/qingflow_mcp/cli/main.py +33 -117
  77. package/src/qingflow_mcp/errors.py +2 -43
  78. package/src/qingflow_mcp/public_surface.py +17 -26
  79. package/src/qingflow_mcp/response_trim.py +17 -81
  80. package/src/qingflow_mcp/server.py +12 -14
  81. package/src/qingflow_mcp/server_app_builder.py +36 -79
  82. package/src/qingflow_mcp/server_app_user.py +16 -22
  83. package/src/qingflow_mcp/session_store.py +7 -11
  84. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  85. package/src/qingflow_mcp/solution/executor.py +18 -245
  86. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  87. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  88. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  89. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  90. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  92. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  93. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  94. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  95. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  96. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  97. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  98. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  99. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  100. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  101. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  103. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  104. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  105. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  106. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  107. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  108. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  109. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  110. package/npm/bin/qingflow-skills.mjs +0 -5
  111. package/skills/qingflow-record-analysis/manifest.yaml +0 -10
  112. package/skills/qingflow-record-insert/manifest.yaml +0 -6
  113. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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, backend_code_int, is_auth_like_error, raise_tool_error
6
+ from ..errors import QingflowApiError, 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, _coerce_tool_error
12
+ from .qingbi_report_tools import QingbiReportTools
13
13
 
14
14
 
15
15
  class ResourceReadTools(ToolBase):
@@ -141,60 +141,21 @@ 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,
146
144
  "questions_verified": True,
147
145
  }
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
- )
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")
182
148
  questions: list[dict[str, Any]] = []
183
149
  try:
184
150
  questions_payload = self.backend.request("GET", context, f"/view/{view_key}/question")
185
151
  if isinstance(questions_payload, list):
186
152
  questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
187
- except QingflowApiError as error:
188
- if not _is_optional_view_question_error(error):
189
- raise
153
+ except QingflowApiError:
190
154
  verification["questions_verified"] = False
191
155
  warnings.append(
192
156
  {
193
157
  "code": "VIEW_QUESTIONS_UNAVAILABLE",
194
158
  "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,
198
159
  }
199
160
  )
200
161
 
@@ -262,32 +223,13 @@ class ResourceReadTools(ToolBase):
262
223
  self._require_chart_id(chart_id)
263
224
 
264
225
  def runner(session_profile, _context):
226
+ base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
265
227
  warnings: list[JSONObject] = []
266
228
  verification = {
267
229
  "chart_exists": True,
268
- "chart_base_loaded": False,
269
230
  "chart_data_loaded": False,
270
231
  "chart_config_loaded": False,
271
232
  }
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
- )
291
233
  data: Any = None
292
234
  data_config: dict[str, Any] = {}
293
235
  try:
@@ -297,54 +239,23 @@ class ResourceReadTools(ToolBase):
297
239
  data_config = deepcopy(data.get("config"))
298
240
  verification["chart_config_loaded"] = True
299
241
  except (QingflowApiError, RuntimeError) as error:
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))
242
+ api_error = error if isinstance(error, QingflowApiError) else None
304
243
  warnings.append(
305
244
  {
306
245
  "code": "CHART_DATA_UNAVAILABLE",
307
246
  "message": "chart_get could not load chart data; returning chart metadata and config when available.",
308
- **_transport_warning_fields(api_error),
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,
309
249
  }
310
250
  )
311
251
  if not data_config:
312
252
  try:
313
253
  config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
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
- )
254
+ except (QingflowApiError, RuntimeError):
326
255
  config_result = {}
327
256
  if isinstance(config_result, dict) and config_result:
328
257
  data_config = deepcopy(config_result)
329
258
  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
- }
348
259
  data_payload: dict[str, Any] = {
349
260
  "chart_id": chart_id,
350
261
  "chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
@@ -385,9 +296,7 @@ class ResourceReadTools(ToolBase):
385
296
  def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
386
297
  try:
387
298
  form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
388
- except QingflowApiError as exc:
389
- if not _is_optional_view_metadata_resolution_error(exc):
390
- raise
299
+ except QingflowApiError:
391
300
  return None
392
301
  if not isinstance(form_payload, dict):
393
302
  return None
@@ -400,13 +309,38 @@ class ResourceReadTools(ToolBase):
400
309
  return None
401
310
  try:
402
311
  payload = self.backend.request("GET", context, "/tag/apps")
403
- except QingflowApiError as exc:
404
- if not _is_optional_view_metadata_resolution_error(exc):
405
- raise
312
+ except QingflowApiError:
406
313
  payload = None
407
314
  app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
408
315
  if app_key:
409
316
  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
410
344
  return None
411
345
 
412
346
 
@@ -489,89 +423,6 @@ def _normalize_user_portal_components(components: Any) -> list[dict[str, Any]]:
489
423
  return items
490
424
 
491
425
 
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
-
575
426
  def _normalize_portal_component_source_type(value: Any) -> str:
576
427
  raw = str(value or "").strip()
577
428
  mapping = {
@@ -1,11 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
-
5
3
  from mcp.server.fastmcp import FastMCP
6
4
 
7
5
  from ..config import DEFAULT_PROFILE
8
- from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
6
+ from ..errors import QingflowApiError, raise_tool_error
9
7
  from ..json_types import JSONObject
10
8
  from .base import ToolBase, tool_cn_name
11
9
 
@@ -22,7 +20,7 @@ class RoleTools(ToolBase):
22
20
 
23
21
  def register(self, mcp: FastMCP) -> None:
24
22
  """注册当前工具到 MCP 服务。"""
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.")
23
+ @mcp.tool()
26
24
  def role_search(
27
25
  profile: str = DEFAULT_PROFILE,
28
26
  keyword: str = "",
@@ -52,36 +50,18 @@ class RoleTools(ToolBase):
52
50
  """执行角色相关逻辑。"""
53
51
  if page_num <= 0 or page_size <= 0:
54
52
  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
- )
62
53
 
63
54
  def runner(session_profile, context):
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
55
+ result = self.backend.request(
56
+ "GET",
57
+ context,
58
+ "/contact/roleByPage",
59
+ params={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
60
+ )
81
61
  return {
82
62
  "profile": profile,
83
63
  "ws_id": session_profile.selected_ws_id,
84
- "keyword": normalized_keyword,
64
+ "keyword": keyword,
85
65
  "page": result,
86
66
  }
87
67
 
@@ -130,54 +110,3 @@ class RoleTools(ToolBase):
130
110
  )
131
111
 
132
112
  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, backend_code_int, is_auth_like_error, raise_tool_error
12
+ from ..errors import QingflowApiError
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,7 +38,6 @@ 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
42
41
  from .workspace_tools import WorkspaceTools
43
42
 
44
43
  STAGED_BUILD_MODES = {"preflight", "plan", "apply", "repair"}
@@ -737,6 +736,36 @@ class SolutionTools(ToolBase):
737
736
  ) -> dict[str, Any]:
738
737
  """执行方案相关逻辑。"""
739
738
  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
+ }
740
769
  return self._stage_build(
741
770
  profile=profile,
742
771
  mode=mode,
@@ -1409,7 +1438,6 @@ class SolutionTools(ToolBase):
1409
1438
  role_tools=RoleTools(self.sessions, self.backend),
1410
1439
  app_tools=AppTools(self.sessions, self.backend),
1411
1440
  record_tools=RecordTools(self.sessions, self.backend),
1412
- workflow_tools=WorkflowTools(self.sessions, self.backend),
1413
1441
  view_tools=ViewTools(self.sessions, self.backend),
1414
1442
  chart_tools=QingbiReportTools(self.sessions, self.backend),
1415
1443
  portal_tools=PortalTools(self.sessions, self.backend),
@@ -1615,15 +1643,14 @@ class SolutionTools(ToolBase):
1615
1643
  )
1616
1644
  except Exception as exc: # noqa: BLE001
1617
1645
  verification["status"] = "partial"
1618
- error_payload = _verification_error_payload(exc)
1619
- error_payload.update(
1646
+ verification["errors"].append(
1620
1647
  {
1621
1648
  "category": "verification",
1649
+ "detail": str(exc),
1622
1650
  "entity_id": entity_id,
1623
1651
  "app_key": app_key,
1624
1652
  }
1625
1653
  )
1626
- verification["errors"].append(error_payload)
1627
1654
  if verification["views_created"]:
1628
1655
  verification["views_strategy"] = "created"
1629
1656
  else:
@@ -1909,9 +1936,7 @@ class SolutionTools(ToolBase):
1909
1936
  result = packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=False)
1910
1937
  except (QingflowApiError, RuntimeError) as exc:
1911
1938
  error = _coerce_solution_api_error(exc)
1912
- if is_auth_like_error(error):
1913
- raise_tool_error(error)
1914
- if backend_code_int(error) in {40002, 40027}:
1939
+ if error.backend_code in {40002, 40027}:
1915
1940
  return {
1916
1941
  "status": "resolved",
1917
1942
  "matched_via": "tag_id",
@@ -1937,33 +1962,13 @@ class SolutionTools(ToolBase):
1937
1962
  retried=False,
1938
1963
  )
1939
1964
  summary = result.get("result") if isinstance(result.get("result"), dict) else {}
1940
- resolution: dict[str, Any] = {
1965
+ return {
1941
1966
  "status": "resolved",
1942
1967
  "matched_via": "tag_id",
1943
1968
  "tag_id": package_tag_id,
1944
1969
  "tag_name": summary.get("tagName"),
1945
1970
  "candidates": [],
1946
1971
  }
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
1967
1972
  if not normalized_name:
1968
1973
  return {
1969
1974
  "status": "new_package",
@@ -3113,30 +3118,24 @@ def _builder_package_resolution_failed(
3113
3118
  error: QingflowApiError,
3114
3119
  retried: bool,
3115
3120
  ) -> 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 permission_restricted_listing:
3123
+ elif error.backend_code in {40002, 40027}:
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
- 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"
3130
+ error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
3131
+ error_fields["error_code"] = "PACKAGE_RESOLVE_FAILED"
3133
3132
  return {
3134
3133
  "status": "failed",
3135
3134
  "response": {
3136
3135
  "status": "failed",
3137
3136
  "mode": "plan",
3138
3137
  "stage": "app",
3139
- "errors": [{"category": category, "detail": detail}],
3138
+ "errors": [{"category": "config", "detail": detail}],
3140
3139
  "package_resolution": {
3141
3140
  "status": "failed",
3142
3141
  "requested_name": package_name or None,
@@ -3438,28 +3437,15 @@ def _stage_skip_reason(stage_name: str) -> str:
3438
3437
  return reasons.get(stage_name, "Stage skipped because no applicable payload was provided.")
3439
3438
 
3440
3439
 
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
-
3456
3440
  def _append_verification_error(verification: dict[str, Any], scope: str, exc: Exception) -> None:
3457
3441
  errors = verification.setdefault("errors", [])
3458
3442
  if isinstance(errors, list):
3459
- error_payload = _verification_error_payload(exc)
3460
- error_payload["scope"] = scope
3461
- error_payload["message"] = str(exc)
3462
- errors.append(error_payload)
3443
+ errors.append(
3444
+ {
3445
+ "scope": scope,
3446
+ "message": str(exc),
3447
+ }
3448
+ )
3463
3449
 
3464
3450
 
3465
3451
  PREFERRED_RECORD_TITLE_LABELS = (