@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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-builder/SKILL.md +88 -184
  7. package/skills/qingflow-app-builder/references/create-app.md +15 -34
  8. package/skills/qingflow-app-builder/references/gotchas.md +3 -3
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
  10. package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
  11. package/src/qingflow_mcp/__init__.py +33 -1
  12. package/src/qingflow_mcp/backend_client.py +109 -0
  13. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +58 -9
  15. package/src/qingflow_mcp/builder_facade/service.py +1711 -240
  16. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  17. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  18. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  19. package/src/qingflow_mcp/cli/commands/builder.py +11 -3
  20. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  21. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  22. package/src/qingflow_mcp/cli/commands/task.py +701 -27
  23. package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
  24. package/src/qingflow_mcp/cli/context.py +3 -0
  25. package/src/qingflow_mcp/cli/formatters.py +424 -50
  26. package/src/qingflow_mcp/cli/interaction.py +72 -0
  27. package/src/qingflow_mcp/cli/main.py +11 -1
  28. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  29. package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
  30. package/src/qingflow_mcp/config.py +1 -1
  31. package/src/qingflow_mcp/errors.py +4 -4
  32. package/src/qingflow_mcp/export_store.py +14 -0
  33. package/src/qingflow_mcp/id_utils.py +49 -0
  34. package/src/qingflow_mcp/public_surface.py +16 -1
  35. package/src/qingflow_mcp/response_trim.py +394 -9
  36. package/src/qingflow_mcp/server.py +26 -0
  37. package/src/qingflow_mcp/server_app_builder.py +15 -1
  38. package/src/qingflow_mcp/server_app_user.py +113 -0
  39. package/src/qingflow_mcp/session_store.py +126 -21
  40. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  41. package/src/qingflow_mcp/solution/executor.py +2 -2
  42. package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
  43. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  44. package/src/qingflow_mcp/tools/auth_tools.py +243 -9
  45. package/src/qingflow_mcp/tools/base.py +6 -2
  46. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  47. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  48. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  49. package/src/qingflow_mcp/tools/import_tools.py +78 -4
  50. package/src/qingflow_mcp/tools/record_tools.py +551 -165
  51. package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
  52. package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
  53. 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
- profile=profile,
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
- data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
202
- data_config = data.get("config") if isinstance(data, dict) and isinstance(data.get("config"), dict) else {}
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
- "chart_exists": True,
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 _resolve_app_key_from_form_id(self, *, profile: str, form_id: int | None) -> str | None:
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.apps.app_list(profile=profile)
309
+ payload = self.backend.request("GET", context, "/tag/apps")
245
310
  except QingflowApiError:
246
- return None
247
- items = payload.get("items")
248
- if not isinstance(items, list):
249
- return None
250
- for item in items:
251
- if not isinstance(item, dict):
252
- continue
253
- if _coerce_positive_int(item.get("form_id")) != form_id:
254
- continue
255
- app_key = str(item.get("app_key") or "").strip()
256
- if app_key:
257
- return app_key
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