@qingflow-tech/qingflow-app-user-mcp 1.0.39 → 1.0.40
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 +6 -5
- package/skills/qingflow-app-builder/references/create-app.md +10 -6
- package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
- package/src/qingflow_mcp/cli/commands/builder.py +62 -2
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +127 -5
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.40
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.40 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -36,15 +36,15 @@ Default modeling rules:
|
|
|
36
36
|
|
|
37
37
|
Before any builder write, classify the request:
|
|
38
38
|
|
|
39
|
-
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
|
|
39
|
+
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(package_id=..., apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
|
|
40
40
|
- **Single app**: the user names one form/app or gives one `app_key`. Use `app_resolve`/`app_get`, then `app_schema_apply` and the app-scoped apply tools.
|
|
41
41
|
- **Record/user operation**: the user wants to add, edit, delete, approve, or analyze data. Route to the record/task skills instead of builder tools.
|
|
42
42
|
|
|
43
|
-
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide.
|
|
43
|
+
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide. Create the related apps with one multi-app schema apply; do not create each app separately just to collect app keys and then patch relations afterward.
|
|
44
44
|
|
|
45
45
|
Builder schema inputs should follow agent-intuitive semantics:
|
|
46
46
|
|
|
47
|
-
-
|
|
47
|
+
- primary icon syntax is `icon + color`, for example `icon: "table", color: "blue"`; `icon_name/icon_color`, `icon_config`, and `icon: {name, color}` are compatibility aliases only
|
|
48
48
|
- `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
|
|
49
49
|
- relation fields need a target plus `display_field` and `visible_fields`
|
|
50
50
|
- do not create built-in system fields as form fields: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are platform-generated; only reference supported system fields where a tool explicitly says so, such as button `source_field: "数据ID"`
|
|
@@ -67,7 +67,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
67
67
|
- use `package_get(package_id=...)` to read one known package
|
|
68
68
|
- use `package_apply(...)` for package creation, rename, icon, visibility, grouping, ordering, and app/portal layout
|
|
69
69
|
- Multi-app schema work:
|
|
70
|
-
- use one `app_schema_apply(apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
|
|
70
|
+
- use one `app_schema_apply(package_id=..., apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
|
|
71
|
+
- every `apps[]` item should carry its own `client_key`, `app_name`, `icon`, `color`, and `add_fields`
|
|
71
72
|
- same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`, or `target_app` to point at another `apps[].app_name`
|
|
72
73
|
- App base permissions:
|
|
73
74
|
- trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
|
|
@@ -127,7 +128,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
127
128
|
1. Trust the current MCP/session when it is already injected by the runtime; only run auth/workspace recovery after a tool explicitly reports an auth, credential, or workspace error
|
|
128
129
|
2. Confirm whether the task is read-only or write-impacting
|
|
129
130
|
3. Classify the build scope:
|
|
130
|
-
- complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(apps=[...])`
|
|
131
|
+
- complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(package_id=..., apps=[...])`
|
|
131
132
|
- single app -> `app_resolve` / `app_get`, then app-scoped apply tools
|
|
132
133
|
- record/task/data request -> leave builder and use the matching record/task skill
|
|
133
134
|
4. Resolve the smallest stable target:
|
|
@@ -6,7 +6,7 @@ This playbook follows the current public builder surface, not legacy package hel
|
|
|
6
6
|
Do not use this playbook when the user is really asking for a system/package with multiple forms or modules. In that case:
|
|
7
7
|
|
|
8
8
|
1. read or create the package through `package_get` / `package_apply`
|
|
9
|
-
2. create the related apps in one `app_schema_apply(apps=[...])` call
|
|
9
|
+
2. create the related apps in one `app_schema_apply(package_id=..., apps=[...])` call
|
|
10
10
|
3. keep package ownership on the public `package_id` path instead of a separate attach step
|
|
11
11
|
4. add same-call relation fields with `target_app_ref` when one new app references another new app
|
|
12
12
|
5. choose explicit non-template `icon + color` for each new package/app
|
|
@@ -38,7 +38,7 @@ then do not treat that as one app.
|
|
|
38
38
|
Use this pattern instead:
|
|
39
39
|
|
|
40
40
|
1. `package_get` or `package_apply(create_if_missing=true, package_name=...)`
|
|
41
|
-
2. for a multi-app system, run one `app_schema_apply` with `apps[]`
|
|
41
|
+
2. for a multi-app system, run one `app_schema_apply` with `package_id` and `apps[]`
|
|
42
42
|
3. use `apps[].client_key` plus relation field `target_app_ref` when one new app references another new app
|
|
43
43
|
|
|
44
44
|
## Example
|
|
@@ -54,7 +54,8 @@ Create a new package only after the user confirms package creation:
|
|
|
54
54
|
"profile": "default",
|
|
55
55
|
"package_name": "研发项目管理",
|
|
56
56
|
"create_if_missing": true,
|
|
57
|
-
"icon":
|
|
57
|
+
"icon": "briefcase",
|
|
58
|
+
"color": "azure"
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
```
|
|
@@ -80,7 +81,8 @@ Apply schema for a new app:
|
|
|
80
81
|
"profile": "default",
|
|
81
82
|
"app_name": "客户订单",
|
|
82
83
|
"package_id": 1218950,
|
|
83
|
-
"icon":
|
|
84
|
+
"icon": "delivery-box-1",
|
|
85
|
+
"color": "emerald",
|
|
84
86
|
"create_if_missing": true,
|
|
85
87
|
"publish": true,
|
|
86
88
|
"add_fields": [
|
|
@@ -110,7 +112,8 @@ Apply schema for multiple apps in one call:
|
|
|
110
112
|
{
|
|
111
113
|
"client_key": "customer",
|
|
112
114
|
"app_name": "客户",
|
|
113
|
-
"icon":
|
|
115
|
+
"icon": "business-personalcard",
|
|
116
|
+
"color": "emerald",
|
|
114
117
|
"add_fields": [
|
|
115
118
|
{"name": "客户名称", "type": "text", "required": true, "as_data_title": true}
|
|
116
119
|
]
|
|
@@ -118,7 +121,8 @@ Apply schema for multiple apps in one call:
|
|
|
118
121
|
{
|
|
119
122
|
"client_key": "order",
|
|
120
123
|
"app_name": "订单",
|
|
121
|
-
"icon":
|
|
124
|
+
"icon": "delivery-box-1",
|
|
125
|
+
"color": "blue",
|
|
122
126
|
"add_fields": [
|
|
123
127
|
{"name": "订单编号", "type": "text", "required": true, "as_data_title": true},
|
|
124
128
|
{
|
|
@@ -21,7 +21,7 @@ If the user asks for multiple forms/modules that relate to each other, this is a
|
|
|
21
21
|
|
|
22
22
|
Use this split consistently:
|
|
23
23
|
|
|
24
|
-
- Complete system/package: `package_apply` first, then one `app_schema_apply(apps=[...])`.
|
|
24
|
+
- Complete system/package: `package_apply` first, then one `app_schema_apply(package_id=..., apps=[...])`.
|
|
25
25
|
- Single app: `app_resolve/app_get` first, then app-scoped apply tools.
|
|
26
26
|
- Record/task/data operation: leave builder and use record/task skills.
|
|
27
27
|
|
|
@@ -103,4 +103,4 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
|
|
|
103
103
|
- Do not omit assignees on approval/fill/copy nodes
|
|
104
104
|
- Do not patch preset flows with brand new approval/fill node ids unless you are intentionally replacing the skeleton; reuse preset ids like `approve_1` and `fill_1`
|
|
105
105
|
- Do not guess role ids, member ids, or editable field ids; resolve names first
|
|
106
|
-
- Do not force agent-authored schema into backend-internal names when public
|
|
106
|
+
- Do not force agent-authored schema into backend-internal names when public keys exist: write icons as `icon + color` first; `icon_config` / `icon:{name,color}` are compatibility aliases only. Options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
|
|
@@ -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
|
)
|
|
@@ -383,7 +383,7 @@ class AiBuilderTools(ToolBase):
|
|
|
383
383
|
remove_fields: list[JSONObject] | None = None,
|
|
384
384
|
apps: list[JSONObject] | None = None,
|
|
385
385
|
) -> JSONObject:
|
|
386
|
-
if apps:
|
|
386
|
+
if apps is not None:
|
|
387
387
|
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
388
388
|
return _config_failure(
|
|
389
389
|
tool_name="app_schema_apply",
|
|
@@ -1718,8 +1718,18 @@ class AiBuilderTools(ToolBase):
|
|
|
1718
1718
|
icon_color=icon_color,
|
|
1719
1719
|
icon_config=icon_config,
|
|
1720
1720
|
)
|
|
1721
|
-
if apps:
|
|
1722
|
-
|
|
1721
|
+
if apps is not None:
|
|
1722
|
+
normalized_apps_payload = _normalize_schema_apps_argument(
|
|
1723
|
+
tool_name="app_schema_apply",
|
|
1724
|
+
package_id=package_id,
|
|
1725
|
+
apps=apps,
|
|
1726
|
+
)
|
|
1727
|
+
failure = normalized_apps_payload.get("failure")
|
|
1728
|
+
if isinstance(failure, dict):
|
|
1729
|
+
return _attach_builder_apply_envelope("app_schema_apply", failure)
|
|
1730
|
+
package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
|
|
1731
|
+
apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
|
|
1732
|
+
input_warnings = list(normalized_apps_payload.get("warnings") or [])
|
|
1723
1733
|
result = self._app_schema_apply_multi(
|
|
1724
1734
|
profile=profile,
|
|
1725
1735
|
package_id=package_id,
|
|
@@ -1728,6 +1738,10 @@ class AiBuilderTools(ToolBase):
|
|
|
1728
1738
|
publish=publish,
|
|
1729
1739
|
apps=apps,
|
|
1730
1740
|
)
|
|
1741
|
+
if input_warnings:
|
|
1742
|
+
result_warnings = list(result.get("warnings") or [])
|
|
1743
|
+
result_warnings.extend(deepcopy(input_warnings))
|
|
1744
|
+
result["warnings"] = result_warnings
|
|
1731
1745
|
return _attach_builder_apply_envelope("app_schema_apply", result)
|
|
1732
1746
|
result = self._app_schema_apply_once(
|
|
1733
1747
|
profile=profile,
|
|
@@ -1803,9 +1817,13 @@ class AiBuilderTools(ToolBase):
|
|
|
1803
1817
|
if not apps:
|
|
1804
1818
|
return _config_failure(
|
|
1805
1819
|
tool_name="app_schema_apply",
|
|
1820
|
+
error_code="APPS_FILE_EMPTY",
|
|
1806
1821
|
message="app_schema_apply multi-app mode requires non-empty apps.",
|
|
1807
1822
|
fix_hint="Pass apps as a non-empty list of app schema items.",
|
|
1808
1823
|
)
|
|
1824
|
+
shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
|
|
1825
|
+
if shape_failure is not None:
|
|
1826
|
+
return shape_failure
|
|
1809
1827
|
icon_errors: list[JSONObject] = []
|
|
1810
1828
|
seen_new_app_icons: dict[str, int] = {}
|
|
1811
1829
|
for index, raw_item in enumerate(apps):
|
|
@@ -2932,6 +2950,109 @@ def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
|
|
|
2932
2950
|
return normalized
|
|
2933
2951
|
|
|
2934
2952
|
|
|
2953
|
+
def _schema_apps_expected_shape() -> JSONObject:
|
|
2954
|
+
return {
|
|
2955
|
+
"package_id": 1001,
|
|
2956
|
+
"apps": [
|
|
2957
|
+
{
|
|
2958
|
+
"client_key": "employee",
|
|
2959
|
+
"app_name": "员工花名册",
|
|
2960
|
+
"icon": "business-personalcard",
|
|
2961
|
+
"color": "emerald",
|
|
2962
|
+
"add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
|
|
2963
|
+
}
|
|
2964
|
+
],
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
|
|
2968
|
+
def _schema_apps_expected_shape_json() -> str:
|
|
2969
|
+
return (
|
|
2970
|
+
'{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
|
|
2971
|
+
'"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
|
|
2972
|
+
)
|
|
2973
|
+
|
|
2974
|
+
|
|
2975
|
+
def _is_schema_apps_wrapper_item(item: object) -> bool:
|
|
2976
|
+
return isinstance(item, dict) and "apps" in item
|
|
2977
|
+
|
|
2978
|
+
|
|
2979
|
+
def _schema_apps_shape_failure(*, tool_name: str, apps: list[JSONObject]) -> JSONObject | None:
|
|
2980
|
+
for index, item in enumerate(apps):
|
|
2981
|
+
if _is_schema_apps_wrapper_item(item):
|
|
2982
|
+
return _config_failure(
|
|
2983
|
+
tool_name=tool_name,
|
|
2984
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
2985
|
+
message="apps[] items must be app schema items, not package wrapper objects.",
|
|
2986
|
+
fix_hint=(
|
|
2987
|
+
"For MCP pass package_id separately and apps=[{app_name, icon, color, add_fields}]. "
|
|
2988
|
+
'For CLI use --apps-file with {"package_id":1001,"apps":[...]}. '
|
|
2989
|
+
"Do not pass multiple {package_id, apps} wrapper objects inside apps[]."
|
|
2990
|
+
),
|
|
2991
|
+
details={
|
|
2992
|
+
"index": index,
|
|
2993
|
+
"row_number": index + 1,
|
|
2994
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
2995
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
2996
|
+
},
|
|
2997
|
+
)
|
|
2998
|
+
return None
|
|
2999
|
+
|
|
3000
|
+
|
|
3001
|
+
def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, apps: list[JSONObject]) -> JSONObject:
|
|
3002
|
+
normalized_package_id = package_id
|
|
3003
|
+
normalized_apps: list[JSONObject] = apps
|
|
3004
|
+
warnings: list[JSONObject] = []
|
|
3005
|
+
|
|
3006
|
+
if len(apps) == 1 and _is_schema_apps_wrapper_item(apps[0]):
|
|
3007
|
+
wrapper = apps[0]
|
|
3008
|
+
wrapper_apps = wrapper.get("apps")
|
|
3009
|
+
if not isinstance(wrapper_apps, list):
|
|
3010
|
+
return {
|
|
3011
|
+
"failure": _config_failure(
|
|
3012
|
+
tool_name=tool_name,
|
|
3013
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3014
|
+
message="apps singleton wrapper requires an apps array.",
|
|
3015
|
+
fix_hint='Use {"package_id":1001,"apps":[...]} in CLI, or pass MCP package_id=1001 and apps=[...].',
|
|
3016
|
+
details={
|
|
3017
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3018
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3019
|
+
},
|
|
3020
|
+
)
|
|
3021
|
+
}
|
|
3022
|
+
if normalized_package_id is None and wrapper.get("package_id") is not None:
|
|
3023
|
+
try:
|
|
3024
|
+
normalized_package_id = int(wrapper.get("package_id"))
|
|
3025
|
+
except (TypeError, ValueError):
|
|
3026
|
+
return {
|
|
3027
|
+
"failure": _config_failure(
|
|
3028
|
+
tool_name=tool_name,
|
|
3029
|
+
error_code="APPS_FILE_SHAPE_INVALID",
|
|
3030
|
+
message="apps singleton wrapper package_id must be an integer.",
|
|
3031
|
+
fix_hint="Pass package_id as a number at the top level.",
|
|
3032
|
+
details={
|
|
3033
|
+
"expected_shape": _schema_apps_expected_shape(),
|
|
3034
|
+
"expected_shape_json": _schema_apps_expected_shape_json(),
|
|
3035
|
+
},
|
|
3036
|
+
)
|
|
3037
|
+
}
|
|
3038
|
+
normalized_apps = wrapper_apps
|
|
3039
|
+
warnings.append(
|
|
3040
|
+
{
|
|
3041
|
+
"code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
|
|
3042
|
+
"message": "apps was a singleton wrapper array; normalized it to package_id + apps.",
|
|
3043
|
+
}
|
|
3044
|
+
)
|
|
3045
|
+
else:
|
|
3046
|
+
failure = _schema_apps_shape_failure(tool_name=tool_name, apps=apps)
|
|
3047
|
+
if failure is not None:
|
|
3048
|
+
return {"failure": failure}
|
|
3049
|
+
|
|
3050
|
+
normalized_items: list[JSONObject] = []
|
|
3051
|
+
for item in normalized_apps:
|
|
3052
|
+
normalized_items.append(_normalize_schema_app_item(item) if isinstance(item, dict) else item)
|
|
3053
|
+
return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
|
|
3054
|
+
|
|
3055
|
+
|
|
2935
3056
|
def _compile_multi_app_schema_item_refs(
|
|
2936
3057
|
item: JSONObject,
|
|
2937
3058
|
client_key_to_app_key: dict[str, str],
|
|
@@ -4438,7 +4559,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4438
4559
|
"execution_notes": [
|
|
4439
4560
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4440
4561
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4441
|
-
"icon
|
|
4562
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
4442
4563
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4443
4564
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4444
4565
|
"metadata keys omitted on update are preserved",
|
|
@@ -5024,12 +5145,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
5024
5145
|
"create mode: package_id + app_name + create_if_missing=true",
|
|
5025
5146
|
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
5026
5147
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
5148
|
+
"CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
|
|
5027
5149
|
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
5028
5150
|
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
5029
5151
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
5030
5152
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
5031
5153
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
5032
|
-
"icon
|
|
5154
|
+
"agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
|
|
5033
5155
|
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
5034
5156
|
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
5035
5157
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|