@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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 (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -1,6 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from threading import Event
5
+ from time import sleep
6
+ from typing import Any
4
7
  from uuid import uuid4
5
8
 
6
9
  import httpx
@@ -201,6 +204,203 @@ class BackendClient:
201
204
  "body": body,
202
205
  }
203
206
 
207
+ def download_binary(self, url: str, *, headers: dict[str, str] | None = None) -> bytes:
208
+ try:
209
+ response = self._client.get(url, headers=headers or None)
210
+ except httpx.RequestError as exc:
211
+ raise QingflowApiError(category="network", message=str(exc))
212
+ if response.status_code >= 400:
213
+ raise QingflowApiError(
214
+ category="http",
215
+ message=self._extract_message(response.text) or f"HTTP {response.status_code}",
216
+ http_status=response.status_code,
217
+ )
218
+ return response.content
219
+
220
+ def request_multipart(
221
+ self,
222
+ method: str,
223
+ context: BackendRequestContext,
224
+ path: str,
225
+ *,
226
+ data: dict[str, Any] | None = None,
227
+ files: dict[str, tuple[str, bytes, str | None]] | None = None,
228
+ unwrap: bool = True,
229
+ ) -> JSONValue:
230
+ return self.request_multipart_with_meta(
231
+ method,
232
+ context,
233
+ path,
234
+ data=data,
235
+ files=files,
236
+ unwrap=unwrap,
237
+ ).data
238
+
239
+ def request_multipart_with_meta(
240
+ self,
241
+ method: str,
242
+ context: BackendRequestContext,
243
+ path: str,
244
+ *,
245
+ data: dict[str, Any] | None = None,
246
+ files: dict[str, tuple[str, bytes, str | None]] | None = None,
247
+ unwrap: bool = True,
248
+ ) -> BackendResponse:
249
+ headers = self._base_headers(
250
+ context.token,
251
+ context.ws_id,
252
+ context.qf_request_id,
253
+ qf_version=context.qf_version,
254
+ )
255
+ request_files: dict[str, tuple[str, bytes, str | None]] | None = None
256
+ if files:
257
+ request_files = {key: value for key, value in files.items()}
258
+ try:
259
+ response = self._client.request(
260
+ method.upper(),
261
+ self._build_url(context.base_url, path),
262
+ data=data,
263
+ files=request_files,
264
+ headers=headers,
265
+ )
266
+ except httpx.RequestError as exc:
267
+ raise QingflowApiError(category="network", message=str(exc), request_id=headers["Qf-Request-Id"])
268
+ parsed = self._parse_response(response, headers["Qf-Request-Id"], unwrap=unwrap)
269
+ return BackendResponse(
270
+ data=parsed,
271
+ headers=dict(response.headers),
272
+ request_id=headers["Qf-Request-Id"],
273
+ http_status=response.status_code,
274
+ qf_response_version=self._extract_response_qf_version(response.headers),
275
+ )
276
+
277
+ def start_socket_data_import(
278
+ self,
279
+ context: BackendRequestContext,
280
+ *,
281
+ app_key: str,
282
+ being_enter_auditing: bool,
283
+ view_key: str | None,
284
+ excel_url: str,
285
+ excel_name: str,
286
+ ack_timeout_seconds: float = 8.0,
287
+ initial_wait_seconds: float = 4.0,
288
+ ) -> dict[str, Any]:
289
+ try:
290
+ import socketio # type: ignore[import-not-found]
291
+ except ImportError as exc:
292
+ raise QingflowApiError(
293
+ category="config",
294
+ message=f"socket.io client dependency is missing: {exc}",
295
+ )
296
+
297
+ socket_base_url = self._build_socket_base_url(context.base_url)
298
+ import_result: dict[str, Any] = {
299
+ "import_id": None,
300
+ "process_id_str": None,
301
+ "status": "accepted",
302
+ "warnings": [],
303
+ "initial_event": None,
304
+ "failure_event": None,
305
+ }
306
+ initial_event_received = Event()
307
+ failure_event_received = Event()
308
+ sio = socketio.Client(reconnection=False, logger=False, engineio_logger=False)
309
+
310
+ def _unwrap_socket_event_payload(payload: Any) -> Any:
311
+ if isinstance(payload, list) and payload:
312
+ return payload[0]
313
+ return payload
314
+
315
+ def _handle_initial(payload: Any) -> None:
316
+ normalized_payload = _unwrap_socket_event_payload(payload)
317
+ import_result["initial_event"] = normalized_payload
318
+ if isinstance(normalized_payload, dict):
319
+ process_id = normalized_payload.get("processIdStr") or normalized_payload.get("process_id_str") or normalized_payload.get("processId")
320
+ if process_id is not None:
321
+ import_result["process_id_str"] = str(process_id)
322
+ initial_event_received.set()
323
+
324
+ def _handle_failure(payload: Any) -> None:
325
+ normalized_payload = _unwrap_socket_event_payload(payload)
326
+ import_result["failure_event"] = normalized_payload
327
+ if isinstance(normalized_payload, dict):
328
+ process_id = normalized_payload.get("processIdStr") or normalized_payload.get("process_id_str") or normalized_payload.get("processId")
329
+ if process_id is not None:
330
+ import_result["process_id_str"] = str(process_id)
331
+ failure_event_received.set()
332
+
333
+ try:
334
+ sio.connect(
335
+ socket_base_url,
336
+ transports=["websocket"],
337
+ socketio_path="socket.io",
338
+ headers=self._base_headers(
339
+ context.token,
340
+ context.ws_id,
341
+ qf_version=context.qf_version,
342
+ ),
343
+ wait_timeout=ack_timeout_seconds,
344
+ )
345
+ sio.emit("token", context.token)
346
+ sleep(0.2)
347
+ ack = sio.call(
348
+ "dataImport",
349
+ (
350
+ context.token,
351
+ app_key,
352
+ bool(being_enter_auditing),
353
+ view_key,
354
+ False,
355
+ excel_url,
356
+ excel_name,
357
+ ),
358
+ timeout=ack_timeout_seconds,
359
+ )
360
+ ack_payload = ack[0] if isinstance(ack, list) and ack else ack
361
+ if isinstance(ack_payload, dict):
362
+ error_code = ack_payload.get("error")
363
+ ack_message = ack_payload.get("message")
364
+ import_id = ack_payload.get("data")
365
+ if error_code not in (None, 0):
366
+ raise QingflowApiError(
367
+ category="backend",
368
+ message=str(ack_message or f"socket import rejected with error {error_code}"),
369
+ details={"socket_error_code": error_code, "import_id": import_id},
370
+ )
371
+ else:
372
+ import_id = ack_payload
373
+ if not import_id:
374
+ raise QingflowApiError(category="backend", message="socket import ack did not return import_id")
375
+ import_result["import_id"] = str(import_id)
376
+ sio.on(f"dataImportRes_{import_result['import_id']}", _handle_initial)
377
+ sio.on(f"dataImportFail_{import_result['import_id']}", _handle_failure)
378
+ if not initial_event_received.wait(timeout=initial_wait_seconds) and not failure_event_received.wait(timeout=0.1):
379
+ import_result["warnings"].append(
380
+ {
381
+ "code": "IMPORT_SOCKET_INITIAL_EVENT_PENDING",
382
+ "message": "Import ack received, but no initial progress payload arrived within the initial wait window.",
383
+ }
384
+ )
385
+ except Exception as exc:
386
+ message = str(exc)
387
+ if "timeout" in message.lower():
388
+ raise QingflowApiError(
389
+ category="network",
390
+ message="socket import ack timed out",
391
+ details={"error_code": "IMPORT_SOCKET_ACK_TIMEOUT"},
392
+ )
393
+ if isinstance(exc, QingflowApiError):
394
+ raise
395
+ raise QingflowApiError(category="network", message=message or "socket import failed")
396
+ finally:
397
+ try:
398
+ if sio.connected:
399
+ sio.disconnect()
400
+ except Exception:
401
+ pass
402
+ return import_result
403
+
204
404
  def _request_with_meta(
205
405
  self,
206
406
  method: str,
@@ -334,3 +534,13 @@ class BackendClient:
334
534
  if not normalized:
335
535
  raise QingflowApiError.config_error("base_url is required")
336
536
  return f"{normalized}/{path.lstrip('/')}"
537
+
538
+ def _build_socket_base_url(self, base_url: str) -> str:
539
+ normalized = normalize_base_url(base_url)
540
+ if not normalized:
541
+ raise QingflowApiError.config_error("base_url is required")
542
+ parsed = urlsplit(normalized)
543
+ path = parsed.path.rstrip("/")
544
+ if path.endswith("/api"):
545
+ path = path[:-4]
546
+ return urlunsplit((parsed.scheme, parsed.netloc, path or "", "", ""))