@josephyan/qingflow-app-builder-mcp 1.1.24 → 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 -3100
- 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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
6
5
|
from mcp.server.fastmcp import FastMCP
|
|
7
6
|
|
|
8
7
|
from ..config import DEFAULT_PROFILE
|
|
9
|
-
from ..errors import QingflowApiError,
|
|
8
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
9
|
from ..list_type_labels import get_record_list_type_label, get_task_type_label
|
|
11
10
|
from .base import ToolBase, tool_cn_name
|
|
12
11
|
|
|
@@ -151,17 +150,7 @@ class TaskTools(ToolBase):
|
|
|
151
150
|
app_key: str | None,
|
|
152
151
|
) -> dict[str, Any]:
|
|
153
152
|
"""执行任务相关逻辑。"""
|
|
154
|
-
|
|
155
|
-
raw = self.task_statistics(profile=profile, app_key=app_key)
|
|
156
|
-
except RuntimeError as exc:
|
|
157
|
-
if not _is_optional_task_runtime_error(exc):
|
|
158
|
-
raise
|
|
159
|
-
return self._runtime_error_as_auxiliary_result(
|
|
160
|
-
exc,
|
|
161
|
-
error_code="TASK_SUMMARY_UNAVAILABLE",
|
|
162
|
-
selection={"app_key": app_key},
|
|
163
|
-
fallback_hint="Use task list/get directly; task summary is an auxiliary statistics entrypoint.",
|
|
164
|
-
)
|
|
153
|
+
raw = self.task_statistics(profile=profile, app_key=app_key)
|
|
165
154
|
statistics = raw.get("statistics", {})
|
|
166
155
|
summary = self._normalize_task_summary_payload(statistics)
|
|
167
156
|
return {
|
|
@@ -254,51 +243,34 @@ class TaskTools(ToolBase):
|
|
|
254
243
|
if limit <= 0:
|
|
255
244
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
256
245
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
page_size=max(limit, 20),
|
|
265
|
-
)
|
|
266
|
-
source = raw.get("page", {})
|
|
267
|
-
elif app_key:
|
|
268
|
-
raw = self.task_node_statistics(
|
|
269
|
-
profile=profile,
|
|
270
|
-
app_key=app_key,
|
|
271
|
-
type=normalized_type,
|
|
272
|
-
search_key=query,
|
|
273
|
-
)
|
|
274
|
-
source = raw.get("nodes", {})
|
|
275
|
-
else:
|
|
276
|
-
raw = self.task_workflow_nodes(
|
|
277
|
-
profile=profile,
|
|
278
|
-
type=normalized_type,
|
|
279
|
-
status=str(normalized_status),
|
|
280
|
-
app_key_list=None,
|
|
281
|
-
search_key=query,
|
|
282
|
-
page_num=1,
|
|
283
|
-
page_size=max(limit, 20),
|
|
284
|
-
)
|
|
285
|
-
source = raw.get("page", {})
|
|
286
|
-
except RuntimeError as exc:
|
|
287
|
-
if not _is_optional_task_runtime_error(exc):
|
|
288
|
-
raise
|
|
289
|
-
return self._runtime_error_as_auxiliary_result(
|
|
290
|
-
exc,
|
|
291
|
-
error_code="TASK_FACETS_UNAVAILABLE",
|
|
292
|
-
selection={
|
|
293
|
-
"task_box": task_box,
|
|
294
|
-
"flow_status": flow_status,
|
|
295
|
-
"dimension": dimension,
|
|
296
|
-
"app_key": app_key,
|
|
297
|
-
"query": query,
|
|
298
|
-
"limit": limit,
|
|
299
|
-
},
|
|
300
|
-
fallback_hint="Use task list/get directly; task facets are an auxiliary grouping entrypoint.",
|
|
246
|
+
if dimension == "worksheet":
|
|
247
|
+
raw = self.task_worksheet_statistics(
|
|
248
|
+
profile=profile,
|
|
249
|
+
type=normalized_type,
|
|
250
|
+
worksheet_name=query,
|
|
251
|
+
page_num=1,
|
|
252
|
+
page_size=max(limit, 20),
|
|
301
253
|
)
|
|
254
|
+
source = raw.get("page", {})
|
|
255
|
+
elif app_key:
|
|
256
|
+
raw = self.task_node_statistics(
|
|
257
|
+
profile=profile,
|
|
258
|
+
app_key=app_key,
|
|
259
|
+
type=normalized_type,
|
|
260
|
+
search_key=query,
|
|
261
|
+
)
|
|
262
|
+
source = raw.get("nodes", {})
|
|
263
|
+
else:
|
|
264
|
+
raw = self.task_workflow_nodes(
|
|
265
|
+
profile=profile,
|
|
266
|
+
type=normalized_type,
|
|
267
|
+
status=str(normalized_status),
|
|
268
|
+
app_key_list=None,
|
|
269
|
+
search_key=query,
|
|
270
|
+
page_num=1,
|
|
271
|
+
page_size=max(limit, 20),
|
|
272
|
+
)
|
|
273
|
+
source = raw.get("page", {})
|
|
302
274
|
|
|
303
275
|
groups = self._normalize_task_facets(source)
|
|
304
276
|
rows_truncated = len(groups) > limit
|
|
@@ -852,46 +824,6 @@ class TaskTools(ToolBase):
|
|
|
852
824
|
groups.append({"key": key if key is not None else label, "label": label, "count": count})
|
|
853
825
|
return groups
|
|
854
826
|
|
|
855
|
-
def _runtime_error_as_auxiliary_result(
|
|
856
|
-
self,
|
|
857
|
-
error: RuntimeError,
|
|
858
|
-
*,
|
|
859
|
-
error_code: str,
|
|
860
|
-
selection: dict[str, Any],
|
|
861
|
-
fallback_hint: str,
|
|
862
|
-
) -> dict[str, Any]:
|
|
863
|
-
"""Return a structured failure for optional task discovery helpers."""
|
|
864
|
-
try:
|
|
865
|
-
payload = json.loads(str(error))
|
|
866
|
-
except json.JSONDecodeError:
|
|
867
|
-
payload = {"message": str(error)}
|
|
868
|
-
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
869
|
-
warning: dict[str, Any] = {
|
|
870
|
-
"code": error_code,
|
|
871
|
-
"message": fallback_hint,
|
|
872
|
-
}
|
|
873
|
-
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
874
|
-
if payload.get(key) is not None:
|
|
875
|
-
warning[key] = payload.get(key)
|
|
876
|
-
response: dict[str, Any] = {
|
|
877
|
-
"ok": False,
|
|
878
|
-
"status": "failed",
|
|
879
|
-
"error_code": details.get("error_code") or error_code,
|
|
880
|
-
"message": payload.get("message") or str(error),
|
|
881
|
-
"warnings": [warning],
|
|
882
|
-
"output_profile": "normal",
|
|
883
|
-
"data": {
|
|
884
|
-
"selection": selection,
|
|
885
|
-
"fallback_hint": fallback_hint,
|
|
886
|
-
},
|
|
887
|
-
}
|
|
888
|
-
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
889
|
-
if payload.get(key) is not None:
|
|
890
|
-
response[key] = payload.get(key)
|
|
891
|
-
if details:
|
|
892
|
-
response["details"] = details
|
|
893
|
-
return response
|
|
894
|
-
|
|
895
827
|
def _public_task_action_response(
|
|
896
828
|
self,
|
|
897
829
|
raw: dict[str, Any],
|
|
@@ -927,22 +859,6 @@ class TaskTools(ToolBase):
|
|
|
927
859
|
}
|
|
928
860
|
|
|
929
861
|
|
|
930
|
-
def _is_optional_task_runtime_error(error: RuntimeError) -> bool:
|
|
931
|
-
try:
|
|
932
|
-
payload = json.loads(str(error))
|
|
933
|
-
except json.JSONDecodeError:
|
|
934
|
-
return False
|
|
935
|
-
if not isinstance(payload, dict):
|
|
936
|
-
return False
|
|
937
|
-
if str(payload.get("category") or "").strip().lower() == "auth":
|
|
938
|
-
return False
|
|
939
|
-
if message_looks_like_invalid_token(payload.get("message")):
|
|
940
|
-
return False
|
|
941
|
-
if backend_code_value_int(payload.get("http_status")) == 401:
|
|
942
|
-
return False
|
|
943
|
-
return backend_code_value_int(payload.get("backend_code")) in {40002, 40027, 404} or backend_code_value_int(payload.get("http_status")) == 404
|
|
944
|
-
|
|
945
|
-
|
|
946
862
|
def _task_page_items(payload: Any) -> list[Any]:
|
|
947
863
|
if isinstance(payload, list):
|
|
948
864
|
return payload
|
|
@@ -3,9 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from mcp.server.fastmcp import FastMCP
|
|
4
4
|
|
|
5
5
|
from ..config import DEFAULT_PROFILE
|
|
6
|
-
from ..errors import QingflowApiError,
|
|
6
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
7
7
|
from ..json_types import JSONObject, JSONValue
|
|
8
|
-
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
|
|
9
8
|
from .base import ToolBase, tool_cn_name
|
|
10
9
|
|
|
11
10
|
|
|
@@ -61,31 +60,7 @@ class ViewTools(ToolBase):
|
|
|
61
60
|
def view_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
62
61
|
"""执行视图相关逻辑。"""
|
|
63
62
|
self._require_app_key(app_key)
|
|
64
|
-
|
|
65
|
-
def runner(session_profile, context):
|
|
66
|
-
warnings: list[JSONObject] = []
|
|
67
|
-
verification: JSONObject = {"custom_views_loaded": True, "system_view_fallback_used": False}
|
|
68
|
-
try:
|
|
69
|
-
result = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
|
|
70
|
-
return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
|
|
71
|
-
except QingflowApiError as exc:
|
|
72
|
-
if not _is_optional_view_list_error(exc):
|
|
73
|
-
raise
|
|
74
|
-
items, probe_warnings = self._resolve_accessible_system_views(context, app_key)
|
|
75
|
-
warnings.extend(probe_warnings)
|
|
76
|
-
warnings.append(_view_list_fallback_warning("VIEW_LIST_UNAVAILABLE", exc))
|
|
77
|
-
verification = {"custom_views_loaded": False, "system_view_fallback_used": True}
|
|
78
|
-
return {
|
|
79
|
-
"profile": profile,
|
|
80
|
-
"ws_id": session_profile.selected_ws_id,
|
|
81
|
-
"app_key": app_key,
|
|
82
|
-
"result": items,
|
|
83
|
-
"items": items,
|
|
84
|
-
"warnings": warnings,
|
|
85
|
-
"verification": verification,
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return self._run(profile, runner)
|
|
63
|
+
return self._request(profile, "GET", f"/app/{app_key}/view/viewList", app_key=app_key)
|
|
89
64
|
|
|
90
65
|
@tool_cn_name("视图列表(扁平)")
|
|
91
66
|
def view_list_flat(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -132,40 +107,7 @@ class ViewTools(ToolBase):
|
|
|
132
107
|
"""执行视图相关逻辑。"""
|
|
133
108
|
self._require_viewgraph_key(viewgraph_key)
|
|
134
109
|
params = {"pass": passcode} if passcode else None
|
|
135
|
-
|
|
136
|
-
warnings: list[JSONObject] = []
|
|
137
|
-
verification: JSONObject = {"base_info_verified": True, "fallback_config_used": False}
|
|
138
|
-
try:
|
|
139
|
-
result = self.backend.request("GET", context, f"/view/{viewgraph_key}/viewConfig/baseInfo", params=params)
|
|
140
|
-
except QingflowApiError as error:
|
|
141
|
-
if not _is_optional_view_list_error(error):
|
|
142
|
-
raise
|
|
143
|
-
result = self.backend.request("GET", context, f"/view/{viewgraph_key}/viewConfig")
|
|
144
|
-
verification["base_info_verified"] = False
|
|
145
|
-
verification["fallback_config_used"] = True
|
|
146
|
-
warning: JSONObject = {
|
|
147
|
-
"code": "VIEW_BASE_INFO_UNAVAILABLE",
|
|
148
|
-
"message": "view_get_base_info used viewConfig because view baseInfo is unavailable in this permission context.",
|
|
149
|
-
}
|
|
150
|
-
if error.backend_code is not None:
|
|
151
|
-
warning["backend_code"] = error.backend_code
|
|
152
|
-
if error.http_status is not None:
|
|
153
|
-
warning["http_status"] = error.http_status
|
|
154
|
-
if error.request_id is not None:
|
|
155
|
-
warning["request_id"] = error.request_id
|
|
156
|
-
if error.details:
|
|
157
|
-
warning["details"] = error.details
|
|
158
|
-
warnings.append(warning)
|
|
159
|
-
return {
|
|
160
|
-
"profile": profile,
|
|
161
|
-
"ws_id": session_profile.selected_ws_id,
|
|
162
|
-
"viewgraph_key": viewgraph_key,
|
|
163
|
-
"result": result,
|
|
164
|
-
"warnings": warnings,
|
|
165
|
-
"verification": verification,
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return self._run(profile, runner)
|
|
110
|
+
return self._request(profile, "GET", f"/view/{viewgraph_key}/viewConfig/baseInfo", viewgraph_key=viewgraph_key, params=params)
|
|
169
111
|
|
|
170
112
|
@tool_cn_name("更新视图配置")
|
|
171
113
|
def view_update(self, *, profile: str, viewgraph_key: str, payload: JSONObject) -> JSONObject:
|
|
@@ -361,34 +303,6 @@ class ViewTools(ToolBase):
|
|
|
361
303
|
if value <= 0:
|
|
362
304
|
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
|
|
363
305
|
|
|
364
|
-
def _resolve_accessible_system_views(self, context, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
|
|
365
|
-
items: list[JSONObject] = []
|
|
366
|
-
warnings: list[JSONObject] = []
|
|
367
|
-
for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
|
|
368
|
-
try:
|
|
369
|
-
self.backend.request(
|
|
370
|
-
"POST",
|
|
371
|
-
context,
|
|
372
|
-
f"/app/{app_key}/apply/filter",
|
|
373
|
-
json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
|
|
374
|
-
)
|
|
375
|
-
except QingflowApiError as exc:
|
|
376
|
-
if _is_optional_view_list_error(exc):
|
|
377
|
-
continue
|
|
378
|
-
raise
|
|
379
|
-
items.append(
|
|
380
|
-
{
|
|
381
|
-
"view_id": view_id,
|
|
382
|
-
"viewId": view_id,
|
|
383
|
-
"viewName": name,
|
|
384
|
-
"name": name,
|
|
385
|
-
"kind": "system",
|
|
386
|
-
"list_type": list_type,
|
|
387
|
-
"analysis_supported": True,
|
|
388
|
-
}
|
|
389
|
-
)
|
|
390
|
-
return items, warnings
|
|
391
|
-
|
|
392
306
|
def _normalize_reorder_payload(self, payload: list[JSONObject]) -> list[JSONObject]:
|
|
393
307
|
"""执行内部辅助逻辑。"""
|
|
394
308
|
first_item = payload[0]
|
|
@@ -419,20 +333,3 @@ class ViewTools(ToolBase):
|
|
|
419
333
|
if not view_keys:
|
|
420
334
|
raise_tool_error(QingflowApiError.config_error("payload must include viewgraphKey/viewKey or ordinalType/viewKeyList"))
|
|
421
335
|
return [{"ordinalType": "FIXED_VIEW_LIST", "viewKeyList": view_keys}]
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def _is_optional_view_list_error(error: QingflowApiError) -> bool:
|
|
425
|
-
if is_auth_like_error(error):
|
|
426
|
-
return False
|
|
427
|
-
backend_code = backend_code_int(error)
|
|
428
|
-
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def _view_list_fallback_warning(code: str, error: QingflowApiError) -> JSONObject:
|
|
432
|
-
return {
|
|
433
|
-
"code": code,
|
|
434
|
-
"message": "custom view list is unavailable; returned accessible system views instead",
|
|
435
|
-
"backend_code": error.backend_code,
|
|
436
|
-
"http_status": error.http_status,
|
|
437
|
-
"request_id": error.request_id,
|
|
438
|
-
}
|
|
@@ -14,25 +14,13 @@ class WorkflowTools(ToolBase):
|
|
|
14
14
|
|
|
15
15
|
类型:流程配置工具。
|
|
16
16
|
主要职责:
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
|
|
17
|
+
1. 支持流程运行时校验与调试辅助能力;
|
|
18
|
+
2. 保留 legacy 节点写路径供内部/测试直接调用(非 MCP 公开)。
|
|
19
|
+
设计期流程配置读取统一走 WorkflowSpec(app_flow_get / GET /api/workflow/spec)。
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
def register(self, mcp: FastMCP) -> None:
|
|
23
23
|
"""注册当前工具到 MCP 服务。"""
|
|
24
|
-
@mcp.tool()
|
|
25
|
-
def workflow_list_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
26
|
-
return self.workflow_list_nodes(profile=profile, app_key=app_key)
|
|
27
|
-
|
|
28
|
-
@mcp.tool()
|
|
29
|
-
def workflow_get_node_detail(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
|
|
30
|
-
return self.workflow_get_node_detail(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
|
|
31
|
-
|
|
32
|
-
@mcp.tool()
|
|
33
|
-
def workflow_get_global_settings(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
34
|
-
return self.workflow_get_global_settings(profile=profile, app_key=app_key)
|
|
35
|
-
|
|
36
24
|
@mcp.tool()
|
|
37
25
|
def workflow_get_future_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> JSONObject:
|
|
38
26
|
return self.workflow_get_future_nodes(profile=profile, app_key=app_key, apply_id=apply_id)
|
|
@@ -53,22 +41,6 @@ class WorkflowTools(ToolBase):
|
|
|
53
41
|
audit_node_id=audit_node_id,
|
|
54
42
|
)
|
|
55
43
|
|
|
56
|
-
@mcp.tool()
|
|
57
|
-
def workflow_get_qsource_active(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
|
|
58
|
-
return self.workflow_get_qsource_active(profile=profile, app_key=app_key, qsource_id=qsource_id)
|
|
59
|
-
|
|
60
|
-
@mcp.tool()
|
|
61
|
-
def workflow_get_qsource_passive(profile: str = DEFAULT_PROFILE, app_key: str = "", qsource_id: int = 0) -> JSONObject:
|
|
62
|
-
return self.workflow_get_qsource_passive(profile=profile, app_key=app_key, qsource_id=qsource_id)
|
|
63
|
-
|
|
64
|
-
@mcp.tool()
|
|
65
|
-
def workflow_get_editable_question_ids(profile: str = DEFAULT_PROFILE, app_key: str = "", audit_node_id: int = 0) -> JSONObject:
|
|
66
|
-
return self.workflow_get_editable_question_ids(profile=profile, app_key=app_key, audit_node_id=audit_node_id)
|
|
67
|
-
|
|
68
|
-
@mcp.tool()
|
|
69
|
-
def workflow_get_print_nodes(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
70
|
-
return self.workflow_get_print_nodes(profile=profile, app_key=app_key)
|
|
71
|
-
|
|
72
44
|
@tool_cn_name("流程节点列表")
|
|
73
45
|
def workflow_list_nodes(self, *, profile: str, app_key: str) -> JSONObject:
|
|
74
46
|
"""执行流程相关逻辑。"""
|
|
@@ -351,23 +323,7 @@ class WorkflowTools(ToolBase):
|
|
|
351
323
|
for candidate_path in paths:
|
|
352
324
|
try:
|
|
353
325
|
result = self.backend.request("POST", call_context, candidate_path, json_body=json_body)
|
|
354
|
-
|
|
355
|
-
response: JSONObject = {
|
|
356
|
-
"profile": profile,
|
|
357
|
-
"ws_id": session_profile.selected_ws_id,
|
|
358
|
-
"result": result,
|
|
359
|
-
"request_path": candidate_path,
|
|
360
|
-
"requested_path": path,
|
|
361
|
-
"fallback_applied": fallback_applied,
|
|
362
|
-
"qf_version_source": (
|
|
363
|
-
call_context.qf_version_source
|
|
364
|
-
or ("context" if call_context.qf_version else "unset")
|
|
365
|
-
),
|
|
366
|
-
"verification": {
|
|
367
|
-
"primary_path_used": candidate_path == path,
|
|
368
|
-
"qf_version_used": call_context.qf_version is not None,
|
|
369
|
-
},
|
|
370
|
-
}
|
|
326
|
+
response: JSONObject = {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
|
|
371
327
|
response.update(extra)
|
|
372
328
|
if risk_operation and risk_target:
|
|
373
329
|
return self._attach_human_review_notice(response, operation=risk_operation, target=risk_target)
|
|
@@ -6,13 +6,10 @@ from mcp.server.fastmcp import FastMCP
|
|
|
6
6
|
|
|
7
7
|
from ..backend_client import BackendRequestContext
|
|
8
8
|
from ..config import DEFAULT_PROFILE
|
|
9
|
-
from ..errors import QingflowApiError,
|
|
9
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
10
10
|
from .base import ToolBase, tool_cn_name
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
_WORKSPACE_DETAIL_WARNING_KEY = "_qingflow_workspace_detail_warning"
|
|
14
|
-
|
|
15
|
-
|
|
16
13
|
class WorkspaceTools(ToolBase):
|
|
17
14
|
"""工作区工具(中文名:工作区与插件管理)。
|
|
18
15
|
|
|
@@ -94,14 +91,10 @@ class WorkspaceTools(ToolBase):
|
|
|
94
91
|
normalized = result if isinstance(result, dict) else {"list": result if isinstance(result, list) else []}
|
|
95
92
|
workspace_list = normalized.get("list") if isinstance(normalized.get("list"), list) else []
|
|
96
93
|
selected_ws_id = _.selected_ws_id if _ is not None else None
|
|
97
|
-
warnings: list[dict[str, Any]] = []
|
|
98
94
|
if selected_ws_id and not any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list):
|
|
99
95
|
try:
|
|
100
96
|
selected_workspace = self.backend.request("GET", context, f"/user/workspace/{selected_ws_id}")
|
|
101
|
-
except QingflowApiError
|
|
102
|
-
if not _is_optional_workspace_detail_error(exc):
|
|
103
|
-
raise
|
|
104
|
-
warnings.append(_workspace_detail_fallback_warning(exc, ws_id=selected_ws_id))
|
|
97
|
+
except QingflowApiError:
|
|
105
98
|
selected_workspace = {
|
|
106
99
|
"wsId": selected_ws_id,
|
|
107
100
|
"workspaceName": _.selected_ws_name if _ is not None else None,
|
|
@@ -116,14 +109,6 @@ class WorkspaceTools(ToolBase):
|
|
|
116
109
|
"profile": profile,
|
|
117
110
|
"include_external": include_external,
|
|
118
111
|
"page": normalized,
|
|
119
|
-
"warnings": warnings,
|
|
120
|
-
"verification": {
|
|
121
|
-
"selected_workspace_detail_verified": not warnings,
|
|
122
|
-
"selected_workspace_included": bool(
|
|
123
|
-
selected_ws_id
|
|
124
|
-
and any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list)
|
|
125
|
-
),
|
|
126
|
-
},
|
|
127
112
|
}
|
|
128
113
|
|
|
129
114
|
return self._run(profile, runner, require_workspace=False)
|
|
@@ -142,7 +127,6 @@ class WorkspaceTools(ToolBase):
|
|
|
142
127
|
if target_ws_id is None or target_ws_id <= 0:
|
|
143
128
|
raise_tool_error(QingflowApiError.workspace_not_selected(profile))
|
|
144
129
|
workspace = self._fetch_workspace_with_fallback(context, ws_id=target_ws_id)
|
|
145
|
-
workspace, warnings = _strip_workspace_detail_warning(workspace)
|
|
146
130
|
system_version = self._workspace_system_version(workspace)
|
|
147
131
|
return {
|
|
148
132
|
"profile": profile,
|
|
@@ -150,11 +134,6 @@ class WorkspaceTools(ToolBase):
|
|
|
150
134
|
"qf_version": system_version,
|
|
151
135
|
"qf_version_source": "workspace_system_version" if system_version else "unverified",
|
|
152
136
|
"workspace": workspace,
|
|
153
|
-
"warnings": warnings,
|
|
154
|
-
"verification": {
|
|
155
|
-
"workspace_detail_verified": not warnings,
|
|
156
|
-
"workspace_list_fallback_used": bool(warnings),
|
|
157
|
-
},
|
|
158
137
|
}
|
|
159
138
|
|
|
160
139
|
return self._run(profile, runner, require_workspace=False)
|
|
@@ -172,7 +151,6 @@ class WorkspaceTools(ToolBase):
|
|
|
172
151
|
|
|
173
152
|
def runner(_, context):
|
|
174
153
|
workspace = self._fetch_workspace_with_fallback(context, ws_id=ws_id)
|
|
175
|
-
workspace, warnings = _strip_workspace_detail_warning(workspace)
|
|
176
154
|
workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip() or None
|
|
177
155
|
selected = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=workspace_name)
|
|
178
156
|
system_version = self._workspace_system_version(workspace)
|
|
@@ -193,11 +171,6 @@ class WorkspaceTools(ToolBase):
|
|
|
193
171
|
"ws_id": selected.selected_ws_id,
|
|
194
172
|
"workspace_name": selected.selected_ws_name,
|
|
195
173
|
},
|
|
196
|
-
"warnings": warnings,
|
|
197
|
-
"verification": {
|
|
198
|
-
"workspace_detail_verified": not warnings,
|
|
199
|
-
"workspace_list_fallback_used": bool(warnings),
|
|
200
|
-
},
|
|
201
174
|
}
|
|
202
175
|
|
|
203
176
|
return self._run(profile, runner, require_workspace=False)
|
|
@@ -239,15 +212,7 @@ class WorkspaceTools(ToolBase):
|
|
|
239
212
|
*,
|
|
240
213
|
ws_id: int,
|
|
241
214
|
) -> dict[str, Any]:
|
|
242
|
-
|
|
243
|
-
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
244
|
-
except QingflowApiError as error:
|
|
245
|
-
if not _is_optional_workspace_detail_error(error):
|
|
246
|
-
raise
|
|
247
|
-
fallback = self._fetch_workspace_from_list(context, ws_id=ws_id)
|
|
248
|
-
if isinstance(fallback, dict):
|
|
249
|
-
return _with_workspace_detail_warning(fallback, error, ws_id=ws_id)
|
|
250
|
-
raise
|
|
215
|
+
workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
|
|
251
216
|
if not isinstance(workspace, dict):
|
|
252
217
|
raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
|
|
253
218
|
if self._workspace_needs_list_fallback(workspace):
|
|
@@ -299,36 +264,3 @@ class WorkspaceTools(ToolBase):
|
|
|
299
264
|
return None
|
|
300
265
|
normalized = str(value).strip()
|
|
301
266
|
return normalized or None
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def _is_optional_workspace_detail_error(error: QingflowApiError) -> bool:
|
|
305
|
-
if is_auth_like_error(error):
|
|
306
|
-
return False
|
|
307
|
-
backend_code = backend_code_int(error)
|
|
308
|
-
return backend_code in {40002, 40027, 404, 59004} or error.http_status == 404
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _with_workspace_detail_warning(workspace: dict[str, Any], error: QingflowApiError, *, ws_id: int) -> dict[str, Any]:
|
|
312
|
-
enriched = dict(workspace)
|
|
313
|
-
enriched[_WORKSPACE_DETAIL_WARNING_KEY] = _workspace_detail_fallback_warning(error, ws_id=ws_id)
|
|
314
|
-
return enriched
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _strip_workspace_detail_warning(workspace: dict[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
318
|
-
public_workspace = dict(workspace)
|
|
319
|
-
warning = public_workspace.pop(_WORKSPACE_DETAIL_WARNING_KEY, None)
|
|
320
|
-
warnings = [warning] if isinstance(warning, dict) else []
|
|
321
|
-
return public_workspace, warnings
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def _workspace_detail_fallback_warning(error: QingflowApiError, *, ws_id: int) -> dict[str, Any]:
|
|
325
|
-
return {
|
|
326
|
-
"code": "WORKSPACE_DETAIL_UNAVAILABLE",
|
|
327
|
-
"message": (
|
|
328
|
-
f"workspace detail route for ws_id {ws_id} was unavailable; "
|
|
329
|
-
"the tool used the visible workspace list as fallback."
|
|
330
|
-
),
|
|
331
|
-
"backend_code": error.backend_code,
|
|
332
|
-
"http_status": error.http_status,
|
|
333
|
-
"request_id": error.request_id,
|
|
334
|
-
}
|