@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.
- package/README.md +5 -3
- package/docs/local-agent-install.md +21 -5
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/lib/runtime.mjs +168 -12
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/skills/qingflow-app-builder/SKILL.md +155 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +210 -0
- package/src/qingflow_mcp/builder_facade/models.py +1252 -3
- package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +515 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/record.py +304 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +355 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +149 -0
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server.py +160 -18
- package/src/qingflow_mcp/server_app_builder.py +275 -68
- package/src/qingflow_mcp/server_app_user.py +219 -191
- package/src/qingflow_mcp/session_store.py +41 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +107 -11
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
- package/src/qingflow_mcp/tools/app_tools.py +419 -12
- package/src/qingflow_mcp/tools/approval_tools.py +571 -72
- package/src/qingflow_mcp/tools/auth_tools.py +398 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +2150 -0
- package/src/qingflow_mcp/tools/package_tools.py +18 -4
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
- package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- 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 "", "", ""))
|