@josephyan/qingflow-cli 1.1.3 → 1.1.5
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 +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -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/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +287 -25
- package/src/qingflow_mcp/builder_facade/service.py +4195 -856
- package/src/qingflow_mcp/cli/commands/builder.py +316 -247
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- 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 +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1782 -399
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- 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 +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- 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 +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- 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 +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
|
@@ -3,7 +3,7 @@ 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, 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 .base import ToolBase, tool_cn_name
|
|
9
9
|
|
|
@@ -85,8 +85,26 @@ class NavigationTools(ToolBase):
|
|
|
85
85
|
params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
|
|
86
86
|
if query_key:
|
|
87
87
|
params["queryCondition"] = query_key
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
fallback_error: QingflowApiError | None = None
|
|
89
|
+
try:
|
|
90
|
+
result = self.backend.request("GET", context, "/navigation/page", params=params)
|
|
91
|
+
response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "page": result}
|
|
92
|
+
except QingflowApiError as exc:
|
|
93
|
+
if not _is_optional_draft_navigation_read_error(exc):
|
|
94
|
+
raise
|
|
95
|
+
fallback_error = exc
|
|
96
|
+
result = self.backend.request("GET", context, "/navigation", params={"pageNum": page_num, "pageSize": page_size})
|
|
97
|
+
response = {
|
|
98
|
+
"profile": profile,
|
|
99
|
+
"ws_id": session_profile.selected_ws_id,
|
|
100
|
+
"status": "published",
|
|
101
|
+
"requested_status": "draft",
|
|
102
|
+
"page": result,
|
|
103
|
+
}
|
|
104
|
+
if fallback_error is not None:
|
|
105
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_PAGE_UNAVAILABLE", fallback_error)]
|
|
106
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
107
|
+
return response
|
|
90
108
|
|
|
91
109
|
return self._run(profile, runner)
|
|
92
110
|
|
|
@@ -97,8 +115,26 @@ class NavigationTools(ToolBase):
|
|
|
97
115
|
params: JSONObject = {}
|
|
98
116
|
if query_key:
|
|
99
117
|
params["queryCondition"] = query_key
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
fallback_error: QingflowApiError | None = None
|
|
119
|
+
try:
|
|
120
|
+
result = self.backend.request("GET", context, "/navigation/all", params=params)
|
|
121
|
+
response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "items": result}
|
|
122
|
+
except QingflowApiError as exc:
|
|
123
|
+
if not _is_optional_draft_navigation_read_error(exc):
|
|
124
|
+
raise
|
|
125
|
+
fallback_error = exc
|
|
126
|
+
result = self.backend.request("GET", context, "/navigation", params={"pageNum": 1, "pageSize": 1000})
|
|
127
|
+
response = {
|
|
128
|
+
"profile": profile,
|
|
129
|
+
"ws_id": session_profile.selected_ws_id,
|
|
130
|
+
"status": "published",
|
|
131
|
+
"requested_status": "draft",
|
|
132
|
+
"items": result,
|
|
133
|
+
}
|
|
134
|
+
if fallback_error is not None:
|
|
135
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_ALL_UNAVAILABLE", fallback_error)]
|
|
136
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
137
|
+
return response
|
|
102
138
|
|
|
103
139
|
return self._run(profile, runner)
|
|
104
140
|
|
|
@@ -108,13 +144,39 @@ class NavigationTools(ToolBase):
|
|
|
108
144
|
self._require_navigation_item_id(navigation_item_id)
|
|
109
145
|
|
|
110
146
|
def runner(session_profile, context):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
requested_status = str(status or "draft")
|
|
148
|
+
fallback_error: QingflowApiError | None = None
|
|
149
|
+
try:
|
|
150
|
+
result = self.backend.request(
|
|
151
|
+
"GET",
|
|
152
|
+
context,
|
|
153
|
+
f"/navigation/detail/{navigation_item_id}",
|
|
154
|
+
params={"status": requested_status},
|
|
155
|
+
)
|
|
156
|
+
effective_status = requested_status
|
|
157
|
+
except QingflowApiError as exc:
|
|
158
|
+
if requested_status != "draft" or not _is_optional_draft_navigation_read_error(exc):
|
|
159
|
+
raise
|
|
160
|
+
fallback_error = exc
|
|
161
|
+
effective_status = "published"
|
|
162
|
+
result = self.backend.request(
|
|
163
|
+
"GET",
|
|
164
|
+
context,
|
|
165
|
+
f"/navigation/detail/{navigation_item_id}",
|
|
166
|
+
params={"status": effective_status},
|
|
167
|
+
)
|
|
168
|
+
response = {
|
|
169
|
+
"profile": profile,
|
|
170
|
+
"ws_id": session_profile.selected_ws_id,
|
|
171
|
+
"navigation_item_id": navigation_item_id,
|
|
172
|
+
"status": effective_status,
|
|
173
|
+
"requested_status": requested_status,
|
|
174
|
+
"result": result,
|
|
175
|
+
}
|
|
176
|
+
if fallback_error is not None:
|
|
177
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_DETAIL_UNAVAILABLE", fallback_error)]
|
|
178
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
179
|
+
return response
|
|
118
180
|
|
|
119
181
|
return self._run(profile, runner)
|
|
120
182
|
|
|
@@ -208,3 +270,20 @@ class NavigationTools(ToolBase):
|
|
|
208
270
|
"""执行内部辅助逻辑。"""
|
|
209
271
|
if navigation_item_id <= 0:
|
|
210
272
|
raise_tool_error(QingflowApiError.config_error("navigation_item_id must be positive"))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_optional_draft_navigation_read_error(error: QingflowApiError) -> bool:
|
|
276
|
+
if is_auth_like_error(error):
|
|
277
|
+
return False
|
|
278
|
+
backend_code = backend_code_int(error)
|
|
279
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _navigation_fallback_warning(code: str, error: QingflowApiError) -> JSONObject:
|
|
283
|
+
return {
|
|
284
|
+
"code": code,
|
|
285
|
+
"message": "draft navigation data is unavailable; returned published navigation data instead",
|
|
286
|
+
"backend_code": error.backend_code,
|
|
287
|
+
"http_status": error.http_status,
|
|
288
|
+
"request_id": error.request_id,
|
|
289
|
+
}
|
|
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|
|
5
5
|
from mcp.server.fastmcp import FastMCP
|
|
6
6
|
|
|
7
7
|
from ..config import DEFAULT_PROFILE
|
|
8
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
8
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
9
9
|
from ..json_types import JSONObject
|
|
10
10
|
from ..solution.compiler.icon_utils import workspace_icon_config
|
|
11
11
|
from .base import ToolBase, tool_cn_name
|
|
@@ -76,8 +76,44 @@ class PackageTools(ToolBase):
|
|
|
76
76
|
self._require_tag_id(tag_id)
|
|
77
77
|
|
|
78
78
|
def runner(session_profile, context):
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
warnings: list[JSONObject] = []
|
|
80
|
+
verification: JSONObject = {"base_info_verified": True, "fallback_visible_package_used": False}
|
|
81
|
+
try:
|
|
82
|
+
base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
|
|
83
|
+
except QingflowApiError as exc:
|
|
84
|
+
if not _is_optional_package_metadata_error(exc):
|
|
85
|
+
raise
|
|
86
|
+
base_info = self._resolve_visible_package(context, tag_id)
|
|
87
|
+
if base_info is None:
|
|
88
|
+
raise
|
|
89
|
+
warnings.append(
|
|
90
|
+
{
|
|
91
|
+
"code": "PACKAGE_BASE_INFO_UNAVAILABLE",
|
|
92
|
+
"message": "package_get used the current user's visible package list because package baseInfo is unavailable in this permission context.",
|
|
93
|
+
"backend_code": exc.backend_code,
|
|
94
|
+
"http_status": exc.http_status,
|
|
95
|
+
"request_id": exc.request_id,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
verification = {"base_info_verified": False, "fallback_visible_package_used": True}
|
|
99
|
+
result: JSONObject = base_info if isinstance(base_info, dict) else {}
|
|
100
|
+
if verification["base_info_verified"]:
|
|
101
|
+
try:
|
|
102
|
+
detail = self.backend.request("GET", context, f"/tag/{tag_id}")
|
|
103
|
+
if isinstance(detail, dict):
|
|
104
|
+
result = detail
|
|
105
|
+
except QingflowApiError as exc:
|
|
106
|
+
if not _is_optional_package_metadata_error(exc):
|
|
107
|
+
raise
|
|
108
|
+
warnings.append(
|
|
109
|
+
{
|
|
110
|
+
"code": "PACKAGE_DETAIL_READ_DEGRADED",
|
|
111
|
+
"message": "package_get used package baseInfo because the detail endpoint requires package edit/add-app permission.",
|
|
112
|
+
"backend_code": exc.backend_code,
|
|
113
|
+
"http_status": exc.http_status,
|
|
114
|
+
"request_id": exc.request_id,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
81
117
|
compact = self._compact_package(
|
|
82
118
|
result if isinstance(result, dict) else {},
|
|
83
119
|
base_info=base_info if isinstance(base_info, dict) else None,
|
|
@@ -88,7 +124,11 @@ class PackageTools(ToolBase):
|
|
|
88
124
|
"tag_id": tag_id,
|
|
89
125
|
"result": result if include_raw else compact,
|
|
90
126
|
"compact": not include_raw,
|
|
127
|
+
"detail_degraded": bool(warnings),
|
|
128
|
+
"verification": verification,
|
|
91
129
|
}
|
|
130
|
+
if warnings:
|
|
131
|
+
response["warnings"] = warnings
|
|
92
132
|
if include_raw:
|
|
93
133
|
response["summary"] = compact
|
|
94
134
|
return response
|
|
@@ -101,7 +141,27 @@ class PackageTools(ToolBase):
|
|
|
101
141
|
self._require_readable_tag_id(tag_id)
|
|
102
142
|
|
|
103
143
|
def runner(session_profile, context):
|
|
104
|
-
|
|
144
|
+
warnings: list[JSONObject] = []
|
|
145
|
+
verification: JSONObject = {"base_info_verified": True, "fallback_visible_package_used": False}
|
|
146
|
+
try:
|
|
147
|
+
result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
|
|
148
|
+
except QingflowApiError as exc:
|
|
149
|
+
if not _is_optional_package_metadata_error(exc):
|
|
150
|
+
raise
|
|
151
|
+
visible_package = self._resolve_visible_package(context, tag_id)
|
|
152
|
+
if visible_package is None:
|
|
153
|
+
raise
|
|
154
|
+
result = visible_package
|
|
155
|
+
warnings.append(
|
|
156
|
+
{
|
|
157
|
+
"code": "PACKAGE_BASE_INFO_UNAVAILABLE",
|
|
158
|
+
"message": "package_get_base used the current user's visible package list because package baseInfo is unavailable in this permission context.",
|
|
159
|
+
"backend_code": exc.backend_code,
|
|
160
|
+
"http_status": exc.http_status,
|
|
161
|
+
"request_id": exc.request_id,
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
verification = {"base_info_verified": False, "fallback_visible_package_used": True}
|
|
105
165
|
compact = self._compact_package(result if isinstance(result, dict) else {})
|
|
106
166
|
response = {
|
|
107
167
|
"profile": profile,
|
|
@@ -109,7 +169,10 @@ class PackageTools(ToolBase):
|
|
|
109
169
|
"tag_id": tag_id,
|
|
110
170
|
"result": result if include_raw else compact,
|
|
111
171
|
"compact": not include_raw,
|
|
172
|
+
"verification": verification,
|
|
112
173
|
}
|
|
174
|
+
if warnings:
|
|
175
|
+
response["warnings"] = warnings
|
|
113
176
|
if include_raw:
|
|
114
177
|
response["summary"] = compact
|
|
115
178
|
return response
|
|
@@ -134,15 +197,44 @@ class PackageTools(ToolBase):
|
|
|
134
197
|
body = self._require_dict(payload)
|
|
135
198
|
|
|
136
199
|
def runner(session_profile, context):
|
|
137
|
-
|
|
200
|
+
warnings: list[JSONObject] = []
|
|
201
|
+
try:
|
|
202
|
+
current = self.backend.request("GET", context, f"/tag/{tag_id}")
|
|
203
|
+
except QingflowApiError as exc:
|
|
204
|
+
if not _is_optional_package_metadata_error(exc):
|
|
205
|
+
raise
|
|
206
|
+
try:
|
|
207
|
+
current = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
|
|
208
|
+
fallback_source = "baseInfo"
|
|
209
|
+
except QingflowApiError as base_exc:
|
|
210
|
+
if not _is_optional_package_metadata_error(base_exc):
|
|
211
|
+
raise
|
|
212
|
+
visible_package = self._resolve_visible_package(context, tag_id)
|
|
213
|
+
if visible_package is None:
|
|
214
|
+
raise base_exc
|
|
215
|
+
current = visible_package
|
|
216
|
+
fallback_source = "visible_package_list"
|
|
217
|
+
warnings.append(
|
|
218
|
+
{
|
|
219
|
+
"code": "PACKAGE_DETAIL_READ_DEGRADED",
|
|
220
|
+
"message": "package_update used package baseInfo or visible package metadata because package detail is unavailable in this permission context.",
|
|
221
|
+
"source": fallback_source,
|
|
222
|
+
"backend_code": exc.backend_code,
|
|
223
|
+
"http_status": exc.http_status,
|
|
224
|
+
"request_id": exc.request_id,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
138
227
|
result = self.backend.request(
|
|
139
228
|
"PUT",
|
|
140
229
|
context,
|
|
141
230
|
f"/tag/{tag_id}",
|
|
142
231
|
json_body=self._normalize_package_payload(body, existing=current),
|
|
143
232
|
)
|
|
233
|
+
response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result}
|
|
234
|
+
if warnings:
|
|
235
|
+
response["warnings"] = warnings
|
|
144
236
|
return self._attach_human_review_notice(
|
|
145
|
-
|
|
237
|
+
response,
|
|
146
238
|
operation="update",
|
|
147
239
|
target="app package settings",
|
|
148
240
|
)
|
|
@@ -251,6 +343,19 @@ class PackageTools(ToolBase):
|
|
|
251
343
|
if tag_id < 0:
|
|
252
344
|
raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
|
|
253
345
|
|
|
346
|
+
def _resolve_visible_package(self, context, tag_id: int) -> JSONObject | None: # type: ignore[no-untyped-def]
|
|
347
|
+
"""Resolve package metadata from the current user's visible package list."""
|
|
348
|
+
payload = self.backend.request("GET", context, "/tag", params={"trialStatus": "all"})
|
|
349
|
+
items, _source_shape = self._extract_package_items(payload)
|
|
350
|
+
for item in items:
|
|
351
|
+
try:
|
|
352
|
+
candidate = int(item.get("tagId")) if item.get("tagId") is not None else None
|
|
353
|
+
except (TypeError, ValueError):
|
|
354
|
+
candidate = None
|
|
355
|
+
if candidate == tag_id:
|
|
356
|
+
return dict(item)
|
|
357
|
+
return None
|
|
358
|
+
|
|
254
359
|
def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
|
|
255
360
|
"""执行内部辅助逻辑。"""
|
|
256
361
|
data = deepcopy(existing) if isinstance(existing, dict) else {}
|
|
@@ -338,3 +443,10 @@ def _default_package_auth() -> JSONObject:
|
|
|
338
443
|
"contactAuth": {"type": "WORKSPACE_ALL", "authMembers": deepcopy(members)},
|
|
339
444
|
"externalMemberAuth": {"type": "NOT", "authMembers": deepcopy(members)},
|
|
340
445
|
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _is_optional_package_metadata_error(error: QingflowApiError) -> bool:
|
|
449
|
+
if is_auth_like_error(error):
|
|
450
|
+
return False
|
|
451
|
+
backend_code = backend_code_int(error)
|
|
452
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
@@ -3,7 +3,7 @@ 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, 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 .base import ToolBase, tool_cn_name
|
|
9
9
|
|
|
@@ -89,8 +89,37 @@ class PortalTools(ToolBase):
|
|
|
89
89
|
self._require_dash_key(dash_key)
|
|
90
90
|
|
|
91
91
|
def runner(session_profile, context):
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
warnings: list[JSONObject] = []
|
|
93
|
+
verification: JSONObject = {"base_info_verified": True, "fallback_detail_used": False}
|
|
94
|
+
try:
|
|
95
|
+
result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
|
|
96
|
+
except QingflowApiError as error:
|
|
97
|
+
if not _is_optional_portal_base_info_error(error):
|
|
98
|
+
raise
|
|
99
|
+
result = self.backend.request("GET", context, f"/dash/{dash_key}", params={"beingDraft": False})
|
|
100
|
+
verification["base_info_verified"] = False
|
|
101
|
+
verification["fallback_detail_used"] = True
|
|
102
|
+
warning: JSONObject = {
|
|
103
|
+
"code": "PORTAL_BASE_INFO_UNAVAILABLE",
|
|
104
|
+
"message": "portal_get_base_info used portal detail because portal baseInfo is unavailable in this permission context.",
|
|
105
|
+
}
|
|
106
|
+
if error.backend_code is not None:
|
|
107
|
+
warning["backend_code"] = error.backend_code
|
|
108
|
+
if error.http_status is not None:
|
|
109
|
+
warning["http_status"] = error.http_status
|
|
110
|
+
if error.request_id is not None:
|
|
111
|
+
warning["request_id"] = error.request_id
|
|
112
|
+
if error.details:
|
|
113
|
+
warning["details"] = error.details
|
|
114
|
+
warnings.append(warning)
|
|
115
|
+
return {
|
|
116
|
+
"profile": profile,
|
|
117
|
+
"ws_id": session_profile.selected_ws_id,
|
|
118
|
+
"dash_key": dash_key,
|
|
119
|
+
"result": result,
|
|
120
|
+
"warnings": warnings,
|
|
121
|
+
"verification": verification,
|
|
122
|
+
}
|
|
94
123
|
|
|
95
124
|
return self._run(profile, runner)
|
|
96
125
|
|
|
@@ -156,3 +185,10 @@ class PortalTools(ToolBase):
|
|
|
156
185
|
"""执行内部辅助逻辑。"""
|
|
157
186
|
if not dash_key:
|
|
158
187
|
raise_tool_error(QingflowApiError.config_error("dash_key is required"))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_optional_portal_base_info_error(error: QingflowApiError) -> bool:
|
|
191
|
+
if is_auth_like_error(error):
|
|
192
|
+
return False
|
|
193
|
+
backend_code = backend_code_int(error)
|
|
194
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
@@ -7,7 +7,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
7
7
|
|
|
8
8
|
from ..backend_client import BackendRequestContext
|
|
9
9
|
from ..config import DEFAULT_PROFILE, normalize_base_url
|
|
10
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
10
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
11
11
|
from ..json_types import JSONObject, JSONValue
|
|
12
12
|
from .base import ToolBase, tool_cn_name
|
|
13
13
|
|
|
@@ -20,13 +20,27 @@ def _qingbi_base_url(base_url: str) -> str:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _should_retry_qflow_base(error: QingflowApiError) -> bool:
|
|
23
|
-
|
|
23
|
+
if is_auth_like_error(error):
|
|
24
|
+
return False
|
|
25
|
+
backend_code = backend_code_int(error)
|
|
26
|
+
http_status = getattr(error, "http_status", None)
|
|
27
|
+
return backend_code in {40002, 40027, 404, 81007} or http_status == 404
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
def _should_retry_asos_data(error: QingflowApiError) -> bool:
|
|
27
|
-
|
|
31
|
+
if is_auth_like_error(error):
|
|
32
|
+
return False
|
|
33
|
+
backend_code = backend_code_int(error)
|
|
28
34
|
http_status = getattr(error, "http_status", None)
|
|
29
|
-
return backend_code in {44011, 81007} or http_status == 404
|
|
35
|
+
return backend_code in {40002, 40027, 404, 44011, 81007} or http_status == 404
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _should_fallback_config_from_data(error: QingflowApiError) -> bool:
|
|
39
|
+
if is_auth_like_error(error):
|
|
40
|
+
return False
|
|
41
|
+
backend_code = backend_code_int(error)
|
|
42
|
+
http_status = getattr(error, "http_status", None)
|
|
43
|
+
return backend_code in {40002, 40027, 404, 81007} or http_status == 404
|
|
30
44
|
|
|
31
45
|
|
|
32
46
|
def _coerce_tool_error(error: RuntimeError | QingflowApiError) -> QingflowApiError | None:
|
|
@@ -177,13 +191,37 @@ class QingbiReportTools(ToolBase):
|
|
|
177
191
|
def qingbi_report_get_base(self, *, profile: str, chart_id: str) -> JSONObject:
|
|
178
192
|
"""执行工具方法逻辑。"""
|
|
179
193
|
self._require_chart_id(chart_id)
|
|
194
|
+
source_error: QingflowApiError | None = None
|
|
180
195
|
try:
|
|
181
|
-
return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
|
|
196
|
+
return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
|
|
182
197
|
except (QingflowApiError, RuntimeError) as raw_error:
|
|
183
198
|
error = _coerce_tool_error(raw_error)
|
|
184
199
|
if error is None or not _should_retry_qflow_base(error):
|
|
185
200
|
raise
|
|
186
|
-
|
|
201
|
+
source_error = error
|
|
202
|
+
try:
|
|
203
|
+
fallback = self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
|
|
204
|
+
except (QingflowApiError, RuntimeError):
|
|
205
|
+
raise
|
|
206
|
+
if not _has_chart_base_identity(fallback.get("result"), chart_id=chart_id):
|
|
207
|
+
raise source_error
|
|
208
|
+
fallback.setdefault(
|
|
209
|
+
"warnings",
|
|
210
|
+
[
|
|
211
|
+
{
|
|
212
|
+
"code": "CHART_BASE_INFO_FALLBACK_FROM_LEGACY",
|
|
213
|
+
"message": "qingbi_report_get_base used the legacy chart baseInfo route because qflow baseInfo was unavailable in this permission context.",
|
|
214
|
+
"backend_code": source_error.backend_code,
|
|
215
|
+
"http_status": source_error.http_status,
|
|
216
|
+
"request_id": source_error.request_id,
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
)
|
|
220
|
+
fallback.setdefault(
|
|
221
|
+
"verification",
|
|
222
|
+
{"qflow_base_route_loaded": False, "legacy_base_route_loaded": True},
|
|
223
|
+
)
|
|
224
|
+
return fallback
|
|
187
225
|
|
|
188
226
|
@tool_cn_name("更新报表基础信息")
|
|
189
227
|
def qingbi_report_update_base(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
|
|
@@ -196,7 +234,45 @@ class QingbiReportTools(ToolBase):
|
|
|
196
234
|
def qingbi_report_get_config(self, *, profile: str, chart_id: str) -> JSONObject:
|
|
197
235
|
"""执行工具方法逻辑。"""
|
|
198
236
|
self._require_chart_id(chart_id)
|
|
199
|
-
|
|
237
|
+
original_error: QingflowApiError | RuntimeError | None = None
|
|
238
|
+
try:
|
|
239
|
+
return self._request(profile, "GET", f"/qingbi/charts/{chart_id}/configs", chart_id=chart_id)
|
|
240
|
+
except (QingflowApiError, RuntimeError) as raw_error:
|
|
241
|
+
original_error = raw_error
|
|
242
|
+
error = _coerce_tool_error(raw_error)
|
|
243
|
+
if error is None or not _should_fallback_config_from_data(error):
|
|
244
|
+
raise
|
|
245
|
+
source_error = error
|
|
246
|
+
try:
|
|
247
|
+
data_payload = self.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={})
|
|
248
|
+
except (QingflowApiError, RuntimeError):
|
|
249
|
+
if original_error is not None:
|
|
250
|
+
raise original_error
|
|
251
|
+
raise
|
|
252
|
+
data_result = data_payload.get("result") if isinstance(data_payload, dict) else None
|
|
253
|
+
if isinstance(data_result, dict) and isinstance(data_result.get("config"), dict) and data_result["config"]:
|
|
254
|
+
return {
|
|
255
|
+
"profile": data_payload.get("profile", profile),
|
|
256
|
+
"ws_id": data_payload.get("ws_id"),
|
|
257
|
+
"chart_id": chart_id,
|
|
258
|
+
"result": data_result["config"],
|
|
259
|
+
"warnings": [
|
|
260
|
+
{
|
|
261
|
+
"code": "CHART_CONFIG_FALLBACK_FROM_DATA",
|
|
262
|
+
"message": "qingbi_report_get_config used config embedded in qflow chart data because the chart config endpoint was unavailable in this permission context.",
|
|
263
|
+
"backend_code": source_error.backend_code,
|
|
264
|
+
"http_status": source_error.http_status,
|
|
265
|
+
"request_id": source_error.request_id,
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"verification": {
|
|
269
|
+
"config_endpoint_loaded": False,
|
|
270
|
+
"data_config_loaded": True,
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
if original_error is not None:
|
|
274
|
+
raise original_error
|
|
275
|
+
raise_tool_error(QingflowApiError.config_error("chart config fallback did not return config"))
|
|
200
276
|
|
|
201
277
|
@tool_cn_name("更新报表配置")
|
|
202
278
|
def qingbi_report_update_config(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
|
|
@@ -251,6 +327,16 @@ class QingbiReportTools(ToolBase):
|
|
|
251
327
|
error = _coerce_tool_error(raw_error)
|
|
252
328
|
if error is None or not _should_retry_asos_data(error):
|
|
253
329
|
raise
|
|
330
|
+
warning: JSONObject = {
|
|
331
|
+
"code": "CHART_DATA_FALLBACK_FROM_ASOS",
|
|
332
|
+
"message": "qingbi_report_get_data used the asos data route because the qflow chart data route was unavailable in this permission context.",
|
|
333
|
+
}
|
|
334
|
+
if error.backend_code is not None:
|
|
335
|
+
warning["backend_code"] = error.backend_code
|
|
336
|
+
if error.http_status is not None:
|
|
337
|
+
warning["http_status"] = error.http_status
|
|
338
|
+
if error.request_id:
|
|
339
|
+
warning["request_id"] = error.request_id
|
|
254
340
|
return self._request(
|
|
255
341
|
profile,
|
|
256
342
|
"POST",
|
|
@@ -258,6 +344,11 @@ class QingbiReportTools(ToolBase):
|
|
|
258
344
|
chart_id=chart_id,
|
|
259
345
|
params=params,
|
|
260
346
|
json_body=payload or {},
|
|
347
|
+
warnings=[warning],
|
|
348
|
+
verification={
|
|
349
|
+
"qflow_data_route_loaded": False,
|
|
350
|
+
"asos_data_route_loaded": True,
|
|
351
|
+
},
|
|
261
352
|
)
|
|
262
353
|
|
|
263
354
|
@tool_cn_name("删除报表")
|
|
@@ -372,3 +463,21 @@ def _extract_sorted_chart_items(result: JSONValue) -> list[JSONObject]:
|
|
|
372
463
|
if isinstance(result, list):
|
|
373
464
|
return result
|
|
374
465
|
return []
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _has_chart_base_identity(result: JSONValue, *, chart_id: str) -> bool:
|
|
469
|
+
if not isinstance(result, dict):
|
|
470
|
+
return False
|
|
471
|
+
expected = str(chart_id or "").strip()
|
|
472
|
+
id_candidates = (
|
|
473
|
+
result.get("chartId"),
|
|
474
|
+
result.get("biChartId"),
|
|
475
|
+
result.get("id"),
|
|
476
|
+
result.get("chart_id"),
|
|
477
|
+
)
|
|
478
|
+
if expected and any(str(value or "").strip() == expected for value in id_candidates):
|
|
479
|
+
return True
|
|
480
|
+
return any(
|
|
481
|
+
str(result.get(key) or "").strip()
|
|
482
|
+
for key in ("chartName", "chart_name", "name", "title")
|
|
483
|
+
)
|