@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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +18 -7
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +59 -0
- package/skills/qingflow-app-builder/references/create-app.md +13 -7
- package/skills/qingflow-app-builder/references/gotchas.md +6 -0
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +47 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +2 -0
- package/skills/qingflow-app-user/SKILL.md +2 -0
- package/skills/qingflow-mcp-setup/SKILL.md +2 -0
- package/skills/qingflow-record-analysis/SKILL.md +3 -1
- package/skills/qingflow-record-delete/SKILL.md +2 -0
- package/skills/qingflow-record-import/SKILL.md +29 -0
- package/skills/qingflow-record-insert/SKILL.md +24 -1
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +183 -0
- package/src/qingflow_mcp/builder_facade/service.py +722 -74
- package/src/qingflow_mcp/cli/commands/builder.py +62 -2
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/cli/formatters.py +1 -0
- package/src/qingflow_mcp/cli/main.py +2 -0
- package/src/qingflow_mcp/response_trim.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +515 -22
- 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
|
-
|
|
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(
|
|
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":
|
|
58
|
-
"details":
|
|
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")
|