@qingflow-tech/qingflow-app-user-mcp 1.0.38 → 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/builder_facade/models.py +1 -0
- package/src/qingflow_mcp/builder_facade/service.py +148 -11
- 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`.
|
|
@@ -2215,6 +2215,7 @@ class ChartGetResponse(StrictModel):
|
|
|
2215
2215
|
chart_id: str
|
|
2216
2216
|
base: dict[str, Any] = Field(default_factory=dict)
|
|
2217
2217
|
visibility: dict[str, Any] = Field(default_factory=dict)
|
|
2218
|
+
filters: list[list[dict[str, Any]]] = Field(default_factory=list)
|
|
2218
2219
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
2219
2220
|
|
|
2220
2221
|
|
|
@@ -7184,6 +7184,7 @@ class AiBuilderFacade:
|
|
|
7184
7184
|
chart_id=chart_id,
|
|
7185
7185
|
base=deepcopy(base) if isinstance(base, dict) else {},
|
|
7186
7186
|
visibility=_public_visibility_from_chart_visible_auth(base.get("visibleAuth")),
|
|
7187
|
+
filters=_public_chart_filter_groups_from_qingbi_config(config) if isinstance(config, dict) else [],
|
|
7187
7188
|
config=deepcopy(config) if isinstance(config, dict) else {},
|
|
7188
7189
|
)
|
|
7189
7190
|
return {
|
|
@@ -11612,12 +11613,12 @@ class AiBuilderFacade:
|
|
|
11612
11613
|
base_payload=base_payload,
|
|
11613
11614
|
)
|
|
11614
11615
|
if sections_requested:
|
|
11615
|
-
component_payload = self._build_portal_components_from_sections(
|
|
11616
|
+
component_payload, layout_metadata = self._build_portal_components_from_sections(
|
|
11616
11617
|
profile=profile,
|
|
11617
11618
|
sections=request.sections,
|
|
11618
11619
|
layout_preset=request.layout_preset,
|
|
11619
11620
|
)
|
|
11620
|
-
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload)
|
|
11621
|
+
layout_diagnostics = _portal_layout_diagnostics(request.sections, component_payload, layout_metadata=layout_metadata)
|
|
11621
11622
|
update_payload["components"] = component_payload
|
|
11622
11623
|
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
11623
11624
|
write_executed = True
|
|
@@ -12563,8 +12564,9 @@ class AiBuilderFacade:
|
|
|
12563
12564
|
profile: str,
|
|
12564
12565
|
sections: list[PortalSectionPatch],
|
|
12565
12566
|
layout_preset: str | None = None,
|
|
12566
|
-
) -> list[dict[str, Any]]:
|
|
12567
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
12567
12568
|
resolved_components: list[dict[str, Any]] = []
|
|
12569
|
+
layout_metadata: list[dict[str, Any]] = []
|
|
12568
12570
|
pc_x = 0
|
|
12569
12571
|
pc_y = 0
|
|
12570
12572
|
pc_row_height = 0
|
|
@@ -12596,6 +12598,11 @@ class AiBuilderFacade:
|
|
|
12596
12598
|
profile=profile,
|
|
12597
12599
|
ref=section.chart_ref,
|
|
12598
12600
|
)
|
|
12601
|
+
chart_type = str(
|
|
12602
|
+
resolved_chart.get("chart_type")
|
|
12603
|
+
or _public_chart_type_from_backend(section.config.get("chartType") or section.config.get("chart_type"))
|
|
12604
|
+
or ""
|
|
12605
|
+
).strip()
|
|
12599
12606
|
chart_config = {
|
|
12600
12607
|
"biChartId": resolved_chart["chart_id"],
|
|
12601
12608
|
"chartComponentTitle": section.title,
|
|
@@ -12603,6 +12610,7 @@ class AiBuilderFacade:
|
|
|
12603
12610
|
**deepcopy(section.config),
|
|
12604
12611
|
}
|
|
12605
12612
|
component = {"type": 9, "position": position_payload, "chartConfig": _compact_dict(chart_config)}
|
|
12613
|
+
layout_metadata.append({"source_type": section.source_type, "chart_type": chart_type})
|
|
12606
12614
|
elif section.source_type == "view":
|
|
12607
12615
|
resolved_view = _resolve_view_reference(
|
|
12608
12616
|
facade=self,
|
|
@@ -12625,26 +12633,31 @@ class AiBuilderFacade:
|
|
|
12625
12633
|
**deepcopy(section.config),
|
|
12626
12634
|
}
|
|
12627
12635
|
component = {"type": 10, "position": position_payload, "viewgraphConfig": _compact_dict(view_config)}
|
|
12636
|
+
layout_metadata.append({"source_type": section.source_type})
|
|
12628
12637
|
elif section.source_type == "grid":
|
|
12629
12638
|
component = {
|
|
12630
12639
|
"type": 2,
|
|
12631
12640
|
"position": position_payload,
|
|
12632
12641
|
"gridConfig": _compact_dict({"gridTitle": section.title, "beingShowTitle": True, **deepcopy(section.config)}),
|
|
12633
12642
|
}
|
|
12643
|
+
layout_metadata.append({"source_type": section.source_type})
|
|
12634
12644
|
elif section.source_type == "filter":
|
|
12635
12645
|
component = {"type": 6, "position": position_payload, "filterConfig": deepcopy(section.config)}
|
|
12646
|
+
layout_metadata.append({"source_type": section.source_type})
|
|
12636
12647
|
elif section.source_type == "text":
|
|
12637
12648
|
component = {"type": 5, "position": position_payload, "textConfig": {"text": section.text or "", **deepcopy(section.config)}}
|
|
12649
|
+
layout_metadata.append({"source_type": section.source_type})
|
|
12638
12650
|
else:
|
|
12639
12651
|
component = {
|
|
12640
12652
|
"type": 4,
|
|
12641
12653
|
"position": position_payload,
|
|
12642
12654
|
"linkConfig": {"url": section.url or "", "beingLoginAuth": False, **deepcopy(section.config)},
|
|
12643
12655
|
}
|
|
12656
|
+
layout_metadata.append({"source_type": section.source_type})
|
|
12644
12657
|
if dash_style is not None:
|
|
12645
12658
|
component["dashStyleConfigBO"] = dash_style
|
|
12646
12659
|
resolved_components.append(component)
|
|
12647
|
-
return resolved_components
|
|
12660
|
+
return resolved_components, layout_metadata
|
|
12648
12661
|
|
|
12649
12662
|
def _resolve_current_user_identity(self, *, profile: str) -> JSONObject:
|
|
12650
12663
|
session_profile = self.apps.sessions.get_profile(profile)
|
|
@@ -15212,6 +15225,103 @@ def _qingbi_chart_filter_value_to_text(*, value: Any, form_field: dict[str, Any]
|
|
|
15212
15225
|
reject(value)
|
|
15213
15226
|
|
|
15214
15227
|
|
|
15228
|
+
def _public_chart_filter_groups_from_qingbi_config(config: dict[str, Any]) -> list[list[dict[str, Any]]]:
|
|
15229
|
+
groups: list[list[dict[str, Any]]] = []
|
|
15230
|
+
raw_groups = config.get("beforeAggregationFilterMatrix")
|
|
15231
|
+
if not isinstance(raw_groups, list):
|
|
15232
|
+
return groups
|
|
15233
|
+
for raw_group in raw_groups:
|
|
15234
|
+
if not isinstance(raw_group, list):
|
|
15235
|
+
continue
|
|
15236
|
+
group: list[dict[str, Any]] = []
|
|
15237
|
+
for raw_rule in raw_group:
|
|
15238
|
+
if not isinstance(raw_rule, dict):
|
|
15239
|
+
continue
|
|
15240
|
+
operator = _public_chart_filter_operator_from_judge_type(raw_rule.get("judgeType"))
|
|
15241
|
+
field_id = raw_rule.get("fieldId") or raw_rule.get("field_id")
|
|
15242
|
+
field_name = (
|
|
15243
|
+
raw_rule.get("fieldName")
|
|
15244
|
+
or raw_rule.get("field_name")
|
|
15245
|
+
or raw_rule.get("queTitle")
|
|
15246
|
+
or raw_rule.get("title")
|
|
15247
|
+
or field_id
|
|
15248
|
+
)
|
|
15249
|
+
public_rule: dict[str, Any] = {
|
|
15250
|
+
"field_name": _stringify_condition_value(field_name).strip(),
|
|
15251
|
+
"operator": operator,
|
|
15252
|
+
}
|
|
15253
|
+
if field_id is not None:
|
|
15254
|
+
public_rule["field_id"] = _stringify_condition_value(field_id).strip()
|
|
15255
|
+
values = _public_chart_filter_values_from_rule(raw_rule, operator=operator)
|
|
15256
|
+
if values:
|
|
15257
|
+
public_rule["values"] = values
|
|
15258
|
+
group.append(public_rule)
|
|
15259
|
+
if group:
|
|
15260
|
+
groups.append(group)
|
|
15261
|
+
return groups
|
|
15262
|
+
|
|
15263
|
+
|
|
15264
|
+
def _public_chart_filter_operator_from_judge_type(judge_type: Any) -> str:
|
|
15265
|
+
normalized = _stringify_condition_value(judge_type).strip()
|
|
15266
|
+
mapping = {
|
|
15267
|
+
"equal": "eq",
|
|
15268
|
+
"equals": "eq",
|
|
15269
|
+
"=": "eq",
|
|
15270
|
+
"unequal": "neq",
|
|
15271
|
+
"notEqual": "neq",
|
|
15272
|
+
"!=": "neq",
|
|
15273
|
+
"anyMatch": "in",
|
|
15274
|
+
"oneOf": "in",
|
|
15275
|
+
"include": "contains",
|
|
15276
|
+
"contains": "contains",
|
|
15277
|
+
"greaterOrEqual": "gte",
|
|
15278
|
+
"gte": "gte",
|
|
15279
|
+
"lessOrEqual": "lte",
|
|
15280
|
+
"lte": "lte",
|
|
15281
|
+
"isNull": "is_empty",
|
|
15282
|
+
"empty": "is_empty",
|
|
15283
|
+
"notNull": "not_empty",
|
|
15284
|
+
"not_empty": "not_empty",
|
|
15285
|
+
}
|
|
15286
|
+
if normalized in mapping:
|
|
15287
|
+
return mapping[normalized]
|
|
15288
|
+
numeric = _coerce_nonnegative_int(judge_type)
|
|
15289
|
+
if numeric is not None:
|
|
15290
|
+
return _public_view_filter_operator_from_judge_type(numeric)
|
|
15291
|
+
return f"judge_type:{judge_type}"
|
|
15292
|
+
|
|
15293
|
+
|
|
15294
|
+
def _public_chart_filter_values_from_rule(rule: dict[str, Any], *, operator: str) -> list[str]:
|
|
15295
|
+
if operator in {"is_empty", "not_empty"}:
|
|
15296
|
+
return []
|
|
15297
|
+
raw_value = rule.get("judgeValue")
|
|
15298
|
+
if raw_value is None and "judgeValues" in rule:
|
|
15299
|
+
raw_value = rule.get("judgeValues")
|
|
15300
|
+
values: list[str] = []
|
|
15301
|
+
if isinstance(raw_value, list):
|
|
15302
|
+
values = [_stringify_condition_value(value).strip() for value in raw_value]
|
|
15303
|
+
elif isinstance(raw_value, str) and "<&&>" in raw_value:
|
|
15304
|
+
values = [value.strip() for value in raw_value.split("<&&>")]
|
|
15305
|
+
elif raw_value is not None:
|
|
15306
|
+
values = [_stringify_condition_value(raw_value).strip()]
|
|
15307
|
+
values = [value for value in values if value]
|
|
15308
|
+
if values:
|
|
15309
|
+
return values
|
|
15310
|
+
for detail_key in ("judgeValueDetailList", "judgeValueDetails"):
|
|
15311
|
+
details = rule.get(detail_key)
|
|
15312
|
+
if not isinstance(details, list):
|
|
15313
|
+
continue
|
|
15314
|
+
for detail in details:
|
|
15315
|
+
if not isinstance(detail, dict):
|
|
15316
|
+
continue
|
|
15317
|
+
for value_key in ("value", "label", "name", "title", "dataValue", "id"):
|
|
15318
|
+
value = _stringify_condition_value(detail.get(value_key)).strip()
|
|
15319
|
+
if value:
|
|
15320
|
+
values.append(value)
|
|
15321
|
+
break
|
|
15322
|
+
return values
|
|
15323
|
+
|
|
15324
|
+
|
|
15215
15325
|
def _build_public_chart_config_payload(
|
|
15216
15326
|
*,
|
|
15217
15327
|
patch: ChartUpsertPatch,
|
|
@@ -15421,6 +15531,17 @@ def _extract_chart_identifier(chart: Any) -> str:
|
|
|
15421
15531
|
).strip()
|
|
15422
15532
|
|
|
15423
15533
|
|
|
15534
|
+
def _chart_type_from_item(chart: Any) -> str:
|
|
15535
|
+
if not isinstance(chart, dict):
|
|
15536
|
+
return ""
|
|
15537
|
+
return _public_chart_type_from_backend(
|
|
15538
|
+
chart.get("chartType")
|
|
15539
|
+
or chart.get("chart_type")
|
|
15540
|
+
or chart.get("chartGraphType")
|
|
15541
|
+
or chart.get("type")
|
|
15542
|
+
)
|
|
15543
|
+
|
|
15544
|
+
|
|
15424
15545
|
def _normalize_backend_chart_type(value: Any) -> str:
|
|
15425
15546
|
raw = str(value or "").strip()
|
|
15426
15547
|
if not raw:
|
|
@@ -15537,7 +15658,7 @@ def _portal_component_position_public(
|
|
|
15537
15658
|
rows = 2
|
|
15538
15659
|
elif source_name == "view":
|
|
15539
15660
|
cols = 24
|
|
15540
|
-
rows =
|
|
15661
|
+
rows = 11
|
|
15541
15662
|
elif source_name == "link":
|
|
15542
15663
|
cols = 12
|
|
15543
15664
|
rows = 2
|
|
@@ -15546,7 +15667,7 @@ def _portal_component_position_public(
|
|
|
15546
15667
|
cols = 12
|
|
15547
15668
|
else:
|
|
15548
15669
|
cols = 8
|
|
15549
|
-
rows =
|
|
15670
|
+
rows = 7
|
|
15550
15671
|
if cols == 24:
|
|
15551
15672
|
if pc_x != 0:
|
|
15552
15673
|
pc_y += pc_row_height
|
|
@@ -15581,7 +15702,12 @@ def _empty_portal_layout_diagnostics() -> dict[str, Any]:
|
|
|
15581
15702
|
}
|
|
15582
15703
|
|
|
15583
15704
|
|
|
15584
|
-
def _portal_layout_diagnostics(
|
|
15705
|
+
def _portal_layout_diagnostics(
|
|
15706
|
+
sections: list[PortalSectionPatch],
|
|
15707
|
+
components: list[dict[str, Any]],
|
|
15708
|
+
*,
|
|
15709
|
+
layout_metadata: list[dict[str, Any]] | None = None,
|
|
15710
|
+
) -> dict[str, Any]:
|
|
15585
15711
|
diagnostics = _empty_portal_layout_diagnostics()
|
|
15586
15712
|
diagnostics["section_count"] = len(sections)
|
|
15587
15713
|
explicit_count = sum(1 for section in sections if section.position is not None)
|
|
@@ -15596,16 +15722,26 @@ def _portal_layout_diagnostics(sections: list[PortalSectionPatch], components: l
|
|
|
15596
15722
|
if pc:
|
|
15597
15723
|
pc_positions.append(pc)
|
|
15598
15724
|
section = sections[index] if index < len(sections) else None
|
|
15725
|
+
metadata = layout_metadata[index] if isinstance(layout_metadata, list) and index < len(layout_metadata) and isinstance(layout_metadata[index], dict) else {}
|
|
15599
15726
|
source_type = str(getattr(section, "source_type", "") or "").lower() if section is not None else ""
|
|
15600
15727
|
title = str(getattr(section, "title", "") or "").strip() if section is not None else None
|
|
15601
15728
|
cols = int(pc.get("cols") or 0)
|
|
15602
15729
|
rows = int(pc.get("rows") or 0)
|
|
15603
|
-
|
|
15730
|
+
chart_type = str(metadata.get("chart_type") or "").strip().lower()
|
|
15731
|
+
is_metric_chart = chart_type in {"target", "indicator"}
|
|
15732
|
+
min_chart_cols = 6 if is_metric_chart else 8
|
|
15733
|
+
min_chart_rows = 5 if is_metric_chart else 7
|
|
15734
|
+
if source_type == "chart" and (cols < min_chart_cols or rows < min_chart_rows):
|
|
15604
15735
|
warnings.append(_warning(
|
|
15605
15736
|
"PORTAL_CHART_CARD_TOO_SMALL",
|
|
15606
|
-
|
|
15737
|
+
(
|
|
15738
|
+
"metric chart portal card is too small; use at least pc.cols >= 6 and pc.rows >= 5"
|
|
15739
|
+
if is_metric_chart
|
|
15740
|
+
else "chart portal card is too small; use at least pc.cols >= 8 and pc.rows >= 7 for non-metric charts"
|
|
15741
|
+
),
|
|
15607
15742
|
section_index=index,
|
|
15608
15743
|
title=title,
|
|
15744
|
+
chart_type=chart_type or None,
|
|
15609
15745
|
pc=deepcopy(pc),
|
|
15610
15746
|
))
|
|
15611
15747
|
if section is not None and section.position is not None and not bool(getattr(section.position, "mobile_provided", False)):
|
|
@@ -15648,7 +15784,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
|
|
|
15648
15784
|
item_id = _extract_chart_identifier(item)
|
|
15649
15785
|
item_name = str(item.get("chartName") or "").strip()
|
|
15650
15786
|
if item_id == chart_id:
|
|
15651
|
-
return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key}
|
|
15787
|
+
return {"chart_id": item_id, "chart_name": item_name, "app_key": app_key, "chart_type": _chart_type_from_item(item)}
|
|
15652
15788
|
raise ValueError(f"chart ref chart_id '{chart_id}' could not be resolved under app '{app_key}'")
|
|
15653
15789
|
matches = _find_charts_by_name(items, chart_name=chart_name)
|
|
15654
15790
|
if len(matches) > 1:
|
|
@@ -15659,6 +15795,7 @@ def _resolve_chart_reference(*, charts: QingbiReportTools, profile: str, ref: An
|
|
|
15659
15795
|
"chart_id": _extract_chart_identifier(item),
|
|
15660
15796
|
"chart_name": str(item.get("chartName") or "").strip(),
|
|
15661
15797
|
"app_key": app_key,
|
|
15798
|
+
"chart_type": _chart_type_from_item(item),
|
|
15662
15799
|
}
|
|
15663
15800
|
raise ValueError(f"chart ref could not be resolved under app '{app_key}'")
|
|
15664
15801
|
|
|
@@ -24719,7 +24856,7 @@ def _public_view_filter_groups_from_match_rules(
|
|
|
24719
24856
|
|
|
24720
24857
|
|
|
24721
24858
|
def _public_view_filter_operator_from_judge_type(judge_type: Any) -> str:
|
|
24722
|
-
normalized =
|
|
24859
|
+
normalized = _coerce_nonnegative_int(judge_type)
|
|
24723
24860
|
if normalized == JUDGE_EQUAL:
|
|
24724
24861
|
return "eq"
|
|
24725
24862
|
if normalized == JUDGE_UNEQUAL:
|
|
@@ -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",
|