@qingflow-tech/qingflow-app-builder-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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.38
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.40
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.38 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.40 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.38"
7
+ version = "1.0.40"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- - icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
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": {"name": "briefcase", "color": "azure"}
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": {"name": "delivery-box-1", "color": "emerald"},
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": {"name": "business-personalcard", "color": "emerald"},
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": {"name": "delivery-box-1", "color": "blue"},
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 aliases exist: icons may use `icon_config` / `icon:{name,color}`, options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
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 = 8
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 = 6
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(sections: list[PortalSectionPatch], components: list[dict[str, Any]]) -> dict[str, Any]:
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
- if source_type == "chart" and (cols < 8 or rows < 5):
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
- "chart portal card is too small; use at least pc.cols >= 8 and pc.rows >= 5, preferably rows >= 6",
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 = _coerce_positive_int(judge_type)
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
- 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
  )
@@ -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
- apps = [_normalize_schema_app_item(item) if isinstance(item, dict) else item for item in apps]
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 can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
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 can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
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",