@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.3

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 (42) 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/builder_facade/models.py +14 -4
  13. package/src/qingflow_mcp/builder_facade/service.py +1582 -124
  14. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  15. package/src/qingflow_mcp/cli/commands/builder.py +4 -3
  16. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  17. package/src/qingflow_mcp/cli/commands/task.py +74 -22
  18. package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
  19. package/src/qingflow_mcp/cli/formatters.py +287 -48
  20. package/src/qingflow_mcp/cli/main.py +6 -1
  21. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  22. package/src/qingflow_mcp/config.py +1 -1
  23. package/src/qingflow_mcp/errors.py +2 -2
  24. package/src/qingflow_mcp/id_utils.py +49 -0
  25. package/src/qingflow_mcp/public_surface.py +11 -1
  26. package/src/qingflow_mcp/response_trim.py +380 -9
  27. package/src/qingflow_mcp/server.py +4 -0
  28. package/src/qingflow_mcp/server_app_builder.py +11 -1
  29. package/src/qingflow_mcp/server_app_user.py +24 -0
  30. package/src/qingflow_mcp/session_store.py +69 -15
  31. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  32. package/src/qingflow_mcp/solution/executor.py +2 -2
  33. package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
  34. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  35. package/src/qingflow_mcp/tools/auth_tools.py +217 -9
  36. package/src/qingflow_mcp/tools/base.py +6 -2
  37. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  38. package/src/qingflow_mcp/tools/import_tools.py +36 -2
  39. package/src/qingflow_mcp/tools/record_tools.py +410 -156
  40. package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
  41. package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
  42. 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("资源读取-门户列表")
@@ -153,9 +151,11 @@ class ResourceReadTools(ToolBase):
153
151
  or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
154
152
  )
155
153
  resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
154
+ if not resolved_app_key:
155
+ resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
156
156
  if not resolved_app_key:
157
157
  resolved_app_key = self._resolve_app_key_from_form_id(
158
- profile=profile,
158
+ context=context,
159
159
  form_id=_coerce_positive_int(base_info.get("formId") or config.get("formId")),
160
160
  )
161
161
  if not resolved_app_key:
@@ -198,25 +198,56 @@ class ResourceReadTools(ToolBase):
198
198
 
199
199
  def runner(session_profile, _context):
200
200
  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 {}
201
+ warnings: list[JSONObject] = []
202
+ verification = {
203
+ "chart_exists": True,
204
+ "chart_data_loaded": False,
205
+ "chart_config_loaded": False,
206
+ }
207
+ data: Any = None
208
+ data_config: dict[str, Any] = {}
209
+ try:
210
+ data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
211
+ verification["chart_data_loaded"] = True
212
+ if isinstance(data, dict) and isinstance(data.get("config"), dict):
213
+ data_config = deepcopy(data.get("config"))
214
+ verification["chart_config_loaded"] = True
215
+ except (QingflowApiError, RuntimeError) as error:
216
+ api_error = error if isinstance(error, QingflowApiError) else None
217
+ warnings.append(
218
+ {
219
+ "code": "CHART_DATA_UNAVAILABLE",
220
+ "message": "chart_get could not load chart data; returning chart metadata and config when available.",
221
+ "backend_code": api_error.backend_code if api_error is not None else None,
222
+ "http_status": api_error.http_status if api_error is not None else None,
223
+ }
224
+ )
225
+ if not data_config:
226
+ try:
227
+ config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
228
+ except (QingflowApiError, RuntimeError):
229
+ config_result = {}
230
+ if isinstance(config_result, dict) and config_result:
231
+ data_config = deepcopy(config_result)
232
+ verification["chart_config_loaded"] = True
233
+ data_payload: dict[str, Any] = {
234
+ "chart_id": chart_id,
235
+ "chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
236
+ "chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
237
+ "data_source_type": str(base.get("dataSourceType") or "").strip() or None,
238
+ "data_source_id": str(base.get("dataSourceId") or "").strip() or None,
239
+ }
240
+ if verification["chart_data_loaded"]:
241
+ data_payload["data"] = deepcopy(data) if isinstance(data, dict) else {"value": data}
242
+ if data_config and not verification["chart_data_loaded"]:
243
+ data_payload["config"] = deepcopy(data_config)
203
244
  return {
204
245
  "profile": profile,
205
246
  "ws_id": session_profile.selected_ws_id,
206
247
  "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
- },
248
+ "warnings": warnings,
249
+ "verification": verification,
250
+ "data": data_payload,
220
251
  }
221
252
 
222
253
  return self._run(profile, runner)
@@ -236,25 +267,54 @@ class ResourceReadTools(ToolBase):
236
267
  if not chart_id:
237
268
  raise_tool_error(QingflowApiError.config_error("chart_id is required"))
238
269
 
239
- def _resolve_app_key_from_form_id(self, *, profile: str, form_id: int | None) -> str | None:
270
+ def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
271
+ try:
272
+ form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
273
+ except QingflowApiError:
274
+ return None
275
+ if not isinstance(form_payload, dict):
276
+ return None
277
+ app_key = str(form_payload.get("appKey") or "").strip()
278
+ return app_key or None
279
+
280
+ def _resolve_app_key_from_form_id(self, *, context: Any, form_id: int | None) -> str | None:
240
281
  """执行内部辅助逻辑。"""
241
282
  if form_id is None:
242
283
  return None
243
284
  try:
244
- payload = self.apps.app_list(profile=profile)
285
+ payload = self.backend.request("GET", context, "/tag/apps")
245
286
  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
287
+ payload = None
288
+ app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
289
+ if app_key:
290
+ return app_key
291
+ page_num = 1
292
+ page_size = 200
293
+ while page_num <= 20:
294
+ try:
295
+ page = self.backend.request(
296
+ "GET",
297
+ context,
298
+ "/app/item",
299
+ params={"pageNum": page_num, "pageSize": page_size},
300
+ )
301
+ except QingflowApiError:
302
+ return None
303
+ items = page.get("list") if isinstance(page, dict) else []
304
+ if not isinstance(items, list) or not items:
305
+ return None
306
+ for item in items:
307
+ if not isinstance(item, dict):
308
+ continue
309
+ if _coerce_positive_int(item.get("formId") or item.get("form_id")) != form_id:
310
+ continue
311
+ app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
312
+ if app_key:
313
+ return app_key
314
+ total = _coerce_positive_int(page.get("total")) if isinstance(page, dict) else None
315
+ if total is not None and page_num * page_size >= total:
316
+ break
317
+ page_num += 1
258
318
  return None
259
319
 
260
320
 
@@ -419,3 +479,25 @@ def _coerce_positive_int(value: Any) -> int | None:
419
479
  except (TypeError, ValueError):
420
480
  return None
421
481
  return number if number > 0 else None
482
+
483
+
484
+ def _find_visible_app_key_by_form_id(payload: Any, *, form_id: int) -> str | None:
485
+ if isinstance(payload, list):
486
+ for item in payload:
487
+ resolved = _find_visible_app_key_by_form_id(item, form_id=form_id)
488
+ if resolved:
489
+ return resolved
490
+ return None
491
+ if not isinstance(payload, dict):
492
+ return None
493
+ candidate_form_id = _coerce_positive_int(payload.get("formId") or payload.get("form_id"))
494
+ if candidate_form_id == form_id:
495
+ app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
496
+ if app_key:
497
+ return app_key
498
+ for value in payload.values():
499
+ if isinstance(value, (list, dict)):
500
+ resolved = _find_visible_app_key_by_form_id(value, form_id=form_id)
501
+ if resolved:
502
+ return resolved
503
+ return None