@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.4
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 +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +88 -184
- package/skills/qingflow-app-builder/references/create-app.md +15 -34
- package/skills/qingflow-app-builder/references/gotchas.md +3 -3
- package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/backend_client.py +109 -0
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +58 -9
- package/src/qingflow_mcp/builder_facade/service.py +1711 -240
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +11 -3
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +701 -27
- package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +424 -50
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +11 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +4 -4
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +16 -1
- package/src/qingflow_mcp/response_trim.py +394 -9
- package/src/qingflow_mcp/server.py +26 -0
- package/src/qingflow_mcp/server_app_builder.py +15 -1
- package/src/qingflow_mcp/server_app_user.py +113 -0
- package/src/qingflow_mcp/session_store.py +126 -21
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +243 -9
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +78 -4
- package/src/qingflow_mcp/tools/record_tools.py +551 -165
- package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
- package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -7,7 +7,6 @@ from ..errors import QingflowApiError, raise_tool_error
|
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
|
|
9
9
|
from .app_tools import _analysis_supported_for_view_type
|
|
10
|
-
from .app_tools import AppTools
|
|
11
10
|
from .base import ToolBase, tool_cn_name
|
|
12
11
|
from .qingbi_report_tools import QingbiReportTools
|
|
13
12
|
|
|
@@ -25,7 +24,6 @@ class ResourceReadTools(ToolBase):
|
|
|
25
24
|
def __init__(self, sessions, backend) -> None:
|
|
26
25
|
"""执行内部辅助逻辑。"""
|
|
27
26
|
super().__init__(sessions, backend)
|
|
28
|
-
self.apps = AppTools(sessions, backend)
|
|
29
27
|
self.charts = QingbiReportTools(sessions, backend)
|
|
30
28
|
|
|
31
29
|
@tool_cn_name("资源读取-门户列表")
|
|
@@ -98,6 +96,9 @@ class ResourceReadTools(ToolBase):
|
|
|
98
96
|
)
|
|
99
97
|
)
|
|
100
98
|
if system_view is not None:
|
|
99
|
+
export_capability = _view_export_capability_payload(
|
|
100
|
+
supported=_export_supported_for_view_type(system_view["view_type"])
|
|
101
|
+
)
|
|
101
102
|
return self._run(
|
|
102
103
|
profile,
|
|
103
104
|
lambda session_profile, _context: {
|
|
@@ -108,11 +109,17 @@ class ResourceReadTools(ToolBase):
|
|
|
108
109
|
{
|
|
109
110
|
"code": "VIEW_APP_KEY_UNRESOLVED",
|
|
110
111
|
"message": f"view_get could not resolve app_key for system view `{view_id}`; keep using the app_key from the parent app context.",
|
|
111
|
-
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
115
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs the parent app context `app_key`.",
|
|
116
|
+
},
|
|
112
117
|
],
|
|
113
118
|
"verification": {
|
|
114
119
|
"view_exists": True,
|
|
115
120
|
"descriptor_only": True,
|
|
121
|
+
"export_route_supported": export_capability["supported"],
|
|
122
|
+
"export_permission_verified": export_capability["permission_verified"],
|
|
116
123
|
},
|
|
117
124
|
"data": {
|
|
118
125
|
"app_key": None,
|
|
@@ -122,11 +129,13 @@ class ResourceReadTools(ToolBase):
|
|
|
122
129
|
"view_type": system_view["view_type"],
|
|
123
130
|
"visible_columns": [],
|
|
124
131
|
"analysis_supported": system_view["analysis_supported"],
|
|
132
|
+
"export_capability": export_capability,
|
|
125
133
|
},
|
|
126
134
|
},
|
|
127
135
|
)
|
|
128
136
|
|
|
129
137
|
def runner(session_profile, context):
|
|
138
|
+
raw_view_type = None
|
|
130
139
|
warnings: list[JSONObject] = []
|
|
131
140
|
verification = {
|
|
132
141
|
"view_exists": True,
|
|
@@ -152,10 +161,17 @@ class ResourceReadTools(ToolBase):
|
|
|
152
161
|
str(base_info.get("viewgraphType") or "").strip()
|
|
153
162
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
154
163
|
)
|
|
164
|
+
export_capability = _view_export_capability_payload(
|
|
165
|
+
supported=_export_supported_for_view_type(raw_view_type or None)
|
|
166
|
+
)
|
|
167
|
+
verification["export_route_supported"] = export_capability["supported"]
|
|
168
|
+
verification["export_permission_verified"] = export_capability["permission_verified"]
|
|
155
169
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
170
|
+
if not resolved_app_key:
|
|
171
|
+
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
156
172
|
if not resolved_app_key:
|
|
157
173
|
resolved_app_key = self._resolve_app_key_from_form_id(
|
|
158
|
-
|
|
174
|
+
context=context,
|
|
159
175
|
form_id=_coerce_positive_int(base_info.get("formId") or config.get("formId")),
|
|
160
176
|
)
|
|
161
177
|
if not resolved_app_key:
|
|
@@ -165,6 +181,13 @@ class ResourceReadTools(ToolBase):
|
|
|
165
181
|
"message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
|
|
166
182
|
}
|
|
167
183
|
)
|
|
184
|
+
if export_capability["supported"]:
|
|
185
|
+
warnings.append(
|
|
186
|
+
{
|
|
187
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
188
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
168
191
|
return {
|
|
169
192
|
"profile": profile,
|
|
170
193
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -186,6 +209,7 @@ class ResourceReadTools(ToolBase):
|
|
|
186
209
|
if str(item.get("queTitle") or item.get("title") or "").strip()
|
|
187
210
|
],
|
|
188
211
|
"analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
|
|
212
|
+
"export_capability": export_capability,
|
|
189
213
|
},
|
|
190
214
|
}
|
|
191
215
|
|
|
@@ -198,25 +222,56 @@ class ResourceReadTools(ToolBase):
|
|
|
198
222
|
|
|
199
223
|
def runner(session_profile, _context):
|
|
200
224
|
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
201
|
-
|
|
202
|
-
|
|
225
|
+
warnings: list[JSONObject] = []
|
|
226
|
+
verification = {
|
|
227
|
+
"chart_exists": True,
|
|
228
|
+
"chart_data_loaded": False,
|
|
229
|
+
"chart_config_loaded": False,
|
|
230
|
+
}
|
|
231
|
+
data: Any = None
|
|
232
|
+
data_config: dict[str, Any] = {}
|
|
233
|
+
try:
|
|
234
|
+
data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
|
|
235
|
+
verification["chart_data_loaded"] = True
|
|
236
|
+
if isinstance(data, dict) and isinstance(data.get("config"), dict):
|
|
237
|
+
data_config = deepcopy(data.get("config"))
|
|
238
|
+
verification["chart_config_loaded"] = True
|
|
239
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
240
|
+
api_error = error if isinstance(error, QingflowApiError) else None
|
|
241
|
+
warnings.append(
|
|
242
|
+
{
|
|
243
|
+
"code": "CHART_DATA_UNAVAILABLE",
|
|
244
|
+
"message": "chart_get could not load chart data; returning chart metadata and config when available.",
|
|
245
|
+
"backend_code": api_error.backend_code if api_error is not None else None,
|
|
246
|
+
"http_status": api_error.http_status if api_error is not None else None,
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
if not data_config:
|
|
250
|
+
try:
|
|
251
|
+
config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
252
|
+
except (QingflowApiError, RuntimeError):
|
|
253
|
+
config_result = {}
|
|
254
|
+
if isinstance(config_result, dict) and config_result:
|
|
255
|
+
data_config = deepcopy(config_result)
|
|
256
|
+
verification["chart_config_loaded"] = True
|
|
257
|
+
data_payload: dict[str, Any] = {
|
|
258
|
+
"chart_id": chart_id,
|
|
259
|
+
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
260
|
+
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
261
|
+
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
262
|
+
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
263
|
+
}
|
|
264
|
+
if verification["chart_data_loaded"]:
|
|
265
|
+
data_payload["data"] = deepcopy(data) if isinstance(data, dict) else {"value": data}
|
|
266
|
+
if data_config and not verification["chart_data_loaded"]:
|
|
267
|
+
data_payload["config"] = deepcopy(data_config)
|
|
203
268
|
return {
|
|
204
269
|
"profile": profile,
|
|
205
270
|
"ws_id": session_profile.selected_ws_id,
|
|
206
271
|
"ok": True,
|
|
207
|
-
"warnings":
|
|
208
|
-
"verification":
|
|
209
|
-
|
|
210
|
-
"chart_data_loaded": True,
|
|
211
|
-
},
|
|
212
|
-
"data": {
|
|
213
|
-
"chart_id": chart_id,
|
|
214
|
-
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
215
|
-
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
216
|
-
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
217
|
-
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
218
|
-
"data": deepcopy(data) if isinstance(data, dict) else {"value": data},
|
|
219
|
-
},
|
|
272
|
+
"warnings": warnings,
|
|
273
|
+
"verification": verification,
|
|
274
|
+
"data": data_payload,
|
|
220
275
|
}
|
|
221
276
|
|
|
222
277
|
return self._run(profile, runner)
|
|
@@ -236,25 +291,54 @@ class ResourceReadTools(ToolBase):
|
|
|
236
291
|
if not chart_id:
|
|
237
292
|
raise_tool_error(QingflowApiError.config_error("chart_id is required"))
|
|
238
293
|
|
|
239
|
-
def
|
|
294
|
+
def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
|
|
295
|
+
try:
|
|
296
|
+
form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
|
|
297
|
+
except QingflowApiError:
|
|
298
|
+
return None
|
|
299
|
+
if not isinstance(form_payload, dict):
|
|
300
|
+
return None
|
|
301
|
+
app_key = str(form_payload.get("appKey") or "").strip()
|
|
302
|
+
return app_key or None
|
|
303
|
+
|
|
304
|
+
def _resolve_app_key_from_form_id(self, *, context: Any, form_id: int | None) -> str | None:
|
|
240
305
|
"""执行内部辅助逻辑。"""
|
|
241
306
|
if form_id is None:
|
|
242
307
|
return None
|
|
243
308
|
try:
|
|
244
|
-
payload = self.
|
|
309
|
+
payload = self.backend.request("GET", context, "/tag/apps")
|
|
245
310
|
except QingflowApiError:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
311
|
+
payload = None
|
|
312
|
+
app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
|
|
313
|
+
if app_key:
|
|
314
|
+
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
|
|
258
342
|
return None
|
|
259
343
|
|
|
260
344
|
|
|
@@ -404,6 +488,21 @@ def _lookup_system_view_descriptor(view_id: str) -> dict[str, Any] | None:
|
|
|
404
488
|
return None
|
|
405
489
|
|
|
406
490
|
|
|
491
|
+
def _view_export_capability_payload(*, supported: bool) -> JSONObject:
|
|
492
|
+
return {
|
|
493
|
+
"supported": supported,
|
|
494
|
+
"tool": "record_export_start",
|
|
495
|
+
"format": "xlsx",
|
|
496
|
+
"async": True,
|
|
497
|
+
"requires_app_key": True,
|
|
498
|
+
"permission_verified": False,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _export_supported_for_view_type(view_type: str | None) -> bool:
|
|
503
|
+
return _analysis_supported_for_view_type(view_type)
|
|
504
|
+
|
|
505
|
+
|
|
407
506
|
def _normalize_view_type(view_type: Any) -> str | None:
|
|
408
507
|
value = str(view_type or "").strip()
|
|
409
508
|
if not value:
|
|
@@ -419,3 +518,25 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
419
518
|
except (TypeError, ValueError):
|
|
420
519
|
return None
|
|
421
520
|
return number if number > 0 else None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _find_visible_app_key_by_form_id(payload: Any, *, form_id: int) -> str | None:
|
|
524
|
+
if isinstance(payload, list):
|
|
525
|
+
for item in payload:
|
|
526
|
+
resolved = _find_visible_app_key_by_form_id(item, form_id=form_id)
|
|
527
|
+
if resolved:
|
|
528
|
+
return resolved
|
|
529
|
+
return None
|
|
530
|
+
if not isinstance(payload, dict):
|
|
531
|
+
return None
|
|
532
|
+
candidate_form_id = _coerce_positive_int(payload.get("formId") or payload.get("form_id"))
|
|
533
|
+
if candidate_form_id == form_id:
|
|
534
|
+
app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
|
|
535
|
+
if app_key:
|
|
536
|
+
return app_key
|
|
537
|
+
for value in payload.values():
|
|
538
|
+
if isinstance(value, (list, dict)):
|
|
539
|
+
resolved = _find_visible_app_key_by_form_id(value, form_id=form_id)
|
|
540
|
+
if resolved:
|
|
541
|
+
return resolved
|
|
542
|
+
return None
|