@qingflow-tech/qingflow-app-user-mcp 1.0.39 → 1.0.41

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 (28) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-app-builder/SKILL.md +18 -7
  5. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +59 -0
  6. package/skills/qingflow-app-builder/references/create-app.md +13 -7
  7. package/skills/qingflow-app-builder/references/gotchas.md +6 -0
  8. package/skills/qingflow-app-builder/references/single-app-development-guide.md +47 -0
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
  10. package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
  11. package/skills/qingflow-app-builder-code-integrations/SKILL.md +2 -0
  12. package/skills/qingflow-app-user/SKILL.md +2 -0
  13. package/skills/qingflow-mcp-setup/SKILL.md +2 -0
  14. package/skills/qingflow-record-analysis/SKILL.md +3 -1
  15. package/skills/qingflow-record-delete/SKILL.md +2 -0
  16. package/skills/qingflow-record-import/SKILL.md +29 -0
  17. package/skills/qingflow-record-insert/SKILL.md +24 -1
  18. package/skills/qingflow-record-update/SKILL.md +3 -0
  19. package/skills/qingflow-task-ops/SKILL.md +2 -0
  20. package/src/qingflow_mcp/builder_facade/models.py +183 -0
  21. package/src/qingflow_mcp/builder_facade/service.py +722 -74
  22. package/src/qingflow_mcp/cli/commands/builder.py +62 -2
  23. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  24. package/src/qingflow_mcp/cli/formatters.py +1 -0
  25. package/src/qingflow_mcp/cli/main.py +2 -0
  26. package/src/qingflow_mcp/response_trim.py +1 -0
  27. package/src/qingflow_mcp/tools/ai_builder_tools.py +515 -22
  28. package/src/qingflow_mcp/tools/record_tools.py +28 -2
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ from copy import deepcopy
4
5
 
5
6
  from ..context import CliContext
6
- from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
7
7
  from ..json_io import load_json_value
8
+ from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
8
9
 
9
10
 
10
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -479,6 +480,7 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
479
480
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
480
481
  apps_payload = _load_apps_file_arg(args.apps_file)
481
482
  apps = apps_payload["apps"]
483
+ apps_warnings = apps_payload.get("warnings") or []
482
484
  package_id = args.package_id
483
485
  if package_id is None and apps_payload.get("package_id") is not None:
484
486
  package_id = int(apps_payload["package_id"])
@@ -487,6 +489,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
487
489
  raise_config_error(
488
490
  "schema apply multi-app mode requires a non-empty --apps-file.",
489
491
  fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
492
+ error_code="APPS_FILE_EMPTY",
490
493
  )
491
494
  if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
492
495
  raise_config_error(
@@ -498,7 +501,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
498
501
  "schema apply multi-app mode requires --package-id.",
499
502
  fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
500
503
  )
501
- return context.builder.app_schema_apply(
504
+ result = context.builder.app_schema_apply(
502
505
  profile=args.profile,
503
506
  package_id=package_id,
504
507
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
@@ -509,6 +512,11 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
509
512
  update_fields=[],
510
513
  remove_fields=[],
511
514
  )
515
+ if apps_warnings and isinstance(result, dict):
516
+ result_warnings = list(result.get("warnings") or [])
517
+ result_warnings.extend(deepcopy(apps_warnings))
518
+ result["warnings"] = result_warnings
519
+ return result
512
520
  has_app_key = bool((args.app_key or "").strip())
513
521
  has_app_name = bool((args.app_name or "").strip())
514
522
  has_app_title = bool((args.app_title or "").strip())
@@ -545,8 +553,56 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
545
553
  def _load_apps_file_arg(path: str | None) -> dict[str, object]:
546
554
  if not path:
547
555
  return {"apps": []}
556
+ expected_shape = {
557
+ "package_id": 1001,
558
+ "apps": [
559
+ {
560
+ "client_key": "employee",
561
+ "app_name": "员工花名册",
562
+ "icon": "business-personalcard",
563
+ "color": "emerald",
564
+ "add_fields": [],
565
+ }
566
+ ],
567
+ }
568
+ expected_shape_details = {
569
+ "expected_shape": expected_shape,
570
+ "expected_shape_json": (
571
+ '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
572
+ '"icon":"business-personalcard","color":"emerald","add_fields":[]}]}'
573
+ ),
574
+ }
548
575
  payload = load_json_value(path, option_name="--apps-file")
549
576
  if isinstance(payload, list):
577
+ if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
578
+ wrapper = payload[0]
579
+ apps = wrapper.get("apps")
580
+ if not isinstance(apps, list):
581
+ raise_config_error(
582
+ "--apps-file wrapper object requires an apps array.",
583
+ fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw apps array like [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}].",
584
+ error_code="APPS_FILE_SHAPE_INVALID",
585
+ details=expected_shape_details,
586
+ )
587
+ result: dict[str, object] = {
588
+ "apps": apps,
589
+ "warnings": [
590
+ {
591
+ "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
592
+ "message": "--apps-file was a singleton wrapper array; normalized it to {package_id, apps}.",
593
+ }
594
+ ],
595
+ }
596
+ if wrapper.get("package_id") is not None:
597
+ result["package_id"] = wrapper.get("package_id")
598
+ return result
599
+ if any(isinstance(item, dict) and "apps" in item for item in payload):
600
+ raise_config_error(
601
+ "--apps-file root array items must be app items, not package wrapper objects.",
602
+ fix_hint="Use one object {\"package_id\":1001,\"apps\":[...]} or a raw app array [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}]. Do not wrap multiple {package_id, apps} objects in an array.",
603
+ error_code="APPS_FILE_SHAPE_INVALID",
604
+ details=expected_shape_details,
605
+ )
550
606
  return {"apps": payload}
551
607
  if isinstance(payload, dict):
552
608
  apps = payload.get("apps")
@@ -554,6 +610,8 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
554
610
  raise_config_error(
555
611
  "--apps-file JSON object requires an apps array.",
556
612
  fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
613
+ error_code="APPS_FILE_SHAPE_INVALID",
614
+ details=expected_shape_details,
557
615
  )
558
616
  result: dict[str, object] = {"apps": apps}
559
617
  if payload.get("package_id") is not None:
@@ -562,6 +620,8 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
562
620
  raise_config_error(
563
621
  "--apps-file must be a JSON array or an object containing apps.",
564
622
  fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
623
+ error_code="APPS_FILE_SHAPE_INVALID",
624
+ details=expected_shape_details,
565
625
  )
566
626
 
567
627
 
@@ -48,14 +48,23 @@ def read_secret_arg(value: str | None, *, stdin_enabled: bool, label: str) -> st
48
48
  raise QingflowApiError.config_error(f"{label} is required")
49
49
 
50
50
 
51
- def raise_config_error(message: str, *, fix_hint: str) -> None:
51
+ def raise_config_error(
52
+ message: str,
53
+ *,
54
+ fix_hint: str | None = None,
55
+ error_code: str = "CONFIG_ERROR",
56
+ details: dict[str, Any] | None = None,
57
+ ) -> None:
58
+ error_details: dict[str, Any] = {"fix_hint": fix_hint or message}
59
+ if details:
60
+ error_details.update(details)
52
61
  raise RuntimeError(
53
62
  json.dumps(
54
63
  {
55
64
  "category": "config",
56
65
  "message": message,
57
- "error_code": "CONFIG_ERROR",
58
- "details": {"fix_hint": fix_hint},
66
+ "error_code": error_code,
67
+ "details": error_details,
59
68
  },
60
69
  ensure_ascii=False,
61
70
  )
@@ -36,6 +36,7 @@ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
36
36
  status = str(result.get("status") or "").lower()
37
37
  executed = bool(
38
38
  result.get("write_executed")
39
+ or result.get("write_may_have_succeeded")
39
40
  or result.get("delete_executed")
40
41
  or result.get("action_executed")
41
42
  or result.get("export_executed")
@@ -423,6 +423,7 @@ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
423
423
  status = str(result.get("status") or "").lower()
424
424
  executed = bool(
425
425
  result.get("write_executed")
426
+ or result.get("write_may_have_succeeded")
426
427
  or result.get("delete_executed")
427
428
  or result.get("action_executed")
428
429
  or result.get("export_executed")
@@ -441,6 +442,7 @@ def _has_readback_unavailable_verification(result: dict[str, Any]) -> bool:
441
442
  "readback_pending",
442
443
  "metadata_unverified",
443
444
  "views_read_unavailable",
445
+ "readback_before_retry",
444
446
  )
445
447
  )
446
448
 
@@ -169,6 +169,7 @@ def _is_executed_nonfatal_payload(payload: dict[str, Any]) -> bool:
169
169
  status = str(payload.get("status") or "").lower()
170
170
  executed = bool(
171
171
  payload.get("write_executed")
172
+ or payload.get("write_may_have_succeeded")
172
173
  or payload.get("delete_executed")
173
174
  or payload.get("action_executed")
174
175
  or payload.get("export_executed")