@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12
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 +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -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 +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -3,12 +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
|
+
from ..solution.compiler.icon_utils import workspace_icon_config
|
|
9
10
|
from .app_tools import _analysis_supported_for_view_type
|
|
10
11
|
from .base import ToolBase, tool_cn_name
|
|
11
|
-
from .qingbi_report_tools import QingbiReportTools
|
|
12
|
+
from .qingbi_report_tools import QingbiReportTools, _coerce_tool_error
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ResourceReadTools(ToolBase):
|
|
@@ -75,6 +76,7 @@ class ResourceReadTools(ToolBase):
|
|
|
75
76
|
"dash_key": dash_key,
|
|
76
77
|
"dash_name": dash_name,
|
|
77
78
|
"dash_icon": dash_icon,
|
|
79
|
+
"icon_config": workspace_icon_config(dash_icon),
|
|
78
80
|
"package_tag_ids": package_tag_ids,
|
|
79
81
|
"component_count": len(components),
|
|
80
82
|
"components": components,
|
|
@@ -139,21 +141,60 @@ class ResourceReadTools(ToolBase):
|
|
|
139
141
|
warnings: list[JSONObject] = []
|
|
140
142
|
verification = {
|
|
141
143
|
"view_exists": True,
|
|
144
|
+
"config_verified": True,
|
|
145
|
+
"base_info_verified": True,
|
|
142
146
|
"questions_verified": True,
|
|
143
147
|
}
|
|
144
|
-
|
|
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
182
|
questions: list[dict[str, Any]] = []
|
|
147
183
|
try:
|
|
148
184
|
questions_payload = self.backend.request("GET", context, f"/view/{view_key}/question")
|
|
149
185
|
if isinstance(questions_payload, list):
|
|
150
186
|
questions = [deepcopy(item) for item in questions_payload if isinstance(item, dict)]
|
|
151
|
-
except QingflowApiError:
|
|
187
|
+
except QingflowApiError as error:
|
|
188
|
+
if not _is_optional_view_question_error(error):
|
|
189
|
+
raise
|
|
152
190
|
verification["questions_verified"] = False
|
|
153
191
|
warnings.append(
|
|
154
192
|
{
|
|
155
193
|
"code": "VIEW_QUESTIONS_UNAVAILABLE",
|
|
156
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,
|
|
157
198
|
}
|
|
158
199
|
)
|
|
159
200
|
|
|
@@ -221,13 +262,32 @@ class ResourceReadTools(ToolBase):
|
|
|
221
262
|
self._require_chart_id(chart_id)
|
|
222
263
|
|
|
223
264
|
def runner(session_profile, _context):
|
|
224
|
-
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
225
265
|
warnings: list[JSONObject] = []
|
|
226
266
|
verification = {
|
|
227
267
|
"chart_exists": True,
|
|
268
|
+
"chart_base_loaded": False,
|
|
228
269
|
"chart_data_loaded": False,
|
|
229
270
|
"chart_config_loaded": False,
|
|
230
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
|
+
)
|
|
231
291
|
data: Any = None
|
|
232
292
|
data_config: dict[str, Any] = {}
|
|
233
293
|
try:
|
|
@@ -237,23 +297,54 @@ class ResourceReadTools(ToolBase):
|
|
|
237
297
|
data_config = deepcopy(data.get("config"))
|
|
238
298
|
verification["chart_config_loaded"] = True
|
|
239
299
|
except (QingflowApiError, RuntimeError) as error:
|
|
240
|
-
api_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))
|
|
241
304
|
warnings.append(
|
|
242
305
|
{
|
|
243
306
|
"code": "CHART_DATA_UNAVAILABLE",
|
|
244
307
|
"message": "chart_get could not load chart data; returning chart metadata and config when available.",
|
|
245
|
-
|
|
246
|
-
"http_status": api_error.http_status if api_error is not None else None,
|
|
308
|
+
**_transport_warning_fields(api_error),
|
|
247
309
|
}
|
|
248
310
|
)
|
|
249
311
|
if not data_config:
|
|
250
312
|
try:
|
|
251
313
|
config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
252
|
-
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
|
+
)
|
|
253
326
|
config_result = {}
|
|
254
327
|
if isinstance(config_result, dict) and config_result:
|
|
255
328
|
data_config = deepcopy(config_result)
|
|
256
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
|
+
}
|
|
257
348
|
data_payload: dict[str, Any] = {
|
|
258
349
|
"chart_id": chart_id,
|
|
259
350
|
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
@@ -294,7 +385,9 @@ class ResourceReadTools(ToolBase):
|
|
|
294
385
|
def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
|
|
295
386
|
try:
|
|
296
387
|
form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
|
|
297
|
-
except QingflowApiError:
|
|
388
|
+
except QingflowApiError as exc:
|
|
389
|
+
if not _is_optional_view_metadata_resolution_error(exc):
|
|
390
|
+
raise
|
|
298
391
|
return None
|
|
299
392
|
if not isinstance(form_payload, dict):
|
|
300
393
|
return None
|
|
@@ -307,38 +400,13 @@ class ResourceReadTools(ToolBase):
|
|
|
307
400
|
return None
|
|
308
401
|
try:
|
|
309
402
|
payload = self.backend.request("GET", context, "/tag/apps")
|
|
310
|
-
except QingflowApiError:
|
|
403
|
+
except QingflowApiError as exc:
|
|
404
|
+
if not _is_optional_view_metadata_resolution_error(exc):
|
|
405
|
+
raise
|
|
311
406
|
payload = None
|
|
312
407
|
app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
|
|
313
408
|
if app_key:
|
|
314
409
|
return app_key
|
|
315
|
-
page_num = 1
|
|
316
|
-
page_size = 200
|
|
317
|
-
while page_num <= 20:
|
|
318
|
-
try:
|
|
319
|
-
page = self.backend.request(
|
|
320
|
-
"GET",
|
|
321
|
-
context,
|
|
322
|
-
"/app/item",
|
|
323
|
-
params={"pageNum": page_num, "pageSize": page_size},
|
|
324
|
-
)
|
|
325
|
-
except QingflowApiError:
|
|
326
|
-
return None
|
|
327
|
-
items = page.get("list") if isinstance(page, dict) else []
|
|
328
|
-
if not isinstance(items, list) or not items:
|
|
329
|
-
return None
|
|
330
|
-
for item in items:
|
|
331
|
-
if not isinstance(item, dict):
|
|
332
|
-
continue
|
|
333
|
-
if _coerce_positive_int(item.get("formId") or item.get("form_id")) != form_id:
|
|
334
|
-
continue
|
|
335
|
-
app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
|
|
336
|
-
if app_key:
|
|
337
|
-
return app_key
|
|
338
|
-
total = _coerce_positive_int(page.get("total")) if isinstance(page, dict) else None
|
|
339
|
-
if total is not None and page_num * page_size >= total:
|
|
340
|
-
break
|
|
341
|
-
page_num += 1
|
|
342
410
|
return None
|
|
343
411
|
|
|
344
412
|
|
|
@@ -368,6 +436,7 @@ def _normalize_portal_list_items(raw_items: Any) -> list[dict[str, Any]]:
|
|
|
368
436
|
"dash_key": dash_key or None,
|
|
369
437
|
"dash_name": dash_name or None,
|
|
370
438
|
"dash_icon": dash_icon,
|
|
439
|
+
"icon_config": workspace_icon_config(dash_icon),
|
|
371
440
|
"package_tag_ids": package_tag_ids,
|
|
372
441
|
}
|
|
373
442
|
)
|
|
@@ -420,6 +489,89 @@ def _normalize_user_portal_components(components: Any) -> list[dict[str, Any]]:
|
|
|
420
489
|
return items
|
|
421
490
|
|
|
422
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
|
+
|
|
423
575
|
def _normalize_portal_component_source_type(value: Any) -> str:
|
|
424
576
|
raw = str(value or "").strip()
|
|
425
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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":
|
|
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
|
|
@@ -1615,14 +1615,15 @@ class SolutionTools(ToolBase):
|
|
|
1615
1615
|
)
|
|
1616
1616
|
except Exception as exc: # noqa: BLE001
|
|
1617
1617
|
verification["status"] = "partial"
|
|
1618
|
-
|
|
1618
|
+
error_payload = _verification_error_payload(exc)
|
|
1619
|
+
error_payload.update(
|
|
1619
1620
|
{
|
|
1620
1621
|
"category": "verification",
|
|
1621
|
-
"detail": str(exc),
|
|
1622
1622
|
"entity_id": entity_id,
|
|
1623
1623
|
"app_key": app_key,
|
|
1624
1624
|
}
|
|
1625
1625
|
)
|
|
1626
|
+
verification["errors"].append(error_payload)
|
|
1626
1627
|
if verification["views_created"]:
|
|
1627
1628
|
verification["views_strategy"] = "created"
|
|
1628
1629
|
else:
|
|
@@ -1908,7 +1909,9 @@ class SolutionTools(ToolBase):
|
|
|
1908
1909
|
result = packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=False)
|
|
1909
1910
|
except (QingflowApiError, RuntimeError) as exc:
|
|
1910
1911
|
error = _coerce_solution_api_error(exc)
|
|
1911
|
-
if error
|
|
1912
|
+
if is_auth_like_error(error):
|
|
1913
|
+
raise_tool_error(error)
|
|
1914
|
+
if backend_code_int(error) in {40002, 40027}:
|
|
1912
1915
|
return {
|
|
1913
1916
|
"status": "resolved",
|
|
1914
1917
|
"matched_via": "tag_id",
|
|
@@ -1934,13 +1937,33 @@ class SolutionTools(ToolBase):
|
|
|
1934
1937
|
retried=False,
|
|
1935
1938
|
)
|
|
1936
1939
|
summary = result.get("result") if isinstance(result.get("result"), dict) else {}
|
|
1937
|
-
|
|
1940
|
+
resolution: dict[str, Any] = {
|
|
1938
1941
|
"status": "resolved",
|
|
1939
1942
|
"matched_via": "tag_id",
|
|
1940
1943
|
"tag_id": package_tag_id,
|
|
1941
1944
|
"tag_name": summary.get("tagName"),
|
|
1942
1945
|
"candidates": [],
|
|
1943
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
|
|
1944
1967
|
if not normalized_name:
|
|
1945
1968
|
return {
|
|
1946
1969
|
"status": "new_package",
|
|
@@ -3090,24 +3113,30 @@ def _builder_package_resolution_failed(
|
|
|
3090
3113
|
error: QingflowApiError,
|
|
3091
3114
|
retried: bool,
|
|
3092
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
|
+
)
|
|
3093
3121
|
if package_tag_id > 0:
|
|
3094
3122
|
detail = f"failed to resolve package_tag_id '{package_tag_id}': {error.message}"
|
|
3095
|
-
elif
|
|
3123
|
+
elif permission_restricted_listing:
|
|
3096
3124
|
detail = (
|
|
3097
3125
|
f"failed to resolve package '{package_name}' because package listing is permission-restricted; "
|
|
3098
3126
|
"provide package_tag_id explicitly or use an account that can list packages"
|
|
3099
3127
|
)
|
|
3100
3128
|
else:
|
|
3101
3129
|
detail = f"failed to resolve package '{package_name}': {error.message}"
|
|
3102
|
-
|
|
3103
|
-
error_fields
|
|
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"
|
|
3104
3133
|
return {
|
|
3105
3134
|
"status": "failed",
|
|
3106
3135
|
"response": {
|
|
3107
3136
|
"status": "failed",
|
|
3108
3137
|
"mode": "plan",
|
|
3109
3138
|
"stage": "app",
|
|
3110
|
-
"errors": [{"category":
|
|
3139
|
+
"errors": [{"category": category, "detail": detail}],
|
|
3111
3140
|
"package_resolution": {
|
|
3112
3141
|
"status": "failed",
|
|
3113
3142
|
"requested_name": package_name or None,
|
|
@@ -3409,15 +3438,28 @@ def _stage_skip_reason(stage_name: str) -> str:
|
|
|
3409
3438
|
return reasons.get(stage_name, "Stage skipped because no applicable payload was provided.")
|
|
3410
3439
|
|
|
3411
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
|
+
|
|
3412
3456
|
def _append_verification_error(verification: dict[str, Any], scope: str, exc: Exception) -> None:
|
|
3413
3457
|
errors = verification.setdefault("errors", [])
|
|
3414
3458
|
if isinstance(errors, list):
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
}
|
|
3420
|
-
)
|
|
3459
|
+
error_payload = _verification_error_payload(exc)
|
|
3460
|
+
error_payload["scope"] = scope
|
|
3461
|
+
error_payload["message"] = str(exc)
|
|
3462
|
+
errors.append(error_payload)
|
|
3421
3463
|
|
|
3422
3464
|
|
|
3423
3465
|
PREFERRED_RECORD_TITLE_LABELS = (
|