@qingflow-tech/qingflow-app-builder-mcp 1.0.8 → 1.0.9

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.8
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.9
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.8 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.9 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.8",
3
+ "version": "1.0.9",
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.8"
7
+ version = "1.0.9"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -61,7 +61,10 @@ Treat these as the official surface. Do not default to `package_create`, `packag
61
61
  - in edit mode, omitting `sections` means “preserve existing layout and update base info only”
62
62
  - supplying `sections` means full replace semantics for sections
63
63
  - Chart work:
64
- - `app_charts_apply` is the public write path
64
+ - `app_charts_apply` is the public write path for app-source QingBI report bodies/configs; it creates/updates reports with `dataSourceType=qingflow`
65
+ - dataset BI reports are not created or edited by `app_charts_apply` yet; create them in QingBI first, then attach the existing report with `app_associated_resources_apply` and `report_source="dataset"`
66
+ - it does not attach the report to the Qingflow app associated-resource display; use `app_associated_resources_apply` for that
67
+ - supported `chart_type` values include legacy public aliases `target`, `table` and QingBI types such as `summary`, `columnar`, `area`, `stacked_area`, `funnel`, `waterfall`, `gauge`, `heatmap`, `histogram`, `treemap`, `radar`, `stacked_bar`, `stacked_column`, `scatter`, `ring`, `rose`, `dualaxes`, `map`, and `timeline`
65
68
  - use `patch_charts` for changing a chart name, visibility, filters, or one config fragment on an existing chart
66
69
  - `visibility` is a public capability and should be treated as a base-only permission update
67
70
  - do not model chart visibility changes as raw config rewrites
@@ -79,12 +82,13 @@ Treat these as the official surface. Do not default to `package_create`, `packag
79
82
  - `placement=list` configures row/list buttons and maps to the backend `INSIDE` button position
80
83
  - advanced bindings may use `button_limit`, `button_formula`, `button_formula_type`, and `print_tpls` only when visibility or print-template behavior is required
81
84
  - Associated resources:
82
- - `app_associated_resources_apply` is the public write path for the app-level associated report/view pool and per-view display config
85
+ - `app_associated_resources_apply` is the public write path for the Qingflow app-level associated report/view pool and per-view display config
86
+ - it attaches existing BI reports/views for in-app display; it does not create or edit QingBI report bodies/configs, including dataset reports
83
87
  - use `patch_resources` for changing match rules or other existing associated-resource parameters; the tool preserves backend-required raw fields internally
84
- - `associated_item_id` must come from `app_get.associated_resources[].associated_item_id`; it is not `chart_id`, `chart_key`, or `view_key`
88
+ - `associated_item_id` is the backend internal associated-resource id; for `view_configs`, `remove_associated_item_ids`, and `reorder_associated_item_ids`, you may pass `associated_item_id` or an existing resource's `chart_id`/`chart_key`/`view_key`, and the tool resolves it to the internal id
85
89
  - before creating a resource, check `app_get.associated_resources` for the same `target_app_key + view_key/chart_key`; if it already exists, use `patch_resources` with that `associated_item_id`
86
90
  - `client_key` is only a same-call reference for `view_configs[].associated_item_refs`; it is not saved and cannot deduplicate later calls
87
- - do not pass backend raw `sourceType`; view resources infer the internal Qingflow view source, report/chart resources default to BI app reports, and dataset reports use `report_source="dataset"`
91
+ - do not pass backend raw `sourceType`; view resources infer the internal Qingflow view source, report/chart resources default to BI app reports, and existing dataset reports use `report_source="dataset"`
88
92
  - use `match_mappings` for associated view/report filters; dynamic conditions use `source_field`, static conditions use `value`
89
93
  - if field type compatibility is unclear, read [references/match-rules.md](references/match-rules.md)
90
94
  - Views and flows:
@@ -25,6 +25,14 @@
25
25
  - `app_custom_buttons_apply` and `app_associated_resources_apply` publish after at least one write succeeds and do not accept `publish=false`
26
26
  - `app_publish_verify` is for explicit final verification, not the default next step after every write
27
27
 
28
+ ## BI Reports
29
+
30
+ - Use `app_charts_apply` for QingBI report body/config creation or updates.
31
+ - Use `app_associated_resources_apply` only when an existing BI report or view needs to appear in the Qingflow app associated-resource area or a specific view.
32
+ - `app_charts_apply` currently creates/updates app-source BI reports only (`dataSourceType=qingflow`); dataset BI reports can only be attached when they already exist.
33
+ - Creating a chart with `app_charts_apply` does not automatically show it in the Qingflow app UI; attach it separately if display is required.
34
+ - `target` and `table` remain compatibility aliases for QingBI `indicator` and `detail`; use the real QingBI chart type when you need a specific type such as `summary`, `columnar`, `stacked_bar`, `scatter`, or `dualaxes`.
35
+
28
36
  ## Custom buttons
29
37
 
30
38
  - Use `app_custom_buttons_apply` for button creation/update/removal and view placement; do not split placement into `app_views_apply.buttons` unless you are maintaining a legacy patch.
@@ -45,6 +53,7 @@
45
53
 
46
54
  - Before creating an associated view/report, check `app_get.associated_resources` for the same `target_app_key + view_key/chart_key`.
47
55
  - If the resource already exists, use `patch_resources` with its `associated_item_id`; do not call `upsert_resources` again.
56
+ - For per-view display, remove, and reorder, existing associated reports/views may be referenced by `associated_item_id`, `chart_id`/`chart_key`, or `view_key`; the tool resolves these to the internal id before writing.
48
57
  - `client_key` is only a same-call alias for `view_configs[].associated_item_refs`. It is not stored by Qingflow and cannot prevent duplicates in later calls.
49
58
  - Repeated `upsert_resources` without `associated_item_id` can create multiple app-level associated items pointing to the same view/report.
50
59
 
@@ -47,8 +47,8 @@ These execute normalized patches. Some app apply tools publish by default and st
47
47
  - `app_flow_apply`: replace workflow
48
48
  - `app_views_apply`: use `patch_views` for existing-view parameter replacement; use `upsert_views` for creation or full target config; remove views by key
49
49
  - `app_custom_buttons_apply`: use `patch_buttons` for existing-button parameter replacement; use `upsert_buttons` for creation or full target config; configure add-data field mappings/default values; bind buttons to header/detail/list view positions; `placement=list` maps to the backend `INSIDE` row/list button position; merge-mode view configs require `buttons`; use `view_configs[].mode="replace"` or `buttons=[]` to clear a view's custom button bindings. For child-record creation linked to the current source record, map `source_field: "数据ID"` to the target relation field.
50
- - `app_associated_resources_apply`: use `patch_resources` for existing associated-resource parameter replacement; use `upsert_resources` for creation or full target config; use `match_mappings` for associated view/report filters; manage per-view display config; publishes after successful writes; omit raw `sourceType`, and use `report_source="dataset"` only for BI dataset reports. Before `upsert_resources`, read `app_get.associated_resources` and reuse an existing matching `target_app_key + view_key/chart_key`; repeated upsert can create duplicates because `client_key` is only valid inside one apply call.
51
- - `app_charts_apply`: use `patch_charts` for existing-chart parameter replacement; use `upsert_charts` for creation or full target config; remove/reorder QingBI charts; charts are immediate-live and do not publish; use `chart_id` when names are not unique
50
+ - `app_associated_resources_apply`: attach existing BI reports/views to the Qingflow app associated-resource pool and per-view display area; it does not create or edit QingBI report bodies/configs. Use `patch_resources` for existing associated-resource parameter replacement; use `upsert_resources` for creation or full target config; `view_configs`, remove, and reorder may reference existing resources by internal `associated_item_id` or by `chart_id`/`chart_key`/`view_key`; use `match_mappings` for associated view/report filters; publishes after successful writes; omit raw `sourceType`, and use `report_source="dataset"` only to attach an existing BI dataset report. Before `upsert_resources`, read `app_get.associated_resources` and reuse an existing matching `target_app_key + view_key/chart_key`; repeated upsert can create duplicates because `client_key` is only valid inside one apply call.
51
+ - `app_charts_apply`: create/edit/remove/reorder app-source QingBI report bodies/configs with `dataSourceType=qingflow`; it does not create/edit dataset BI reports and does not attach reports to Qingflow app associated-resource display. Use `patch_charts` for existing-chart parameter replacement; use `upsert_charts` for creation or full target config; supports `target/table` aliases plus QingBI chart types such as `summary`, `columnar`, `area`, `funnel`, `radar`, `scatter`, `dualaxes`, and `map`; charts are immediate-live and do not publish; use `chart_id` when names are not unique
52
52
  - `portal_apply`: create or replace-update portal pages; use `dash_key` for update mode or `package_id + dash_name` for create mode; edit mode may omit `sections` for base-info-only updates; when sections are supplied they still use replace semantics
53
53
 
54
54
  For object-level updates, the safe partial syntax is `patch_*` with the object's real selector field plus `set` and optional `unset`. `selector` is only a concept, not a literal key. Examples: `patch_views: [{"view_key": "VIEW_KEY", "set": {...}}]`, `patch_buttons: [{"button_id": 1001, "set": {...}}]`, `patch_resources: [{"associated_item_id": 123, "set": {...}}]`, `patch_charts: [{"chart_id": 456, "set": {...}}]`. The tool reads the current backend config, merges the patch, then submits the full backend payload internally. Do not send a partial `upsert_*` and expect missing required fields to be preserved.
@@ -75,6 +75,8 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
75
75
  `builder_tool_contract -> app_get_fields -> app_get_views -> app_views_apply.patch_views/upsert_views -> app_get_views`
76
76
  - Add QingBI charts:
77
77
  `builder_tool_contract -> app_get_fields -> app_get_charts -> app_charts_apply.patch_charts/upsert_charts -> app_get_charts`
78
+ - Show an existing QingBI chart inside a Qingflow app/view:
79
+ `app_get -> app_associated_resources_apply.upsert_resources/patch_resources + view_configs -> app_get`
78
80
  - Create or update a portal:
79
81
  `builder_tool_contract -> portal_get -> portal_apply -> portal_get`
80
82
 
@@ -30,7 +30,7 @@ Canonical rules before any example:
30
30
  - Treat `fields` only as a legacy alias the MCP may normalize, not as the preferred shape
31
31
  - Use `filters` with canonical keys `field_name`, `operator`, `value`/`values`
32
32
  - Use `query_conditions` for the frontend query panel. Do not put query-panel fields into `filters`.
33
- - Use `app_associated_resources_apply` for the frontend associated report/view area. `associated_item_id` must be copied from `app_get.associated_resources[].associated_item_id`; it is not `chart_id` or `chart_key`. Do not write backend raw `sourceType`; reports default to BI app reports, and dataset reports use `report_source="dataset"`. Use `match_mappings` for associated view/report filters; read `match-rules.md` if field type compatibility is unclear.
33
+ - Use `app_associated_resources_apply` for the frontend associated report/view area. `associated_item_id` is the internal associated-resource id from `app_get.associated_resources`; `view_configs`, remove, and reorder may also pass an existing resource's `chart_id`/`chart_key`/`view_key`, which the tool resolves to the internal id. Do not write backend raw `sourceType`; reports default to BI app reports, and dataset reports use `report_source="dataset"`. Use `match_mappings` for associated view/report filters; read `match-rules.md` if field type compatibility is unclear.
34
34
  - For gantt, use `start_field`, `end_field`, and optionally `title_field`
35
35
  - If `app_get.views` or `app_get_views` shows duplicate view names, include `view_key` in `upsert_views[]` and update that exact target
36
36
  - Builder view writes always use the raw `view_key` from `app_get.views[].view_key`, such as `emsrao25rs02`. Do not pass `custom:emsrao25rs02`; that prefixed form is only for record-data `view_id`.
@@ -116,7 +116,7 @@ View associated resources example:
116
116
  }
117
117
  ```
118
118
 
119
- Use `{"visible": true, "limit_type": "all"}` to show all app-level associated resources, and `{"visible": false}` to hide the area. The ids above must come from `app_get.associated_resources`. Before creating a resource, check whether the same `target_app_key + view_key/chart_key` already exists; if it does, use `patch_resources` with that `associated_item_id`. If you create a new associated item in the same call, give it a `client_key` and reference it from `view_configs[].associated_item_refs`; `client_key` is not persisted and cannot deduplicate a later call.
119
+ Use `{"visible": true, "limit_type": "all"}` to show all app-level associated resources, and `{"visible": false}` to hide the area. The ids above can be internal `associated_item_id` values or existing resource `chart_id`/`chart_key`/`view_key` values from `app_get.associated_resources`. Before creating a resource, check whether the same `target_app_key + view_key/chart_key` already exists; if it does, use `patch_resources` with that `associated_item_id`. Dataset BI reports must already exist in QingBI and should be attached with `report_source="dataset"`; do not try to create them through `app_charts_apply`. If you create a new associated item in the same call, give it a `client_key` and reference it from `view_configs[].associated_item_refs`; `client_key` is not persisted and cannot deduplicate a later call.
120
120
 
121
121
  Create and show a BI indicator-card report in the same call:
122
122
 
@@ -142,10 +142,34 @@ class PublicButtonPlacement(str, Enum):
142
142
 
143
143
  class PublicChartType(str, Enum):
144
144
  target = "target"
145
+ indicator = "indicator"
146
+ summary = "summary"
145
147
  pie = "pie"
146
148
  bar = "bar"
149
+ columnar = "columnar"
147
150
  line = "line"
148
151
  table = "table"
152
+ detail = "detail"
153
+ area = "area"
154
+ stacked_area = "stacked_area"
155
+ pct_stack_area = "pct_stack_area"
156
+ funnel = "funnel"
157
+ waterfall = "waterfall"
158
+ gauge = "gauge"
159
+ heatmap = "heatmap"
160
+ histogram = "histogram"
161
+ treemap = "treemap"
162
+ radar = "radar"
163
+ stacked_bar = "stacked_bar"
164
+ pct_stack_bar = "pct_stack_bar"
165
+ stacked_column = "stacked_column"
166
+ pct_stack_col = "pct_stack_col"
167
+ scatter = "scatter"
168
+ ring = "ring"
169
+ rose = "rose"
170
+ dualaxes = "dualaxes"
171
+ map = "map"
172
+ timeline = "timeline"
149
173
 
150
174
 
151
175
  class LayoutApplyMode(str, Enum):
@@ -1576,8 +1600,8 @@ class AssociatedResourcesApplyRequest(StrictModel):
1576
1600
  app_key: str
1577
1601
  upsert_resources: list[AssociatedResourceUpsertPatch] = Field(default_factory=list)
1578
1602
  patch_resources: list[AssociatedResourcePartialPatch] = Field(default_factory=list)
1579
- remove_associated_item_ids: list[int] = Field(default_factory=list)
1580
- reorder_associated_item_ids: list[int] = Field(default_factory=list)
1603
+ remove_associated_item_ids: list[Any] = Field(default_factory=list)
1604
+ reorder_associated_item_ids: list[Any] = Field(default_factory=list)
1581
1605
  view_configs: list[AssociatedResourceViewConfigPatch] = Field(default_factory=list)
1582
1606
 
1583
1607
  @model_validator(mode="before")
@@ -1775,10 +1799,18 @@ class ChartUpsertPatch(StrictModel):
1775
1799
  normalized = raw_type.strip().lower()
1776
1800
  aliases = {
1777
1801
  "targetchart": PublicChartType.target.value,
1802
+ "indicatorchart": PublicChartType.indicator.value,
1803
+ "summarychart": PublicChartType.summary.value,
1778
1804
  "piechart": PublicChartType.pie.value,
1779
1805
  "barchart": PublicChartType.bar.value,
1806
+ "columnchart": PublicChartType.columnar.value,
1807
+ "columnarchart": PublicChartType.columnar.value,
1780
1808
  "linechart": PublicChartType.line.value,
1781
1809
  "tablechart": PublicChartType.table.value,
1810
+ "detailchart": PublicChartType.detail.value,
1811
+ "percent_stacked_column": PublicChartType.pct_stack_col.value,
1812
+ "percent_stacked_bar": PublicChartType.pct_stack_bar.value,
1813
+ "percent_stacked_area": PublicChartType.pct_stack_area.value,
1782
1814
  }
1783
1815
  if normalized in aliases:
1784
1816
  payload["chart_type"] = aliases[normalized]
@@ -3672,6 +3672,7 @@ class AiBuilderFacade:
3672
3672
  client_key_to_id: dict[str, int] = {}
3673
3673
  used_client_keys: set[str] = set()
3674
3674
  force_update_resource_ids: set[int] = set()
3675
+ resolved_associated_item_refs: dict[str, list[int]] = {}
3675
3676
 
3676
3677
  upsert_resources = list(request.upsert_resources)
3677
3678
  if request.patch_resources:
@@ -3778,34 +3779,35 @@ class AiBuilderFacade:
3778
3779
  else:
3779
3780
  upsert_ops.append({"operation": "create", "associated_item_id": None, "patch": patch, "index": index})
3780
3781
 
3781
- remove_ids = [
3782
- item_id
3783
- for item_id in (_coerce_positive_int(raw_id) for raw_id in request.remove_associated_item_ids)
3784
- if item_id is not None
3785
- ]
3782
+ remove_ids: list[int] = []
3786
3783
  for raw_id in request.remove_associated_item_ids:
3787
- item_id = _coerce_positive_int(raw_id)
3784
+ item_id, issue = _resolve_associated_resource_selector(
3785
+ raw_id,
3786
+ existing_resources=existing_resources,
3787
+ existing_by_id=existing_by_id,
3788
+ reason_path="remove_associated_item_ids",
3789
+ )
3788
3790
  if item_id is None:
3789
- blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
3791
+ blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "remove_associated_item_ids", "received": raw_id})
3790
3792
  continue
3791
- if item_id not in existing_by_id:
3792
- blocking_issues.append(_associated_resource_not_found_issue("remove_associated_item_ids", item_id, existing_by_id))
3793
- elif item_id in touched_ids:
3793
+ remove_ids.append(item_id)
3794
+ if item_id in touched_ids:
3794
3795
  blocking_issues.append(_duplicate_associated_resource_issue("remove_associated_item_ids", item_id))
3795
3796
  else:
3796
3797
  touched_ids.add(item_id)
3797
3798
 
3798
- reorder_ids = [
3799
- item_id
3800
- for item_id in (_coerce_positive_int(raw_id) for raw_id in request.reorder_associated_item_ids)
3801
- if item_id is not None
3802
- ]
3799
+ reorder_ids: list[int] = []
3803
3800
  for raw_id in request.reorder_associated_item_ids:
3804
- item_id = _coerce_positive_int(raw_id)
3801
+ item_id, issue = _resolve_associated_resource_selector(
3802
+ raw_id,
3803
+ existing_resources=existing_resources,
3804
+ existing_by_id=existing_by_id,
3805
+ reason_path="reorder_associated_item_ids",
3806
+ )
3805
3807
  if item_id is None:
3806
- blocking_issues.append({"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
3807
- elif item_id not in existing_by_id:
3808
- blocking_issues.append(_associated_resource_not_found_issue("reorder_associated_item_ids", item_id, existing_by_id))
3808
+ blocking_issues.append(issue or {"error_code": "ASSOCIATED_RESOURCE_NOT_FOUND", "reason_path": "reorder_associated_item_ids", "received": raw_id})
3809
+ continue
3810
+ reorder_ids.append(item_id)
3809
3811
 
3810
3812
  for index, view_config in enumerate(request.view_configs):
3811
3813
  refs = [str(ref or "").strip() for ref in view_config.associated_item_refs if str(ref or "").strip()]
@@ -3819,21 +3821,26 @@ class AiBuilderFacade:
3819
3821
  "message": "associated_item_refs must reference client_key values from upsert_resources in the same apply call",
3820
3822
  }
3821
3823
  )
3822
- invalid_ids = [
3823
- item_id
3824
- for item_id in view_config.associated_item_ids
3825
- if _coerce_positive_int(item_id) is None or _coerce_positive_int(item_id) not in existing_by_id
3826
- ]
3827
- if invalid_ids:
3828
- blocking_issues.append(
3829
- {
3830
- "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
3831
- "reason_path": f"view_configs[{index}].associated_item_ids",
3832
- "invalid_associated_item_ids": invalid_ids,
3833
- "available_associated_item_ids": sorted(existing_by_id),
3834
- "message": "view_configs.associated_item_ids must use app-level associated_item_id values from app_get",
3835
- }
3824
+ resolved_ids: list[int] = []
3825
+ for raw_id in view_config.associated_item_ids:
3826
+ item_id, issue = _resolve_associated_resource_selector(
3827
+ raw_id,
3828
+ existing_resources=existing_resources,
3829
+ existing_by_id=existing_by_id,
3830
+ reason_path=f"view_configs[{index}].associated_item_ids",
3836
3831
  )
3832
+ if item_id is None:
3833
+ blocking_issues.append(
3834
+ issue
3835
+ or {
3836
+ "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
3837
+ "reason_path": f"view_configs[{index}].associated_item_ids",
3838
+ "received": raw_id,
3839
+ }
3840
+ )
3841
+ continue
3842
+ resolved_ids.append(item_id)
3843
+ resolved_associated_item_refs[f"view_configs[{index}].associated_item_ids"] = resolved_ids
3837
3844
  raw_limit_type = str(view_config.limit_type or ("all" if view_config.visible else "")).strip().lower()
3838
3845
  if view_config.visible and raw_limit_type and raw_limit_type not in {"all", "select"}:
3839
3846
  blocking_issues.append(
@@ -3851,6 +3858,8 @@ class AiBuilderFacade:
3851
3858
  for issue in resource_match_issues
3852
3859
  ]
3853
3860
  )
3861
+ if resolved_associated_item_refs:
3862
+ normalized_args["resolved_associated_item_refs"] = deepcopy(resolved_associated_item_refs)
3854
3863
 
3855
3864
  if blocking_issues:
3856
3865
  return finalize(
@@ -4008,12 +4017,13 @@ class AiBuilderFacade:
4008
4017
  resources_after = []
4009
4018
 
4010
4019
  for index, view_config in enumerate(request.view_configs):
4020
+ resolved_view_config_ids = resolved_associated_item_refs.get(f"view_configs[{index}].associated_item_ids", [])
4011
4021
  selected_ids = [
4012
4022
  item_id
4013
4023
  for item_id in (
4014
4024
  _coerce_positive_int(raw_id)
4015
4025
  for raw_id in [
4016
- *view_config.associated_item_ids,
4026
+ *resolved_view_config_ids,
4017
4027
  *[client_key_to_id.get(str(ref or "").strip()) for ref in view_config.associated_item_refs],
4018
4028
  ]
4019
4029
  )
@@ -4115,6 +4125,7 @@ class AiBuilderFacade:
4115
4125
  str(index): _summarize_compiled_match_rules(rules)
4116
4126
  for index, rules in compiled_resource_match_rules.items()
4117
4127
  },
4128
+ "resolved_associated_item_refs": resolved_associated_item_refs,
4118
4129
  },
4119
4130
  "request_id": None,
4120
4131
  "suggested_next_call": None if verified else {"tool_name": "app_get", "arguments": {"profile": profile, "app_key": app_key}},
@@ -9657,6 +9668,13 @@ class AiBuilderFacade:
9657
9668
 
9658
9669
  for patch in upsert_charts:
9659
9670
  try:
9671
+ dataset_source = _chart_patch_dataset_source_type(patch)
9672
+ if dataset_source:
9673
+ raise ValueError(
9674
+ "app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
9675
+ f"dataset report source '{dataset_source}' is not supported for creation/update yet. "
9676
+ "Create the dataset report in QingBI first, then attach it with app_associated_resources_apply using report_source='dataset'."
9677
+ )
9660
9678
  config_update_requested = _chart_patch_updates_chart_config(patch)
9661
9679
  chart_visible_auth = (
9662
9680
  self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
@@ -9680,6 +9698,13 @@ class AiBuilderFacade:
9680
9698
  existing_name = str((existing or {}).get("chartName") or "").strip()
9681
9699
  existing_type = _normalize_backend_chart_type((existing or {}).get("chartType"))
9682
9700
  target_type = _map_public_chart_type_to_backend(patch.chart_type)
9701
+ existing_source_type = _chart_item_dataset_source_type(existing or {})
9702
+ if existing_source_type:
9703
+ raise ValueError(
9704
+ "app_charts_apply only creates or edits app-source QingBI reports with dataSourceType=qingflow; "
9705
+ f"existing chart '{chart_id or patch.name}' uses dataset report source '{existing_source_type}' and is not supported for update yet. "
9706
+ "Update it in QingBI directly, then attach the existing report with app_associated_resources_apply using report_source='dataset'."
9707
+ )
9683
9708
  if existing is None:
9684
9709
  temp_chart_id = str(patch.chart_id or f"mcp_{uuid4().hex[:16]}")
9685
9710
  create_payload = {
@@ -12337,13 +12362,16 @@ def _bi_field_id_for_field(*, app_key: str, field: dict[str, Any], qingbi_fields
12337
12362
 
12338
12363
 
12339
12364
  def _map_public_chart_type_to_backend(chart_type: PublicChartType) -> str:
12340
- return {
12365
+ aliases = {
12341
12366
  PublicChartType.target: "indicator",
12367
+ PublicChartType.indicator: "indicator",
12342
12368
  PublicChartType.pie: "pie",
12343
12369
  PublicChartType.bar: "bar",
12344
12370
  PublicChartType.line: "line",
12345
12371
  PublicChartType.table: "detail",
12346
- }[chart_type]
12372
+ PublicChartType.detail: "detail",
12373
+ }
12374
+ return aliases.get(chart_type, chart_type.value)
12347
12375
 
12348
12376
 
12349
12377
  _CHART_PARTIAL_PATCH_KEY_ALIASES = {
@@ -12579,6 +12607,25 @@ def _build_public_metric_fields(
12579
12607
  return metrics or [_default_public_total_metric()]
12580
12608
 
12581
12609
 
12610
+ def _split_axis_metric_fields(metrics: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
12611
+ if not metrics:
12612
+ default_metric = _default_public_total_metric()
12613
+ return [deepcopy(default_metric)], [deepcopy(default_metric)]
12614
+ if len(metrics) == 1:
12615
+ return [deepcopy(metrics[0])], [deepcopy(metrics[0])]
12616
+ return [deepcopy(metrics[0])], [deepcopy(metrics[1])]
12617
+
12618
+
12619
+ def _two_gauge_metric_fields(metrics: list[dict[str, Any]], *, requested_metric_count: int) -> list[dict[str, Any]]:
12620
+ if not metrics:
12621
+ return []
12622
+ if len(metrics) == 1:
12623
+ if requested_metric_count <= 0:
12624
+ return [deepcopy(metrics[0])]
12625
+ return [deepcopy(metrics[0]), _default_public_total_metric()]
12626
+ return [deepcopy(metrics[0]), deepcopy(metrics[1])]
12627
+
12628
+
12582
12629
  def _build_public_chart_filter_matrix(
12583
12630
  rules: list[Any],
12584
12631
  *,
@@ -12647,23 +12694,28 @@ def _build_public_chart_config_payload(
12647
12694
  for selector in list(config.pop("query_condition_field_ids", []) or []):
12648
12695
  field = _resolve_public_field(selector, field_lookup=field_lookup)
12649
12696
  query_condition_field_ids.append(_bi_field_id_for_field(app_key=app_key, field=field, qingbi_fields_by_id=qingbi_fields_by_id))
12697
+ backend_chart_type = _map_public_chart_type_to_backend(patch.chart_type)
12698
+ if backend_chart_type == "gauge" and not patch.indicator_field_ids and "selectedMetrics" not in config:
12699
+ raise ValueError("gauge charts require at least one indicator_field_ids value; pass one metric and the CLI will pair it with 数据总量")
12700
+ selected_dimensions = _build_public_dimension_fields(
12701
+ patch.dimension_field_ids,
12702
+ app_key=app_key,
12703
+ field_lookup=field_lookup,
12704
+ qingbi_fields_by_id=qingbi_fields_by_id,
12705
+ )
12706
+ selected_metrics = _build_public_metric_fields(
12707
+ patch.indicator_field_ids,
12708
+ app_key=app_key,
12709
+ field_lookup=field_lookup,
12710
+ qingbi_fields_by_id=qingbi_fields_by_id,
12711
+ aggregate=aggregate,
12712
+ )
12650
12713
  payload: dict[str, Any] = {
12651
12714
  "chartName": patch.name,
12652
- "chartType": _map_public_chart_type_to_backend(patch.chart_type),
12715
+ "chartType": backend_chart_type,
12653
12716
  "dataSource": {"dataSourceId": app_key, "dataSourceType": "qingflow"},
12654
- "selectedDimensions": _build_public_dimension_fields(
12655
- patch.dimension_field_ids,
12656
- app_key=app_key,
12657
- field_lookup=field_lookup,
12658
- qingbi_fields_by_id=qingbi_fields_by_id,
12659
- ),
12660
- "selectedMetrics": _build_public_metric_fields(
12661
- patch.indicator_field_ids,
12662
- app_key=app_key,
12663
- field_lookup=field_lookup,
12664
- qingbi_fields_by_id=qingbi_fields_by_id,
12665
- aggregate=aggregate,
12666
- ),
12717
+ "selectedDimensions": selected_dimensions,
12718
+ "selectedMetrics": selected_metrics,
12667
12719
  "beforeAggregationFilterMatrix": before_filters,
12668
12720
  "afterAggregationFilterMatrix": after_filters,
12669
12721
  "chartStyleConfigs": deepcopy(config.pop("chartStyleConfigs", [])),
@@ -12679,6 +12731,23 @@ def _build_public_chart_config_payload(
12679
12731
  "queryConditionStatus": bool(config.pop("queryConditionStatus", bool(query_condition_field_ids))),
12680
12732
  "queryConditionExact": bool(config.pop("queryConditionExact", False)),
12681
12733
  }
12734
+ if backend_chart_type == "summary":
12735
+ payload.pop("selectedDimensions", None)
12736
+ payload.setdefault("xDimensions", deepcopy(selected_dimensions))
12737
+ payload.setdefault("yDimensions", [])
12738
+ elif backend_chart_type == "scatter":
12739
+ x_metrics, y_metrics = _split_axis_metric_fields(selected_metrics)
12740
+ payload.pop("selectedMetrics", None)
12741
+ payload.setdefault("xMetrics", x_metrics)
12742
+ payload.setdefault("yMetrics", y_metrics)
12743
+ elif backend_chart_type == "dualaxes":
12744
+ left_metrics, right_metrics = _split_axis_metric_fields(selected_metrics)
12745
+ payload.pop("selectedMetrics", None)
12746
+ payload.setdefault("leftMetrics", left_metrics)
12747
+ payload.setdefault("rightMetrics", right_metrics)
12748
+ elif backend_chart_type == "gauge":
12749
+ payload["selectedDimensions"] = []
12750
+ payload["selectedMetrics"] = _two_gauge_metric_fields(selected_metrics, requested_metric_count=len(patch.indicator_field_ids or []))
12682
12751
  for key in (
12683
12752
  "selectedTime",
12684
12753
  "xDimensions",
@@ -12701,6 +12770,46 @@ def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
12701
12770
  return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
12702
12771
 
12703
12772
 
12773
+ def _chart_patch_dataset_source_type(patch: ChartUpsertPatch) -> str:
12774
+ config = patch.config if isinstance(patch.config, dict) else {}
12775
+ candidates = [
12776
+ config.get("dataSourceType"),
12777
+ config.get("data_source_type"),
12778
+ ]
12779
+ data_source = config.get("dataSource")
12780
+ if isinstance(data_source, dict):
12781
+ candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
12782
+ data_source = config.get("data_source")
12783
+ if isinstance(data_source, dict):
12784
+ candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
12785
+ for candidate in candidates:
12786
+ normalized = str(candidate or "").strip().lower()
12787
+ if not normalized:
12788
+ continue
12789
+ if normalized not in {"qingflow", "app"}:
12790
+ return normalized
12791
+ return ""
12792
+
12793
+
12794
+ def _chart_item_dataset_source_type(item: dict[str, Any]) -> str:
12795
+ candidates = [
12796
+ item.get("dataSourceType"),
12797
+ item.get("data_source_type"),
12798
+ item.get("sourceType"),
12799
+ item.get("source_type"),
12800
+ ]
12801
+ data_source = item.get("dataSource")
12802
+ if isinstance(data_source, dict):
12803
+ candidates.extend([data_source.get("dataSourceType"), data_source.get("data_source_type"), data_source.get("type")])
12804
+ for candidate in candidates:
12805
+ normalized = str(candidate or "").strip().lower()
12806
+ if not normalized:
12807
+ continue
12808
+ if normalized not in {"qingflow", "app", "bi_qingflow"}:
12809
+ return normalized
12810
+ return ""
12811
+
12812
+
12704
12813
  def _build_public_portal_base_payload(
12705
12814
  *,
12706
12815
  dash_name: str,
@@ -12771,16 +12880,33 @@ def _normalize_backend_chart_type(value: Any) -> str:
12771
12880
  "12": "dualaxes",
12772
12881
  "13": "map",
12773
12882
  "14": "timeline",
12883
+ "15": "area",
12884
+ "16": "stacked_column",
12885
+ "17": "stacked_bar",
12886
+ "18": "rose",
12887
+ "19": "stacked_area",
12888
+ "20": "pct_stack_col",
12889
+ "21": "pct_stack_bar",
12890
+ "22": "pct_stack_area",
12891
+ "23": "waterfall",
12892
+ "24": "gauge",
12893
+ "25": "heatmap",
12894
+ "26": "histogram",
12895
+ "27": "treemap",
12896
+ }
12897
+ aliases = {
12898
+ "percent_stacked_column": "pct_stack_col",
12899
+ "percent_stacked_bar": "pct_stack_bar",
12900
+ "percent_stacked_area": "pct_stack_area",
12774
12901
  }
12775
- return by_code.get(raw, raw.lower())
12902
+ normalized = by_code.get(raw, raw.lower())
12903
+ return aliases.get(normalized, normalized)
12776
12904
 
12777
12905
 
12778
12906
  def _public_chart_type_from_backend(value: Any) -> str:
12779
12907
  normalized = _normalize_backend_chart_type(value)
12780
12908
  return {
12781
12909
  "indicator": PublicChartType.target.value,
12782
- "summary": PublicChartType.target.value,
12783
- "columnar": PublicChartType.bar.value,
12784
12910
  "detail": PublicChartType.table.value,
12785
12911
  }.get(normalized, normalized)
12786
12912
 
@@ -20129,7 +20255,7 @@ def _build_view_associated_resources_payload(
20129
20255
  "missing_fields": [],
20130
20256
  "received": raw_item_id,
20131
20257
  "message": "associated_item_id must be an app-level associated resource id from app_get.associated_resources",
20132
- "next_action": "call app_get and use associated_resources[].associated_item_id, not chart_id",
20258
+ "next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
20133
20259
  }
20134
20260
  ]
20135
20261
  item_ids.append(item_id)
@@ -20143,7 +20269,7 @@ def _build_view_associated_resources_payload(
20143
20269
  "invalid_associated_item_ids": missing_ids,
20144
20270
  "available_associated_item_ids": sorted(available_by_id),
20145
20271
  "message": "associated_resource references ids that are not in the app-level associated resource pool",
20146
- "next_action": "call app_get and use associated_resources[].associated_item_id, not chart_id",
20272
+ "next_action": "prefer app_associated_resources_apply for associated report/view display; it can resolve associated_item_id, chart_id/chart_key, or view_key",
20147
20273
  }
20148
20274
  ]
20149
20275
  payload = {
@@ -20561,6 +20687,77 @@ def _associated_resource_upsert_payload_from_existing_item(
20561
20687
  return _compact_dict(payload)
20562
20688
 
20563
20689
 
20690
+ def _resolve_associated_resource_selector(
20691
+ selector: Any,
20692
+ *,
20693
+ existing_resources: list[dict[str, Any]],
20694
+ existing_by_id: dict[int, dict[str, Any]],
20695
+ reason_path: str,
20696
+ ) -> tuple[int | None, dict[str, Any] | None]:
20697
+ item_id = _coerce_positive_int(selector)
20698
+ if item_id is not None:
20699
+ if item_id in existing_by_id:
20700
+ return item_id, None
20701
+ raw = str(selector or "").strip()
20702
+ if not raw:
20703
+ return None, {
20704
+ "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
20705
+ "reason_path": reason_path,
20706
+ "received": selector,
20707
+ "available_associated_item_ids": sorted(existing_by_id),
20708
+ "message": "associated resource selector cannot be empty",
20709
+ }
20710
+ matches: list[dict[str, Any]] = []
20711
+ for resource in existing_resources:
20712
+ if not isinstance(resource, dict):
20713
+ continue
20714
+ candidates = [
20715
+ resource.get("chart_id"),
20716
+ resource.get("chart_key"),
20717
+ resource.get("view_key"),
20718
+ resource.get("associated_item_id"),
20719
+ ]
20720
+ if raw in {str(candidate).strip() for candidate in candidates if candidate not in {None, ""}}:
20721
+ matches.append(resource)
20722
+ if len(matches) == 1:
20723
+ resolved_id = _coerce_positive_int(matches[0].get("associated_item_id"))
20724
+ if resolved_id is not None:
20725
+ return resolved_id, None
20726
+ if len(matches) > 1:
20727
+ return None, {
20728
+ "error_code": "AMBIGUOUS_ASSOCIATED_RESOURCE",
20729
+ "reason_path": reason_path,
20730
+ "received": selector,
20731
+ "candidate_associated_item_ids": [
20732
+ item_id
20733
+ for item_id in (_coerce_positive_int(item.get("associated_item_id")) for item in matches)
20734
+ if item_id is not None
20735
+ ],
20736
+ "message": "selector matches multiple associated resources; pass associated_item_id",
20737
+ }
20738
+ return None, {
20739
+ "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
20740
+ "reason_path": reason_path,
20741
+ "received": selector,
20742
+ "available_associated_item_ids": sorted(existing_by_id),
20743
+ "available_chart_ids": sorted(
20744
+ {
20745
+ str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
20746
+ for resource in existing_resources
20747
+ if str(resource.get("chart_id") or resource.get("chart_key") or "").strip()
20748
+ }
20749
+ ),
20750
+ "available_view_keys": sorted(
20751
+ {
20752
+ str(resource.get("view_key") or "").strip()
20753
+ for resource in existing_resources
20754
+ if str(resource.get("view_key") or "").strip()
20755
+ }
20756
+ ),
20757
+ "message": "selector must be an associated_item_id, QingBI chart_id/chart_key, or Qingflow view_key from the app-level associated resource pool",
20758
+ }
20759
+
20760
+
20564
20761
  def _associated_resource_not_found_issue(reason_path: str, associated_item_id: int, existing_by_id: dict[int, dict[str, Any]]) -> dict[str, Any]:
20565
20762
  return {
20566
20763
  "error_code": "ASSOCIATED_RESOURCE_NOT_FOUND",
@@ -20568,7 +20765,7 @@ def _associated_resource_not_found_issue(reason_path: str, associated_item_id: i
20568
20765
  "associated_item_id": associated_item_id,
20569
20766
  "available_associated_item_ids": sorted(existing_by_id),
20570
20767
  "message": "associated_item_id is not in the app-level associated resource pool",
20571
- "next_action": "call app_get and use associated_resources[].associated_item_id; do not pass chart_id/chart_key",
20768
+ "next_action": "call app_get and use associated_resources[].associated_item_id, chart_id/chart_key, or view_key",
20572
20769
  }
20573
20770
 
20574
20771
 
@@ -43,8 +43,9 @@ def build_builder_server() -> FastMCP:
43
43
  "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. "
44
44
  "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. "
45
45
  "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
46
+ "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
46
47
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
47
- "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, not chart_id/chart_key. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
48
+ "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
48
49
  "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
49
50
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
50
51
  "For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
@@ -2976,6 +2976,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2976
2976
  "graphType": "graph_type",
2977
2977
  "targetAppKey": "target_app_key",
2978
2978
  "chartKey": "chart_key",
2979
+ "chartId": "chart_key",
2979
2980
  "viewKey": "view_key",
2980
2981
  "viewgraphKey": "view_key",
2981
2982
  "reportSource": "report_source",
@@ -2990,12 +2991,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2990
2991
  "view_configs[].limit_type": ["all", "select"],
2991
2992
  },
2992
2993
  "execution_notes": [
2994
+ "this tool manages Qingflow in-app associated report/view display; it does not create or edit QingBI report bodies/configs",
2995
+ "create or edit app-source BI report bodies first with app_charts_apply, then attach the resulting chart_id here with graph_type=chart; dataset BI reports can only be attached when they already exist",
2993
2996
  "this is the default associated report/view path; it manages both the app-level associated resource pool and per-view display config",
2994
2997
  "use patch_resources for partial parameter replacement on existing associated resources; the tool reads the current resource including backend-required raw fields, merges patch_resources[].set/unset, then submits the backend full-save payload internally",
2995
- "associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; it is not chart_id, chart_key, or view_key",
2998
+ "associated_item_id is form_asos_chart.id from app_get.associated_resources[].associated_item_id; view_configs/remove/reorder may also pass an existing associated resource's chart_id/chart_key/view_key and the tool resolves it to the internal id",
2996
2999
  "before creating an associated resource, read app_get.associated_resources and reuse an existing item with patch_resources when target_app_key + view_key/chart_key already matches; repeated upsert_resources without associated_item_id can create duplicate associated items",
2997
3000
  "graph_type=view uses view_key and internally compiles to the Qingflow view source; graph_type=chart uses chart_key and defaults to report_source=app",
2998
- "report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET; do not pass raw backend sourceType",
3001
+ "report_source=app maps to BI_QINGFLOW; report_source=dataset maps to BI_DATASET for associating an existing dataset report; do not pass raw backend sourceType",
2999
3002
  "use match_mappings for associated view/report filtering; dynamic conditions use source_field and static conditions use value",
3000
3003
  "match_mappings.source_field accepts source schema fields plus system fields 数据ID(-17) and 编号(0); match_mappings compiles to backend matchRules",
3001
3004
  "do not write raw match_rules unless preserving a legacy backend config; match_mappings and match_rules are mutually exclusive",
@@ -3673,7 +3676,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3673
3676
  "can_edit_form covers form/schema routes only and does not imply app base-info writes",
3674
3677
  "returns normalized app visibility when backend auth is readable",
3675
3678
  "custom_buttons[].button_id is the id required by app_custom_buttons_apply view_configs[].buttons[].button_ref",
3676
- "associated_resources[].associated_item_id is the id required by app_associated_resources_apply.view_configs.associated_item_ids; do not pass chart_id there",
3679
+ "associated_resources[].associated_item_id is the internal id; app_associated_resources_apply.view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key",
3677
3680
  ],
3678
3681
  "minimal_example": {
3679
3682
  "profile": "default",
@@ -3797,6 +3800,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3797
3800
  **deepcopy(_VISIBILITY_ALLOWED_VALUES),
3798
3801
  },
3799
3802
  "execution_notes": [
3803
+ "this tool manages QingBI report bodies/configs; it does not attach reports to Qingflow app associated-resource display",
3804
+ "app_charts_apply creates/updates app-source QingBI reports only; generated payloads use dataSourceType=qingflow",
3805
+ "dataset BI reports are not created or edited by this tool yet; create them in QingBI first, then attach the existing report with app_associated_resources_apply report_source=dataset",
3806
+ "after creating or updating an app-source report body, use app_associated_resources_apply when the report should appear inside a Qingflow app/view",
3800
3807
  "app_charts_apply is immediate-live and does not publish",
3801
3808
  "use patch_charts for partial parameter replacement on existing charts; the tool reads current chart base/config, merges patch_charts[].set/unset, then submits full QingBI base/config payloads internally",
3802
3809
  "chart matching precedence is chart_id first, then exact unique chart name",