@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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":
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3131
|
-
error_fields =
|
|
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":
|
|
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
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3443
|
+
errors.append(
|
|
3444
|
+
{
|
|
3445
|
+
"scope": scope,
|
|
3446
|
+
"message": str(exc),
|
|
3447
|
+
}
|
|
3448
|
+
)
|
|
3463
3449
|
|
|
3464
3450
|
|
|
3465
3451
|
PREFERRED_RECORD_TITLE_LABELS = (
|