@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
|
@@ -263,6 +263,15 @@ def _trim_workspace_list(payload: JSONObject) -> None:
|
|
|
263
263
|
_trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
|
|
264
264
|
|
|
265
265
|
|
|
266
|
+
def _trim_workspace_get(payload: JSONObject) -> None:
|
|
267
|
+
workspace = payload.get("workspace")
|
|
268
|
+
if isinstance(workspace, dict):
|
|
269
|
+
payload["workspace"] = _pick(
|
|
270
|
+
workspace,
|
|
271
|
+
allowed=("wsId", "workspaceName", "remark", "systemVersion", "auth"),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
266
275
|
def _trim_app_search_like(payload: JSONObject) -> None:
|
|
267
276
|
payload.pop("apps", None)
|
|
268
277
|
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
|
|
@@ -287,34 +296,175 @@ def _trim_file_upload_local(payload: JSONObject) -> None:
|
|
|
287
296
|
|
|
288
297
|
|
|
289
298
|
def _trim_import_schema(payload: JSONObject) -> None:
|
|
290
|
-
|
|
299
|
+
columns: list[JSONObject] | None = None
|
|
300
|
+
if isinstance(payload.get("columns"), list):
|
|
301
|
+
columns = [item for item in payload.get("columns", []) if isinstance(item, dict)]
|
|
302
|
+
elif isinstance(payload.get("expected_columns"), list):
|
|
303
|
+
columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
|
|
304
|
+
if columns is not None:
|
|
305
|
+
payload["columns"] = [_compact_import_column(item) for item in columns]
|
|
306
|
+
payload.pop("expected_columns", None)
|
|
307
|
+
payload.pop("schema_fingerprint", None)
|
|
308
|
+
payload.pop("import_capability", None)
|
|
309
|
+
payload.pop("request_route", None)
|
|
310
|
+
payload.pop("verification", None)
|
|
311
|
+
|
|
312
|
+
if _looks_like_import_verify(payload):
|
|
313
|
+
_trim_import_verify_payload(payload)
|
|
314
|
+
return
|
|
315
|
+
if "applied_repairs" in payload or "repaired_file_path" in payload:
|
|
316
|
+
_trim_import_repair_payload(payload)
|
|
317
|
+
return
|
|
318
|
+
if "template_url" in payload or "downloaded_to_path" in payload:
|
|
319
|
+
_trim_import_template_payload(payload)
|
|
320
|
+
return
|
|
321
|
+
if "import_id" in payload or "process_id_str" in payload:
|
|
322
|
+
_trim_import_status_payload(payload)
|
|
323
|
+
return
|
|
291
324
|
|
|
292
325
|
|
|
293
326
|
def _trim_record_schema(payload: JSONObject) -> None:
|
|
294
327
|
payload.pop("legacy_schema", None)
|
|
328
|
+
template_map = payload.get("payload_template")
|
|
329
|
+
if not isinstance(template_map, dict):
|
|
330
|
+
template_map = None
|
|
331
|
+
|
|
332
|
+
if "writable_fields" in payload:
|
|
333
|
+
writable_fields = payload.get("writable_fields")
|
|
334
|
+
payload.pop("writable_fields", None)
|
|
335
|
+
required_fields: list[JSONObject] = []
|
|
336
|
+
optional_fields: list[JSONObject] = []
|
|
337
|
+
if isinstance(writable_fields, list):
|
|
338
|
+
for item in writable_fields:
|
|
339
|
+
compact = _compact_schema_field(item, template_map=template_map)
|
|
340
|
+
if not compact:
|
|
341
|
+
continue
|
|
342
|
+
if compact.get("required") is True:
|
|
343
|
+
required_fields.append(compact)
|
|
344
|
+
else:
|
|
345
|
+
optional_fields.append(compact)
|
|
346
|
+
payload["required_fields"] = required_fields
|
|
347
|
+
payload["optional_fields"] = optional_fields
|
|
348
|
+
|
|
349
|
+
for key in ("required_fields", "optional_fields", "runtime_linked_required_fields", "fields", "ambiguous_fields"):
|
|
350
|
+
if key in payload:
|
|
351
|
+
payload[key] = _compact_schema_fields(payload.get(key), template_map=template_map)
|
|
352
|
+
|
|
353
|
+
for key in ("suggested_dimensions", "suggested_metrics", "suggested_time_fields"):
|
|
354
|
+
if isinstance(payload.get(key), list):
|
|
355
|
+
payload[key] = [
|
|
356
|
+
_pick(item, ("field_id", "title")) for item in payload.get(key) if isinstance(item, dict)
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
for key in ("workflow_node", "view_resolution", "field_count"):
|
|
360
|
+
payload.pop(key, None)
|
|
295
361
|
|
|
296
362
|
|
|
297
363
|
def _trim_record_write(payload: JSONObject) -> None:
|
|
364
|
+
payload.pop("verification", None)
|
|
298
365
|
data = payload.get("data")
|
|
299
366
|
if not isinstance(data, dict):
|
|
300
367
|
return
|
|
368
|
+
data.pop("debug", None)
|
|
301
369
|
data.pop("normalized_payload", None)
|
|
302
370
|
data.pop("human_review", None)
|
|
303
371
|
data.pop("action", None)
|
|
372
|
+
resource = _compact_record_resource(data.get("resource"))
|
|
373
|
+
if resource:
|
|
374
|
+
data["resource"] = resource
|
|
375
|
+
else:
|
|
376
|
+
data.pop("resource", None)
|
|
377
|
+
verification = data.get("verification")
|
|
378
|
+
if isinstance(verification, dict):
|
|
379
|
+
compact_verification = _pick(
|
|
380
|
+
verification,
|
|
381
|
+
(
|
|
382
|
+
"verified",
|
|
383
|
+
"verification_mode",
|
|
384
|
+
"field_level_verified",
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
if compact_verification:
|
|
388
|
+
data["verification"] = compact_verification
|
|
389
|
+
else:
|
|
390
|
+
data.pop("verification", None)
|
|
391
|
+
for key in ("blockers", "field_errors", "confirmation_requests", "resolved_fields"):
|
|
392
|
+
value = data.get(key)
|
|
393
|
+
if value in (None, [], {}, ""):
|
|
394
|
+
data.pop(key, None)
|
|
304
395
|
|
|
305
396
|
|
|
306
397
|
def _trim_record_get(payload: JSONObject) -> None:
|
|
307
|
-
|
|
308
|
-
|
|
398
|
+
data = payload.get("data")
|
|
399
|
+
if not isinstance(data, dict):
|
|
400
|
+
return
|
|
401
|
+
compact: dict[str, Any] = {}
|
|
402
|
+
app_key = data.get("app_key")
|
|
403
|
+
if app_key:
|
|
404
|
+
compact["app_key"] = app_key
|
|
405
|
+
record_id = data.get("record_id")
|
|
406
|
+
if record_id not in (None, ""):
|
|
407
|
+
compact["record_id"] = str(record_id)
|
|
408
|
+
record = data.get("record")
|
|
409
|
+
if isinstance(record, dict):
|
|
410
|
+
compact["record"] = record
|
|
411
|
+
normalized_record = data.get("normalized_record")
|
|
412
|
+
if isinstance(normalized_record, dict):
|
|
413
|
+
compact["normalized_record"] = normalized_record
|
|
414
|
+
normalized_ambiguous_fields = data.get("normalized_ambiguous_fields")
|
|
415
|
+
if isinstance(normalized_ambiguous_fields, dict):
|
|
416
|
+
compact["normalized_ambiguous_fields"] = normalized_ambiguous_fields
|
|
417
|
+
payload["data"] = compact
|
|
309
418
|
|
|
310
419
|
|
|
311
420
|
def _trim_record_list(payload: JSONObject) -> None:
|
|
312
|
-
|
|
313
|
-
|
|
421
|
+
data = payload.get("data")
|
|
422
|
+
if not isinstance(data, dict):
|
|
423
|
+
return
|
|
424
|
+
pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
|
|
425
|
+
returned_items = pagination.get("returned_items")
|
|
426
|
+
result_amount = pagination.get("result_amount")
|
|
427
|
+
limit = pagination.get("limit")
|
|
428
|
+
truncated = False
|
|
429
|
+
if isinstance(result_amount, int) and isinstance(returned_items, int):
|
|
430
|
+
truncated = result_amount > returned_items
|
|
431
|
+
compact_pagination = {
|
|
432
|
+
"loaded": True,
|
|
433
|
+
"page_size": limit,
|
|
434
|
+
"fetched_pages": 1,
|
|
435
|
+
"reported_total": result_amount,
|
|
436
|
+
"truncated": truncated,
|
|
437
|
+
}
|
|
438
|
+
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
439
|
+
view = selection.get("view") if isinstance(selection.get("view"), dict) else {}
|
|
440
|
+
compact: dict[str, Any] = {
|
|
441
|
+
"app_key": data.get("app_key"),
|
|
442
|
+
"items": data.get("items") if isinstance(data.get("items"), list) else [],
|
|
443
|
+
"pagination": compact_pagination,
|
|
444
|
+
}
|
|
445
|
+
if view:
|
|
446
|
+
compact["view"] = _pick(view, ("view_id", "name"))
|
|
447
|
+
payload["data"] = compact
|
|
314
448
|
|
|
315
449
|
|
|
316
450
|
def _trim_record_analyze(payload: JSONObject) -> None:
|
|
317
|
-
|
|
451
|
+
summary: dict[str, Any] = {}
|
|
452
|
+
completeness = payload.get("completeness")
|
|
453
|
+
if isinstance(completeness, dict):
|
|
454
|
+
summary["completeness"] = completeness
|
|
455
|
+
presentation = payload.get("presentation")
|
|
456
|
+
if isinstance(presentation, dict):
|
|
457
|
+
summary["presentation"] = presentation
|
|
458
|
+
ranking = payload.get("ranking")
|
|
459
|
+
if isinstance(ranking, dict):
|
|
460
|
+
summary["ranking"] = ranking
|
|
461
|
+
error = payload.get("error")
|
|
462
|
+
if isinstance(error, dict):
|
|
463
|
+
summary["error"] = error
|
|
464
|
+
if summary:
|
|
465
|
+
payload["summary"] = summary
|
|
466
|
+
for key in ("query", "ranking", "ratios", "completeness", "presentation", "error", "debug"):
|
|
467
|
+
payload.pop(key, None)
|
|
318
468
|
|
|
319
469
|
|
|
320
470
|
def _trim_code_block_schema(payload: JSONObject) -> None:
|
|
@@ -334,6 +484,229 @@ def _trim_task_get(payload: JSONObject) -> None:
|
|
|
334
484
|
_drop_deep_keys(payload, {"request_route", "output_profile"})
|
|
335
485
|
|
|
336
486
|
|
|
487
|
+
def _trim_task_context_detail(payload: JSONObject) -> None:
|
|
488
|
+
_drop_deep_keys(payload, {"request_route", "output_profile"})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _trim_record_delete(payload: JSONObject) -> None:
|
|
492
|
+
data = payload.get("data")
|
|
493
|
+
if not isinstance(data, dict):
|
|
494
|
+
return
|
|
495
|
+
resource = data.get("resource")
|
|
496
|
+
deleted_ids: list[str] = []
|
|
497
|
+
if isinstance(resource, dict):
|
|
498
|
+
raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
|
|
499
|
+
if isinstance(raw_ids, list):
|
|
500
|
+
deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
|
|
501
|
+
data["deleted_ids"] = deleted_ids
|
|
502
|
+
data.setdefault("failed_ids", [])
|
|
503
|
+
for key in (
|
|
504
|
+
"resource",
|
|
505
|
+
"action",
|
|
506
|
+
"normalized_payload",
|
|
507
|
+
"human_review",
|
|
508
|
+
"verification",
|
|
509
|
+
"blockers",
|
|
510
|
+
"field_errors",
|
|
511
|
+
"confirmation_requests",
|
|
512
|
+
"resolved_fields",
|
|
513
|
+
"debug",
|
|
514
|
+
):
|
|
515
|
+
data.pop(key, None)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _compact_record_resource(resource: Any) -> dict[str, Any] | None:
|
|
519
|
+
if not isinstance(resource, dict):
|
|
520
|
+
return None
|
|
521
|
+
compact: dict[str, Any] = {}
|
|
522
|
+
if resource.get("type") not in (None, ""):
|
|
523
|
+
compact["type"] = resource.get("type")
|
|
524
|
+
app_key = resource.get("app_key") or resource.get("appKey")
|
|
525
|
+
if app_key not in (None, ""):
|
|
526
|
+
compact["app_key"] = app_key
|
|
527
|
+
record_id = resource.get("record_id")
|
|
528
|
+
if record_id not in (None, ""):
|
|
529
|
+
compact["record_id"] = str(record_id)
|
|
530
|
+
apply_id = resource.get("apply_id") or resource.get("applyId")
|
|
531
|
+
if apply_id not in (None, "") and "record_id" not in compact:
|
|
532
|
+
compact["record_id"] = str(apply_id)
|
|
533
|
+
record_ids = resource.get("record_ids")
|
|
534
|
+
if isinstance(record_ids, list):
|
|
535
|
+
compact["record_ids"] = [str(item) for item in record_ids if item not in (None, "")]
|
|
536
|
+
apply_ids = resource.get("apply_ids") or resource.get("applyIds")
|
|
537
|
+
if isinstance(apply_ids, list) and "record_ids" not in compact:
|
|
538
|
+
compact["record_ids"] = [str(item) for item in apply_ids if item not in (None, "")]
|
|
539
|
+
return compact or None
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _compact_schema_fields(items: Any, *, template_map: dict[str, Any] | None) -> list[JSONObject]:
|
|
543
|
+
if not isinstance(items, list):
|
|
544
|
+
return []
|
|
545
|
+
compacted: list[JSONObject] = []
|
|
546
|
+
for item in items:
|
|
547
|
+
compact = _compact_schema_field(item, template_map=template_map)
|
|
548
|
+
if compact:
|
|
549
|
+
compacted.append(compact)
|
|
550
|
+
return compacted
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) -> JSONObject | None:
|
|
554
|
+
if not isinstance(item, dict):
|
|
555
|
+
return None
|
|
556
|
+
compact: dict[str, Any] = {}
|
|
557
|
+
field_id = item.get("field_id")
|
|
558
|
+
if field_id not in (None, ""):
|
|
559
|
+
compact["field_id"] = field_id
|
|
560
|
+
title = item.get("title")
|
|
561
|
+
if title not in (None, ""):
|
|
562
|
+
compact["title"] = title
|
|
563
|
+
kind = item.get("kind") or item.get("write_kind")
|
|
564
|
+
if kind not in (None, ""):
|
|
565
|
+
compact["kind"] = kind
|
|
566
|
+
if "required" in item:
|
|
567
|
+
compact["required"] = bool(item.get("required"))
|
|
568
|
+
if template_map is not None and isinstance(title, str) and title in template_map:
|
|
569
|
+
compact["template"] = template_map.get(title)
|
|
570
|
+
candidate_hint = item.get("candidate_hint")
|
|
571
|
+
if isinstance(candidate_hint, dict):
|
|
572
|
+
compact["candidate_hint"] = candidate_hint
|
|
573
|
+
options = item.get("options")
|
|
574
|
+
if isinstance(options, list) and options:
|
|
575
|
+
compact["options"] = options
|
|
576
|
+
target_app_key = item.get("target_app_key")
|
|
577
|
+
if isinstance(target_app_key, str) and target_app_key:
|
|
578
|
+
compact["target_app_key"] = target_app_key
|
|
579
|
+
searchable_fields = item.get("searchable_fields")
|
|
580
|
+
if isinstance(searchable_fields, list) and searchable_fields:
|
|
581
|
+
compact["searchable_fields"] = searchable_fields
|
|
582
|
+
row_fields = item.get("row_fields")
|
|
583
|
+
if isinstance(row_fields, list) and row_fields:
|
|
584
|
+
compact["row_fields"] = _compact_schema_fields(row_fields, template_map=None)
|
|
585
|
+
return compact or None
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
|
|
589
|
+
compact: dict[str, Any] = {}
|
|
590
|
+
title = item.get("title")
|
|
591
|
+
if title not in (None, ""):
|
|
592
|
+
compact["title"] = title
|
|
593
|
+
kind = item.get("kind") or item.get("write_kind")
|
|
594
|
+
if kind not in (None, ""):
|
|
595
|
+
compact["kind"] = kind
|
|
596
|
+
compact["required"] = bool(item.get("required"))
|
|
597
|
+
options = item.get("options")
|
|
598
|
+
if isinstance(options, list) and options:
|
|
599
|
+
compact["options"] = options
|
|
600
|
+
if bool(item.get("accepts_natural_input")):
|
|
601
|
+
compact["accepts_natural_input"] = True
|
|
602
|
+
if bool(item.get("requires_upload")):
|
|
603
|
+
compact["requires_upload"] = True
|
|
604
|
+
target_app_key = item.get("target_app_key")
|
|
605
|
+
if isinstance(target_app_key, str) and target_app_key:
|
|
606
|
+
compact["target_app_key"] = target_app_key
|
|
607
|
+
target_app_name = item.get("target_app_name")
|
|
608
|
+
if isinstance(target_app_name, str) and target_app_name:
|
|
609
|
+
compact["target_app_name"] = target_app_name
|
|
610
|
+
searchable_fields = item.get("searchable_fields")
|
|
611
|
+
if isinstance(searchable_fields, list) and searchable_fields:
|
|
612
|
+
compact["searchable_fields"] = searchable_fields
|
|
613
|
+
return compact
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _looks_like_import_verify(payload: JSONObject) -> bool:
|
|
617
|
+
return "verification_id" in payload and "can_import" in payload
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _trim_import_verify_payload(payload: JSONObject) -> None:
|
|
621
|
+
issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
|
|
622
|
+
issue_summary = _summarize_import_issues(issues)
|
|
623
|
+
payload["issue_summary"] = issue_summary
|
|
624
|
+
columns = payload.get("columns")
|
|
625
|
+
if "expected_columns" not in payload and isinstance(columns, list):
|
|
626
|
+
payload["expected_columns"] = columns
|
|
627
|
+
file_name = payload.get("file_name")
|
|
628
|
+
if not file_name:
|
|
629
|
+
file_path = payload.get("file_path")
|
|
630
|
+
if isinstance(file_path, str) and file_path:
|
|
631
|
+
payload["file_name"] = file_path.split("/")[-1]
|
|
632
|
+
for key in ("apply_rows", "schema_fingerprint", "import_capability", "file_sha256", "verified_file_sha256", "file_format", "local_precheck_limited"):
|
|
633
|
+
payload.pop(key, None)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _trim_import_repair_payload(payload: JSONObject) -> None:
|
|
637
|
+
payload["verification_id"] = payload.get("new_verification_id") or payload.get("verification_id")
|
|
638
|
+
post_repair_issues = payload.get("post_repair_issues")
|
|
639
|
+
if isinstance(post_repair_issues, list):
|
|
640
|
+
payload["post_repair_issue_summary"] = _summarize_import_issues(post_repair_issues)
|
|
641
|
+
for key in ("new_verification_id", "verification"):
|
|
642
|
+
payload.pop(key, None)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _trim_import_template_payload(payload: JSONObject) -> None:
|
|
646
|
+
for key in ("schema_fingerprint", "verification"):
|
|
647
|
+
payload.pop(key, None)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _trim_import_status_payload(payload: JSONObject) -> None:
|
|
651
|
+
total_rows = payload.get("total_rows")
|
|
652
|
+
success_rows = payload.get("success_rows")
|
|
653
|
+
failed_rows = payload.get("failed_rows")
|
|
654
|
+
payload["total"] = total_rows
|
|
655
|
+
if isinstance(success_rows, int) and isinstance(failed_rows, int):
|
|
656
|
+
payload["finished"] = success_rows + failed_rows
|
|
657
|
+
elif isinstance(success_rows, int):
|
|
658
|
+
payload["finished"] = success_rows
|
|
659
|
+
else:
|
|
660
|
+
payload["finished"] = None
|
|
661
|
+
payload["succeeded"] = success_rows
|
|
662
|
+
payload["failed"] = failed_rows
|
|
663
|
+
for key in (
|
|
664
|
+
"matched_by",
|
|
665
|
+
"source_file_name",
|
|
666
|
+
"total_rows",
|
|
667
|
+
"success_rows",
|
|
668
|
+
"failed_rows",
|
|
669
|
+
"operate_time",
|
|
670
|
+
"operate_user",
|
|
671
|
+
"verification",
|
|
672
|
+
):
|
|
673
|
+
payload.pop(key, None)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _trim_export_payload(payload: JSONObject) -> None:
|
|
677
|
+
payload.pop("backend_export_id", None)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
|
|
681
|
+
total = 0
|
|
682
|
+
error_count = 0
|
|
683
|
+
warning_count = 0
|
|
684
|
+
sample: list[dict[str, Any]] = []
|
|
685
|
+
for item in issues:
|
|
686
|
+
if not isinstance(item, dict):
|
|
687
|
+
continue
|
|
688
|
+
total += 1
|
|
689
|
+
severity = str(item.get("severity") or "").lower()
|
|
690
|
+
if severity == "error":
|
|
691
|
+
error_count += 1
|
|
692
|
+
if severity == "warning":
|
|
693
|
+
warning_count += 1
|
|
694
|
+
if len(sample) < 3:
|
|
695
|
+
sample.append(
|
|
696
|
+
{
|
|
697
|
+
"code": item.get("code"),
|
|
698
|
+
"message": item.get("message"),
|
|
699
|
+
"severity": item.get("severity"),
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
return {
|
|
703
|
+
"total": total,
|
|
704
|
+
"errors": error_count,
|
|
705
|
+
"warnings": warning_count,
|
|
706
|
+
"sample": sample,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
|
|
337
710
|
def _trim_directory(payload: JSONObject) -> None:
|
|
338
711
|
pass
|
|
339
712
|
|
|
@@ -367,6 +740,7 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
|
|
|
367
740
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
|
|
368
741
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
|
|
369
742
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
|
|
743
|
+
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
|
|
370
744
|
_register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
|
|
371
745
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
|
|
372
746
|
_register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
|
|
@@ -385,6 +759,16 @@ _register_policy(
|
|
|
385
759
|
),
|
|
386
760
|
_trim_import_schema,
|
|
387
761
|
)
|
|
762
|
+
_register_policy(
|
|
763
|
+
(USER_DOMAIN,),
|
|
764
|
+
(
|
|
765
|
+
"record_export_start",
|
|
766
|
+
"record_export_status_get",
|
|
767
|
+
"record_export_get",
|
|
768
|
+
"record_export_direct",
|
|
769
|
+
),
|
|
770
|
+
_trim_export_payload,
|
|
771
|
+
)
|
|
388
772
|
_register_policy(
|
|
389
773
|
(USER_DOMAIN,),
|
|
390
774
|
(
|
|
@@ -402,15 +786,15 @@ _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
|
|
|
402
786
|
_register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
|
|
403
787
|
_register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
|
|
404
788
|
_register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
|
|
789
|
+
_register_policy((USER_DOMAIN,), ("task_get",), _trim_task_get)
|
|
405
790
|
_register_policy(
|
|
406
791
|
(USER_DOMAIN,),
|
|
407
792
|
(
|
|
408
|
-
"task_get",
|
|
409
793
|
"task_action_execute",
|
|
410
794
|
"task_associated_report_detail_get",
|
|
411
795
|
"task_workflow_log_get",
|
|
412
796
|
),
|
|
413
|
-
|
|
797
|
+
_trim_task_context_detail,
|
|
414
798
|
)
|
|
415
799
|
_register_policy(
|
|
416
800
|
(USER_DOMAIN,),
|
|
@@ -431,10 +815,10 @@ _register_policy(
|
|
|
431
815
|
(
|
|
432
816
|
"record_member_candidates",
|
|
433
817
|
"record_department_candidates",
|
|
434
|
-
"record_delete",
|
|
435
818
|
),
|
|
436
819
|
_trim_builder_list_like,
|
|
437
820
|
)
|
|
821
|
+
_register_policy((USER_DOMAIN,), ("record_delete",), _trim_record_delete)
|
|
438
822
|
_register_policy(
|
|
439
823
|
(BUILDER_DOMAIN,),
|
|
440
824
|
(
|
|
@@ -447,6 +831,7 @@ _register_policy(
|
|
|
447
831
|
"role_create",
|
|
448
832
|
"app_release_edit_lock_if_mine",
|
|
449
833
|
"app_resolve",
|
|
834
|
+
"button_style_catalog_get",
|
|
450
835
|
"app_custom_button_list",
|
|
451
836
|
"app_custom_button_get",
|
|
452
837
|
"app_custom_button_create",
|
|
@@ -10,6 +10,7 @@ from .tools.app_tools import AppTools
|
|
|
10
10
|
from .tools.auth_tools import AuthTools
|
|
11
11
|
from .tools.code_block_tools import CodeBlockTools
|
|
12
12
|
from .tools.feedback_tools import FeedbackTools
|
|
13
|
+
from .tools.export_tools import ExportTools
|
|
13
14
|
from .tools.file_tools import FileTools
|
|
14
15
|
from .tools.import_tools import ImportTools
|
|
15
16
|
from .tools.package_tools import PackageTools
|
|
@@ -50,6 +51,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
|
|
|
50
51
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
51
52
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
52
53
|
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
54
|
+
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
|
|
53
55
|
|
|
54
56
|
## Schema-First Rule
|
|
55
57
|
|
|
@@ -147,10 +149,33 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
147
149
|
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
|
|
148
150
|
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
|
|
149
151
|
|
|
152
|
+
## Export Path
|
|
153
|
+
|
|
154
|
+
`view_get -> record_export_start -> record_export_status_get -> record_export_get`
|
|
155
|
+
|
|
156
|
+
- `record_export_direct` is the one-shot export path that starts the export, waits for completion, downloads locally, and still returns remote download links.
|
|
157
|
+
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list` (`system:*` and `custom:*`).
|
|
158
|
+
- `record_export_start` / `record_export_direct` support frontend-like row selection:
|
|
159
|
+
- omit `record_ids` to export all rows in the selected view
|
|
160
|
+
- pass `record_ids` to export selected rows only
|
|
161
|
+
- `record_export_start` / `record_export_direct` also support internal query selection:
|
|
162
|
+
- pass `where` to resolve matching `record_id` values first
|
|
163
|
+
- pass `order_by` to keep the internal query and export row order aligned with `record_list`
|
|
164
|
+
- then run native export as selected rows
|
|
165
|
+
- `where/order_by` and `record_ids` are mutually exclusive
|
|
166
|
+
- `record_export_start` / `record_export_direct` also support frontend-like column selection:
|
|
167
|
+
- omit `columns` to export all current-view fields
|
|
168
|
+
- pass `columns` to export only selected fields, preserving the provided order
|
|
169
|
+
- `include_workflow_log=true` maps to the native workflow-log export switch.
|
|
170
|
+
|
|
150
171
|
## Task Workflow Path
|
|
151
172
|
|
|
152
173
|
`task_list -> task_get -> task_action_execute`
|
|
153
174
|
|
|
175
|
+
- `task_list` returns task-card summaries keyed by `task_id`.
|
|
176
|
+
- Prefer `task_get(task_id=...)` for detail reads; MCP resolves the current todo locator internally.
|
|
177
|
+
- `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
|
|
178
|
+
- `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
|
|
154
179
|
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
155
180
|
- Use `task_workflow_log_get` for full workflow log history.
|
|
156
181
|
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
@@ -186,6 +211,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
186
211
|
WorkspaceTools(sessions, backend).register(server)
|
|
187
212
|
FileTools(sessions, backend).register(server)
|
|
188
213
|
ImportTools(sessions, backend).register(server)
|
|
214
|
+
ExportTools(sessions, backend).register(server)
|
|
189
215
|
CodeBlockTools(sessions, backend).register(server)
|
|
190
216
|
TaskContextTools(sessions, backend).register(server)
|
|
191
217
|
RoleTools(sessions, backend).register(server)
|
|
@@ -38,7 +38,7 @@ def build_builder_server() -> FastMCP:
|
|
|
38
38
|
"If creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use package_get and app_resolve to locate resources, "
|
|
39
39
|
"app_get/app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
|
|
40
40
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
41
|
-
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates
|
|
41
|
+
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
42
42
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
43
43
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
44
44
|
"For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
|
|
@@ -93,6 +93,16 @@ def build_builder_server() -> FastMCP:
|
|
|
93
93
|
include_external=include_external,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
+
@server.tool()
|
|
97
|
+
def workspace_get(
|
|
98
|
+
profile: str = DEFAULT_PROFILE,
|
|
99
|
+
ws_id: int | None = None,
|
|
100
|
+
) -> dict:
|
|
101
|
+
return workspace.workspace_get(
|
|
102
|
+
profile=profile,
|
|
103
|
+
ws_id=ws_id,
|
|
104
|
+
)
|
|
105
|
+
|
|
96
106
|
@server.tool()
|
|
97
107
|
def file_upload_local(
|
|
98
108
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -280,6 +290,10 @@ def build_builder_server() -> FastMCP:
|
|
|
280
290
|
)
|
|
281
291
|
return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_id=package_id)
|
|
282
292
|
|
|
293
|
+
@server.tool()
|
|
294
|
+
def button_style_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
|
|
295
|
+
return ai_builder.button_style_catalog_get(profile=profile)
|
|
296
|
+
|
|
283
297
|
@server.tool()
|
|
284
298
|
def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
285
299
|
return ai_builder.app_custom_button_list(profile=profile, app_key=app_key)
|